Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions packages/codev-sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,38 @@ Guest image contract for vz (image pipeline): the init must mount the
the server bundle with its env and args, and bridge vsock port 4096 to the
server's TCP port and TCP port 8788 to vsock (e.g. via socat) for the broker.

Still to come: the Windows managed-WSL provider, the desktop main-process
wiring, and the `packages/app` UI. Runtime code sticks to Node APIs (no
Bun-only calls) so it can run inside the Electron main process.
The image pipeline lives in `image/` and builds every backend's guest from
one Debian rootfs, fully containerized (any machine with Docker):

- `image/Dockerfile` — base toolchain (node 22, python3 + pip/venv,
build-essential, git, ripgrep/jq/sqlite3/curl/unzip, socat, iproute2) plus
the optional office layer: `image/office/install-office.sh` translates the
codev-office setup script system-wide — pandoc + LibreOffice + fonts from
Debian, .NET 8 SDK at `/opt/dotnet`, the pinned Python package set, the
bundle's pure-JS node-module trees on `NODE_PATH`, and the office skills at
`/opt/codev-skills` with an `/etc/skel` codev.json pointing at them. Fresh
sandbox homes get skel (vz at image build; bwrap seeded by the boot
script), and images declare extra env in `/etc/codev-guest.env`.
- `image/guest/codev-init` — the vz guest's PID 1: mounts the virtio-fs tags,
runs the socat vsock bridges (4096 server / 8788 broker, host CID 2), and
execs the server bundle per `boot.json`; powers off when the server dies.
- `image/kernel/` — a minimal pinned kernel (6.6 LTS) with virtio
blk/fs/vsock/console + ext4 compiled in: no initrd, no modules, no network
drivers. Cached by Docker layers after the first build.
- `script/build-image.ts` — orchestrates buildx → rootfs tar → ext4 disk
(`mkfs.ext4 -d`, no root needed) → kernel → `vz-<arch>.tar.gz` /
`bwrap-<arch>.tar.gz` → `manifest.json` (validated against
`src/manifest.ts` before writing). `--office-bundle` stages the
arch-independent parts of codev-office-ubuntu.zip; binaries are fetched
arch-correct inside the build. Build each arch on a native host — x64 under
emulation works but is slow.
- `script/publish-image.ts` — uploads artifacts first, `manifest.json` last
(clients never see a version whose artifacts aren't in place), via `mc`
when `CODEV_STORAGE_TARGET` is set, otherwise prints the commands.
- `script/e2e-vz.ts` — boots a locally built image through `codev-vmm` and
waits for the authenticated health check: the full-stack smoke test.

Still to come: the Windows managed-WSL provider (consumes the same rootfs tar
via `wsl --import`), the desktop main-process wiring, and the `packages/app`
UI. Runtime code sticks to Node APIs (no Bun-only calls) so it can run inside
the Electron main process.
38 changes: 38 additions & 0 deletions packages/codev-sandbox/image/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Guest image for the codev sandbox: one Debian rootfs consumed by all
# backends (bwrap tar, vz ext4 disk, later wsl --import tar).
# Built by script/build-image.ts via docker buildx; TARGETARCH is amd64|arm64.

FROM debian:bookworm-slim@sha256:abd67ffcfa541b485a3dff59865ab629aa048a6c613e639d36e7456b0b229241 AS base
ARG DEBIAN_FRONTEND=noninteractive
# Base toolchain: node/git/socat/ca-certs always; python + build tools + the
# CLI set per the sandbox plan. iproute2 is needed by the vz init (loopback).
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl wget git unzip zip jq ripgrep sqlite3 socat iproute2 \
procps less vim-tiny openssh-client \
python3 python3-pip python3-venv python3-dev \
build-essential pkg-config \
&& rm -rf /var/lib/apt/lists/*
ARG TARGETARCH
ARG NODE_VERSION=v22.15.0
RUN set -eux; \
case "$TARGETARCH" in amd64) NODE_ARCH=x64 ;; arm64) NODE_ARCH=arm64 ;; *) echo "unsupported arch: $TARGETARCH" >&2; exit 1 ;; esac; \
curl -fsSL "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" \
| tar -xJ -C /usr/local --strip-components=1; \
node --version
RUN useradd --create-home --shell /bin/bash --uid 1000 codev

# Office layer: the codev-office skill dependencies (dotnet/pandoc/libreoffice/
# python packages/node module trees/skills), system-wide instead of per-user
# because sandbox homes are fresh. Skipped when OFFICE=0.
FROM base AS office
ARG OFFICE=1
COPY office/install-office.sh /tmp/install-office.sh
COPY office/bundle/ /tmp/office-bundle/
RUN if [ "$OFFICE" = "1" ]; then bash /tmp/install-office.sh /tmp/office-bundle; fi \
&& rm -rf /tmp/office-bundle /tmp/install-office.sh

# Guest layer: the vz init (PID 1) and the mount points every backend expects.
# Harmless in the bwrap/wsl tars.
FROM office AS guest
COPY guest/codev-init /sbin/codev-init
RUN chmod 755 /sbin/codev-init && mkdir -p /workspace /opt/codev /codev-config
44 changes: 44 additions & 0 deletions packages/codev-sandbox/image/guest/codev-init
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
# PID 1 of the vz sandbox guest (kernel cmdline: init=/sbin/codev-init).
# Mounts the virtio-fs shares, bridges vsock to loopback TCP, and runs the
# codev server per /codev-config/boot.json. The guest has no NIC: socat over
# vsock (host CID 2) is the only way bytes leave this machine.
set -u

mount -t proc proc /proc
mount -t sysfs sys /sys
mountpoint -q /dev || mount -t devtmpfs dev /dev
mkdir -p /dev/pts /dev/shm
mount -t devpts devpts /dev/pts 2>/dev/null || true
mount -t tmpfs tmpfs /dev/shm 2>/dev/null || true
mount -t tmpfs tmpfs /tmp
mount -t virtiofs workspace /workspace
mount -t virtiofs bundle /opt/codev
mount -t virtiofs config /codev-config
ip link set lo up

[ -f /etc/codev-guest.env ] && . /etc/codev-guest.env

# Server path: host connects vsock:4096 -> guest loopback TCP. Broker path:
# guest loopback TCP:8788 -> host vsock:8788 (the egress broker).
socat VSOCK-LISTEN:4096,fork,reuseaddr TCP:127.0.0.1:4096 &
socat TCP-LISTEN:8788,bind=127.0.0.1,fork,reuseaddr VSOCK-CONNECT:2:8788 &

eval "$(node -e '
const boot = require("/codev-config/boot.json")
for (const [key, value] of Object.entries(boot.env)) console.log("export " + key + "=" + JSON.stringify(value))
console.log("CODEV_SERVE_ARGS=" + JSON.stringify(boot.serveArgs.join(" ")))
')"

export HOME=/home/codev
cd /workspace
node /opt/codev/node.js $CODEV_SERVE_ARGS &
SERVER=$!

# Stay PID 1: reap orphans, power off when the server dies so the helper's
# guestDidStop fires and the provider sees a clean exit.
while :; do
wait -n 2>/dev/null
kill -0 "$SERVER" 2>/dev/null || break
done
poweroff -f
22 changes: 22 additions & 0 deletions packages/codev-sandbox/image/kernel/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Minimal sandbox guest kernel: virtio blk/fs/vsock/console + ext4 compiled
# in, no modules, no initrd, no network drivers. Built per-arch by
# script/build-image.ts; docker layer caching makes rebuilds cheap unless
# KERNEL_VERSION or the fragment changes.
FROM debian:bookworm-slim@sha256:abd67ffcfa541b485a3dff59865ab629aa048a6c613e639d36e7456b0b229241 AS build
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential bc bison flex libssl-dev libelf-dev python3 curl xz-utils kmod cpio ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ARG KERNEL_VERSION=6.6.100
WORKDIR /src
RUN curl -fsSL "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${KERNEL_VERSION}.tar.xz" | tar -xJ --strip-components=1
COPY codev.config /src/codev.config
RUN make defconfig \
&& scripts/kconfig/merge_config.sh -m .config codev.config \
&& make olddefconfig
RUN make -j"$(nproc)" $(case "$(uname -m)" in aarch64) echo Image ;; x86_64) echo bzImage ;; esac)
# Normalize the artifact name; the vz provider expects "vmlinux".
RUN mkdir /out && cp "$(case "$(uname -m)" in aarch64) echo arch/arm64/boot/Image ;; x86_64) echo arch/x86/boot/bzImage ;; esac)" /out/vmlinux

FROM scratch AS artifact
COPY --from=build /out/vmlinux /vmlinux
23 changes: 23 additions & 0 deletions packages/codev-sandbox/image/kernel/codev.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Merged over defconfig: everything the vz guest needs, compiled in (=y) so
# no initrd or module loading is required. No network device drivers — the
# guest's only transport is vsock.
CONFIG_VIRTIO=y
CONFIG_VIRTIO_MENU=y
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_MMIO=y
CONFIG_VIRTIO_BLK=y
CONFIG_VIRTIO_CONSOLE=y
CONFIG_HW_RANDOM_VIRTIO=y
CONFIG_VIRTIO_BALLOON=y
CONFIG_VSOCKETS=y
CONFIG_VIRTIO_VSOCKETS=y
CONFIG_FUSE_FS=y
CONFIG_VIRTIO_FS=y
CONFIG_EXT4_FS=y
CONFIG_EXT4_FS_POSIX_ACL=y
CONFIG_TMPFS=y
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
CONFIG_UNIX=y
CONFIG_INET=y
CONFIG_DAX=y
5 changes: 5 additions & 0 deletions packages/codev-sandbox/image/office/bundle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Staging dir: build-image.ts copies the office bundle's skill and node-module
# zips here per run. They are build inputs, not sources.
*
!.gitignore
!.keep
Empty file.
77 changes: 77 additions & 0 deletions packages/codev-sandbox/image/office/install-office.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# System-wide translation of codev-office-ubuntu-setup.sh for the sandbox
# guest image. Runs inside the Docker build as root; $1 is the staged bundle
# dir (skill zips + pure-JS node-module zips — arch-independent parts only;
# dotnet/pandoc/libreoffice/wheels are fetched arch-correct right here, since
# the BUILD machine has internet even though the sealed guest will not).
set -euxo pipefail
BUNDLE="${1:-/tmp/office-bundle}"
ARCH="$(dpkg --print-architecture)"

# pandoc + LibreOffice + fonts from Debian (available on amd64 and arm64).
apt-get update
apt-get install -y --no-install-recommends \
pandoc libreoffice-writer libreoffice-calc libreoffice-impress \
fonts-dejavu fonts-liberation2
rm -rf /var/lib/apt/lists/*

# .NET SDK 8 into /opt/dotnet (the docx/xlsx skills drive OpenXML with it).
case "$ARCH" in
amd64) DOTNET_ARCH=x64 ;;
arm64) DOTNET_ARCH=arm64 ;;
esac
curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
bash /tmp/dotnet-install.sh --channel 8.0 --install-dir /opt/dotnet --architecture "$DOTNET_ARCH"
rm /tmp/dotnet-install.sh
ln -sf /opt/dotnet/dotnet /usr/local/bin/dotnet
dotnet --list-sdks

# Python package set mirrored from the office bundle's wheel list, installed
# into the system python at build time (the guest never runs pip online).
pip3 install --break-system-packages --no-cache-dir \
pandas openpyxl 'markitdown[pptx]' reportlab pypdf matplotlib

# Prebuilt pure-JS node module trees from the bundle, merged into one tree on
# NODE_PATH (mirrors the setup script's ~/.node_modules require() fallback).
if compgen -G "$BUNDLE/*-node-modules.zip" >/dev/null; then
mkdir -p /opt/codev-office/node_modules /tmp/modules-staging
for mod_zip in "$BUNDLE"/*-node-modules.zip; do
unzip -q "$mod_zip" -d /tmp/modules-staging/current
if [ -d /tmp/modules-staging/current/node_modules ]; then
cp -R /tmp/modules-staging/current/node_modules/. /opt/codev-office/node_modules/
fi
rm -rf /tmp/modules-staging/current
done
fi

# Skills into a system path; /etc/skel seeds each fresh sandbox home with the
# config pointing at them (vz homes get skel at image build, bwrap homes are
# seeded by the provider boot script).
if compgen -G "$BUNDLE/*-skill.zip" >/dev/null; then
mkdir -p /opt/codev-skills
for skill_zip in "$BUNDLE"/*-skill.zip; do
name="$(basename "$skill_zip" .zip)"
name="${name%-skill}"
unzip -q "$skill_zip" -d "/opt/codev-skills/$name"
done
mkdir -p /etc/skel/.config/codev
cat > /etc/skel/.config/codev/codev.json <<'JSON'
{
"skills": {
"paths": ["/opt/codev-skills"]
}
}
JSON
fi

# Env the guest boot paths source (vz init and the bwrap boot script).
cat > /etc/codev-guest.env <<'ENV'
export DOTNET_ROOT=/opt/dotnet
export NODE_PATH=/opt/codev-office/node_modules
export DOTNET_CLI_TELEMETRY_OPTOUT=1
ENV

# The image build already populated /home/codev before this layer; re-apply
# skel so baked homes (vz disk) get the skills config too.
cp -Rn /etc/skel/. /home/codev/ 2>/dev/null || true
chown -R codev:codev /home/codev
6 changes: 6 additions & 0 deletions packages/codev-sandbox/image/tools/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Small helper image for rootfs->ext4 conversion and sparse-aware tarring;
# runs as root inside the container so file ownership survives, and gives the
# pipeline GNU tar + mkfs.ext4 -d regardless of the host OS.
FROM debian:bookworm-slim@sha256:abd67ffcfa541b485a3dff59865ab629aa048a6c613e639d36e7456b0b229241
RUN apt-get update && apt-get install -y --no-install-recommends e2fsprogs tar gzip \
&& rm -rf /var/lib/apt/lists/*
5 changes: 4 additions & 1 deletion packages/codev-sandbox/native/vzhelper/Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ final class GuestDialDelegate: NSObject, VZVirtioSocketListenerDelegate {
func buildConfiguration(_ config: VmConfig) throws -> VZVirtualMachineConfiguration {
let machine = VZVirtualMachineConfiguration()
let boot = VZLinuxBootLoader(kernelURL: URL(fileURLWithPath: config.kernel))
boot.initialRamdiskURL = URL(fileURLWithPath: config.initrd)
// Empty initrd means the kernel boots without one (virtio compiled in).
if !config.initrd.isEmpty {
boot.initialRamdiskURL = URL(fileURLWithPath: config.initrd)
}
boot.commandLine = config.cmdline
machine.bootLoader = boot
machine.cpuCount = min(
Expand Down
Loading
Loading