Files
sausage/userland/box

box

box runs builds and tests in disposable containers through the containers service at citadel.vpn.surma.technology:8093. All network operations use standard wasi:http. The component has no Sausage host imports.

Commands

box create [--flake reference] [--ttl seconds]
box exec ID [--timeout seconds] [--inline-bytes bytes] -- COMMAND ARGUMENTS...
box put ID WORLD_PATH BOX_PATH
box get ID BOX_PATH WORLD_PATH
box logs ID
box ls
box rm ID

Every command accepts --host hostname[:port]. The default port is 8093. Create remembers the host in /var/lib/box/hosts/<id>. Separate files prevent unrelated creates from replacing each other’s mappings. Other commands use the remembered host unless --host overrides it. Unknown IDs use the default service. Host mappings remain after deletion for diagnostics. They contain no credentials.

Create prints the ID and waits up to 240 seconds for readiness. Without a flake, it uses v2’s Node/NixOS configuration with Node, npm, Git, Chromium, fonts, and tar. A flake selects a service-side configuration, for example github:OWNER/REPO#container. TTL is positive seconds from creation. The service defaults to 3600 seconds. A failed or timed-out create reports its accepted ID. Do not repeat create blindly. Inspect logs and remove that ID.

Exec runs as root. It passes an argv array without shell interpolation. For shell syntax, use -- sh -c 'COMMAND'. The timeout defaults to 60 seconds and accepts 1 through 1800 seconds. Exec prints stdout and stderr separately and returns the command’s exit code. It does not consume local stdin. Use put for inputs.

Put detects a local directory. Get detects a remote directory. File transfers support binary bytes and a 64 MiB limit. Downloads replace a destination file only after complete validation. Tree transfers use an uncompressed tar with a 64 MiB limit. Put merges into a box directory. Get requires a new world directory and publishes it only after complete extraction. The port preserves v2’s portable file modes and hardlink materialization. Get rejects symlinks because the world VFS cannot create them. Export a dereferenced tree when necessary. Special files and paths outside the destination fail explicitly. Temporary tar files disappear after a transfer, including failures where cleanup remains possible.

Build a pnpm project in a box

id=$(box create --ttl 1800)
box put "$id" /repos/project /work
box exec "$id" --timeout 600 -- sh -c 'cd /work && npm install --global pnpm && pnpm install --frozen-lockfile && pnpm test && pnpm build'
box get "$id" /work/dist /repos/project-dist
box logs "$id"
box rm "$id"

Use the same ID to inspect failed builds. Always remove disposable boxes when the work ends. Boxes have open network access. The world’s host credential boundary remains unchanged.

Output retention

The CLI preserves v2’s full output archives independently of presentation. It downloads both raw streams for new-service executions, including small and binary output. Files live under /logs/box/YYYY-MM-DD/<id>-<exec>.stdout and .stderr. Collision snapshots receive unique suffixes. Existing archives never change. Result JSON retains original service excerpts before the CLI bounds presentation. The CLI prints archive paths and availability notices on stderr.

Each displayed stream defaults to a 16 KiB UTF-8 head and tail budget. The omission marker is outside that budget. --inline-bytes 0 prints only the marker for nonempty output. A larger budget reads archives, not an already-truncated service excerpt. UTF-8 replacement remains bounded. Full archives have no retention cap. Transfer limits do not cap execution archives.

Downloads request 1 MiB ranges and validate every Content-Range page and exact byte total. They also accept a complete 200 response from a server that ignores Range. A shared deadline allows 60 seconds plus one second per 256 KiB of combined output, capped at 30 minutes. Unknown totals receive five minutes. Only immutable archive GETs retry. Exec POSTs never retry. Incomplete archives retain .partial names. Only complete, flushed downloads receive published paths.

Build logs try /log/full. Only 404 or 405 triggers /log fallback. The CLI retains all available raw build bytes and displays at most their last 16 KiB. A fallback warning states that earlier bytes may be unavailable. Failed creates archive build diagnostics. Explicit removal attempts a build snapshot but proceeds if that snapshot fails.

Old services expose only JSON stream text. The CLI saves all available text before presentation limits. It warns when upstream truncation discarded bytes. Those missing bytes cannot be restored. Archive failures return the completed command and an availability notice. Never replay a command to repair its archive.

Build and validation

Build the shared startup object from the repository root:

nix develop -c nu --no-config-file userland/cwd-init/build.nu

Run the following commands from userland/ so Cargo loads the shared startup and numeric exit support:

df -h /
export CARGO_TARGET_DIR=/home/surma/src/gitea.surma.technology/surma/sausage/target/p1-tools
nix develop -c cargo build -p box-cli --target wasm32-wasip2 --release --locked
nix develop -c kache stats
nix develop -c nu box/test.nu

The existing userland/bundle.nu discovers the workspace member and copies its box binary into /bin. It applies the same cwd initializer as other bundled programs.

Keep the same target directory for tests. Do not override CARGO_HOME or RUSTC_WRAPPER. Stop before a build if disk use exceeds 85%. Run pure unit tests with the native target from rustc -vV, for example:

nix develop -c cargo test -p box-cli --target aarch64-unknown-linux-gnu --locked

The real-service script tests every required command and removes every created box, including after an assertion failure. It round-trips files and nested trees, runs a tiny Node project through npm test, and retrieves its result. It also verifies large binary archive retention and WASI-only component imports. The script preserves its temporary world and archives as evidence.