CubeSandbox feature audit

This is the inventory — Cube’s advertised features on the left, Coppice’s state on the right, one row each. Rows fall into four buckets: closed (measured, receipts attached), partial (the shape is there, edges aren’t), open (acknowledged gap with a test rig proposed), and N/A (the feature is Linux-kernel-shaped in a way that doesn’t translate and doesn’t need to). Cross-links back to parity-gaps where they exist; this page exists to make sure nothing in the README, the docs site, or the examples/ directory slipped between the seams.

Sources surveyed, pinned to commit c439bb513f5124d4d9389451b31b8aeb87ab539c: README.md; docs/ (VitePress site at docs.cubesandbox.ai); examples/code-sandbox-quickstart, browser-sandbox, cubesandbox-base-nginx, openclaw-integration, openai-agents-code-interpreter, openai-agents-example, mini-rl-training; E2B Python SDK surface (e2b + e2b-code-interpreter), cross-read against e2b-compat/src/routes.rs and e2b-compat/src/envd.rs.

Lifecycle & API

Cube featureCoppice statereceipt / note
Create sandbox (POST /sandboxes)closedAxum handler in e2b-compat/src/routes.rs::create_sandbox. SDK round-trip verified. See e2b-compat.
Destroy (DELETE /sandboxes/:id)closedTears down ipykernel, kills jail, releases pool entry. 10/10 SDK calls.
List (GET /sandboxes, v2 with filters)closedBoth /sandboxes and /v2/sandboxes with state + metadata filters.
Get detail (GET /sandboxes/:id)closedReturns the E2B SandboxDetail camelCase shape with configured CPU/RAM/disk caps, latest sampler usage, network policy, lifecycle, tags, and wildcard-DNS hostname.
Pause / resume / connectclosedSIGSTOP/SIGCONT for jail backend; bhyve backend uses bhyvectl —suspend. cc=1 resume 17 ms on bhyve-durable-prewarm-pool. See snapshot-cloning.
Timeout / TTL refreshclosedPOST /sandboxes/:id/timeout and /refreshes mutate end_at; the in-process reaper in e2b-compat/src/reaper.rs sweeps expired sandboxes every 10 s.
Metadata (arbitrary KV at create, filterable)closedCreateRequest.metadata: HashMap; v2 list filters on ?metadata=k=v.
Restart kernel (code-interpreter)closedPOST /contexts/:id/restart in envd.rs::restart_context SIGTERMs the tracked ipykernel PID, polls kill -0 up to 5 s for reap, then calls kernel::spawn_kernel() fresh and swaps the KernelInfo in state.kernels. Bumps coppice_kernel_exits_total + coppice_kernel_spawns_total each restart. Receipt: examples/02-persistent-kernel.py binds x = 42, calls sb.restart_code_context(ctx), asserts the follow-up print(x) returns a NameError (observed green 2026-04-22). SDK verified: e2b_code_interpreter’s Sandbox.restart_code_context() hits <jupyter_url>/contexts/<context_id>/restart.
Durable snapshot creation (POST /sandboxes/:id/snapshots)closed#durable (2026-04-22). Five endpoints wired: POST /sandboxes/:id/snapshots (capture), GET /snapshots + GET /snapshots/:id (list / show), POST /snapshots/:id/fork (clone into a fresh VNET jail), DELETE /snapshots/:id (409 if a fork still depends, 204 otherwise). Backend impl lives in e2b-compat/src/backend/freebsd_jail.rs: zfs snapshot on the sandbox’s dataset, metadata persisted atomically to /var/lib/coppice/snapshots.json, fork re-uses the shared stand_up_vnet_jail helper so every feature on the create path (DNS, lo0-up, pf anchor) shows up on fork for free. Startup reconciles the registry against zfs list -t snapshot, dropping any entry whose underlying snap vanished. CLI: coppice snapshot {create, list, show, fork, delete}. Rig: benchmarks/rigs/snapshot-fork.sh — 7/7 assertions green on honor 2026-04-22 (parent evidence written, snapshot present in zfs list, fork preserves evidence + parent-only file, divergence holds after fork, 409-in-use rejection, 204-after-child clean). Fork parity with cold create: ~40 ms backend wall (zfs clone + epair + jail -c), same ~15 s total including ipykernel-spawn poll. v1 cold-starts the cloned rootfs; live-memory resume stays bhyve-path (17 ms p50). See /appendix/durable-snapshots.
Reaper (TTL enforcement)closede2b-compat/src/reaper.rstokio::spawned at startup, wakes every 10 s, finds sandboxes whose end_at has passed, and destroys each via state::kill_sandbox_internal (the same teardown path DELETE /sandboxes/:id uses). Exposes coppice_sandboxes_reaped_total at /metrics. Rig: benchmarks/rigs/reaper-test.sh creates a sandbox, sets timeout=5, waits 12 s, asserts GET returns 404 and the counter advances — passes on honor.
Per-sandbox live network update (PUT /sandboxes/:id/network)closedHandler in e2b-compat/src/routes.rs translates {allow_out, deny_out} (CIDRs, bare IPs, hostnames — hostnames resolved via tokio::net::lookup_host, lookup failures skipped with a warn) into a pf fragment loaded on anchor coppice/sandbox-<short-id> (first 12 hex chars of the sandbox uuid) via pfctl -a <anchor> -f - on stdin. Anchor creation is lazy — the first PUT primes an empty shape then applies the policy. States for newly-denied IPs are flushed via pfctl -k <ip> so in-flight connections don’t survive the change. 10-entry policy lands in 0.66 ms wall (pfctl subprocess, best-of-5); see the ebpf-to-pf honor table. Rig: benchmarks/rigs/net/per-sandbox-policy.sh creates a sandbox, PUTs deny_out=[“1.1.1.1/32”], asserts jexec <jail> fetch http://1.1.1.1/ fails, then PUTs empty and asserts it succeeds. 3/3 green on honor 2026-04-22.

Filesystem, processes, and the envd data plane

Cube featureCoppice statereceipt / note
Code execution — sandbox.run_code()closedNDJSON envelopes over :49999/execute, ipykernel in-jail, state persists. 7/7 SDK checks. See run-code-protocol.
Persistent-kernel state (imports, variables, open files)closedIn-jail Python bridge translates iopub → NDJSON. x = 42 / print(x). receipt.
Rich MIME output (PNG, HTML, SVG)closedpandas DataFrame → text/html, matplotlib → image/png base64. Verified in examples/03-rich-output.py.
Shell commands — sandbox.commands.run()closede2b-compat/src/commands.rs — Connect-RPC server-streaming at /process.Process/{Start,Connect,List,SendInput,SendSignal,CloseStdin,Update}, JSON codec, 5-byte envelope framing with the 0x02 end-stream flag. Spawns jexec -l -U root e2b-<id> <cmd>, line-reads stdout/stderr into ProcessEvent data envelopes, tees each line into the per-sandbox LogBuffer (source=“exec”). Also ships an NDJSON alias at POST /commands for curl rigs. Metrics: coppice_commands_started_total, coppice_commands_finished_total{status=ok|error|killed}, coppice_commands_active. Receipt: examples/11-commands-stream.py runs seq 1 5, asserts stream + exit 0, then sleep 30 + kill() within 2 s. Writeup: /appendix/commands-streaming.
Filesystem — files.read / files.writeclosedHost-side, no jexec per op. e2b-compat rewrites the caller’s jail-absolute path onto {jails_root}/e2b-<id><path> and uses ordinary tokio::fs. Path safety: reject .., require leading /, canonicalize and verify the rootfs prefix. Receipt: examples/08-filesystem.py (write+read round-trip, 22 bytes, traversal rejected). Design notes: /appendix/filesystem-api.
Filesystem — files.list, make_dir, rename, remove, existsclosedConnect-RPC endpoints at /filesystem.Filesystem/{Stat,ListDir,MakeDir,Move,Remove}, JSON codec. Same host-side ZFS-clone access pattern, same path-safety gate. Receipt: examples/08-filesystem.py — list 8 entries, make_dir nested, rename, remove dir and file, all verified via the Python SDK’s sandbox.files.* surface.
Filesystem watch — files.watchclosedClosed with the real Connect server-streaming /filesystem.Filesystem/WatchDir route the SDK calls. The backend implementation is host-side snapshot-diff polling over the jail rootfs rather than inotify or EVFILT_VNODE, but the SDK-visible contract is now there: initial start frame, then filesystem events mapping to create/write/remove/rename/chmod. Receipt: benchmarks/rigs/files-watch.sh starts a Node SDK watchDir(), mutates /jails/e2b-<id> directly from honor, and verifies create/write/rename/remove plus recursive nested-create events. Latest transcript: benchmarks/results/files-watch/latest.txt.
<port>-<id>.<domain> host-header routingclosedFor the SDK-fanout case (envd, files, execute) the gateway does the routing natively: sandbox_from_host() in e2b-compat/src/envd.rs parses the Host header and jexecs into the target jail — no L7 proxy needed. See wildcard DNS and examples/02-persistent-kernel.py (running without E2B_DEBUG=true). The older pf rdr + dnsmasq + Go coppiceproxy path (LAN-peer curl to 80-sbxa.coppice.lan:30080 returns 200; see parity-gaps § External → sandbox) is retained as the fallback for in-jail listener routing (user web apps, Chromium CDP on :9222) once per-jail IPs land via the VNET refactor (#69).

Compute, templates, and storage

Cube featureCoppice statereceipt / note
Template build — cubemastercli tpl create-from-imageclosed#oci-import (2026-04-22). POST /templates accepts {name, from: “oci:<ref>”} and shells out to tools/coppice-import-oci.shbuildah pullbuildah from/mountrsync -a —numeric-ids —exclude=dev/* into a fresh ZFS dataset → zfs snapshot @base. Handler hot-reloads TemplateRegistry on success so the new template is addressable on the next GET /templates without a gateway restart. Script is atomic (cleanup trap destroys half-built dataset + buildah container on any failure) and idempotent (409 unless —force). CLI: coppice tpl import-oci <name> <oci-ref>; tpl import-dockerfile stubbed (exit 3) — planned path is buildah build into a local ref then the same OCI import pipeline. Receipts: 5 integration tests in tests/cli.rs (201 success with JSON body, bare-ref normalisation, 409 conflict, 400 invalid name, stubbed Dockerfile exit-3), examples/14-oci-template.sh imports quay.io/dougrabson/freebsd14-minimal and asserts freebsd-version -r inside the resulting sandbox. Linux-only OCI images import cleanly but fail at exec time without linuxulator — documented. Full writeup: /appendix/oci-templates.
Template registry REST — GET /templates + GET /templates/:nameclosed#68 (2026-04-22, commit ef8e486). e2b-compat/src/templates.rs scans <templates_root> for <name>-template directories at gateway startup (honor: /zroot/jails/<name>-template) and exposes the discovered set at GET /templates (array) and GET /templates/:name (object). An optional DESCRIPTION file inside each template dir surfaces as the description field; absent → omitted. Registry is immutable after construction — restart to pick up a newly-baked template. CLI: coppice tpl list [—json], coppice tpl show <name> [—json]. Rig: benchmarks/rigs/templates-pool-smoke.sh.
templateID routing — POST /sandboxes honors requested templateclosed#70 (2026-04-22). e2b-compat/src/backend/freebsd_jail.rs::create_with_limits now consults TemplateRegistry (the #68 registry) to resolve templateID → clone source. Non-legacy values resolve to zroot/jails/<name>-template@base, checked via zfs list -H -t snapshot before clone; a missing entry or snapshot returns BackendError::NotFound → HTTP 404. The legacy templateID=python + empty-string shorthands still fall back to the —template-snapshot CLI flag so pre-#70 examples are untouched. exec.start also now unconditionally runs ifconfig lo0 up — fresh VNET jails start with lo0 DOWN, which broke chromium’s DevTools socket and any other in-jail localhost listener. Host-side sysctl kern.ipc.shm_allow_removed=1 is persisted by tools/coppice-net-setup.sh. Receipt: 3 unit tests in backend/mod.rs exercise registry-hit / registry-miss / legacy-shorthand on a mock backend; 1 integration test in tests/cli.rs drives POST /sandboxes with templateID=browser, nonexistent, and python. End-to-end on honor: curl -X POST … templateID=browser creates a VNET jail with chromium at /usr/local/bin/chrome.
Warm-pool REST — /pool + /pool/:template/warm + /pool/:template/drainclosed#68 (2026-04-22, commit ef8e486). e2b-compat/src/pool.rs wraps tools/coppice-pool-ctl.sh: GET /pool aggregates list —json into per-template counts; POST /pool/:t/warm loops checkout; POST /pool/:t/drain releases every entry with a matching pool_entry. Added —json flag to the shell script in the same commit. Dev hosts without the script get 503, not a crash. CLI: coppice pool status|warm|drain [—json]. Rig: benchmarks/rigs/templates-pool-smoke.sh.
Templates with pre-installed packagesclosedPython 3.11 + ipykernel + numpy + pandas + matplotlib baked into the _template ZFS dataset; sandbox create clones in milliseconds. Receipt: run-code-protocol update 2026-04-22 later.
Writable-layer sizing (—writable-layer-size 1G)closedCreateRequest.diskSizeMB in e2b-compat/src/routes.rs sets zfs set quota=<mb>M on the sandbox’s clone dataset after zfs clone and before jail -c, so the cap binds the very first write. Receipt: benchmarks/rigs/cpu-mem-limit-test.sh creates with diskSizeMB=100, runs dd if=/dev/zero of=/tmp/big bs=1M count=200 inside the jail, observes ENOSPC at ~100 MB. See e2b-compat.
CPU / memory limits per sandboxclosedCreateRequest.cpuCount + memoryMB in e2b-compat/src/routes.rs drop into rctl -a jail:e2b-<id>:pcpu:deny=<pct> and rctl -a jail:e2b-<id>:memoryuse:deny=<mb>M right after jail -c, so the first jexec(8) process binds to the caps. PATCH /sandboxes/:id/limits reapplies those caps live; see live-limits. Receipt: benchmarks/rigs/cpu-mem-limit-test.sh creates with cpuCount=50, memoryMB=128, pins a Python while True: pass, reads rctl -h -u jail:e2b-<id> — observed pcpu≈50. Requires kern.racct.enable=1 in /boot/loader.conf; the rig skips gracefully if racct is off.
GPU passthroughclosedClosed on honor with an NVIDIA RTX 3070 Mobile/Max-Q bound to ppt(4) and passed into a Debian bhyve guest. Template sidecars declare TPL_EXTRA_BHYVE_SLOTS=‘20:0,passthru,ppt0;21:0,passthru,ppt1’, and coppice-bhyve-pool-ctl.sh appends those slots to the bhyve launch line for pooled SSH guests and host-console guests. The repo carries the FreeBSD-side NVIDIA workaround as patches/freebsd-src/bhyve-nvidia-linux-passthrough.diff plus tools/honor/install-bhyve-nvidia-passthrough.sh. Receipt: benchmarks/results/gpu-passthrough/gpu-passthrough-2026-04-26T145803Z.txt warmed debian-12-bhyve-gpu-gpu-smoke in 9 s, checked out to SSH in 192 ms, and ran the guest probe to GPU_OK. GPU memory snapshots remain a separate Modal-shaped gap.
Persistent volumes (volumeMounts in create)closedPOST /volumes creates a ZFS dataset under zroot/jails/volumes/<name> (optional quota). POST /sandboxes {volumes:[{name,path,readonly}]} null-mounts each volume into the jail root before jail -c, so the jail sees them on first syscall. Teardown unmounts in reverse order before zfs destroy of the sandbox clone. Multi-mount is permitted (nullfs composes); the registry tracks live attachments and refuses DELETE /volumes/:name with a 409 while any are active. Registry in /var/lib/coppice/volumes.json, atomic rewrite. See /appendix/volumes.
Custom kernel / custom boot imageclosedClosed with the bhyve sidecar + Linux template path. FreeBSD custom kernels were already the vmm thesis; what was missing was an honest Linux receipt. The repo now carries benchmarks/rigs/debian12-cloud-bhyve-template.sh plus TPL_PATCH_MODE=nocloud-seed in coppice-bhyve-pool-ctl.sh, which turns Debian’s official generic cloud image into a first-class bhyve template (debian-12-bhyve) with static IP, hostname, and SSH key seeding. Receipt rig: benchmarks/rigs/linux-bhyve-smoke.sh. See /appendix/linux-bhyve-template.

Networking

Most of this is already tracked in parity-gaps and ebpf-to-pf. Listed here for completeness of the feature-row count.

Cube featureCoppice statereceipt / note
Sandbox-to-sandbox isolation (CubeVS eBPF)closedVNET jails + pf anchor; 7 µs p50 TCP_RR. ebpf-to-pf.
Per-sandbox VNET isolation (distinct IP + stack per sandbox)closed#69 (2026-04-22, commits f33cd4cc072a22). Each sandbox gets its own epair pair bridged to coppicenet0 (10.78.0.0/24), its own VNET, and an IP reservation in 10.78.0.10–.250 returned as sandboxIP on GET /sandboxes/:id. pf anchors are source-IP-scoped (from <sandbox_ip>) rather than the old unscoped from any, so one sandbox’s anchor can’t accidentally match another’s traffic on the shared bridge. Air-gapped fragment also carries a pass from <sandbox_ip> to 10.78.0.1 rule so control-plane (DNS forwarder, metadata, log sink bound on the bridge’s host-side IP) remains reachable when outbound internet is blocked. Rig: benchmarks/rigs/vnet-smoke.sh (11 assertions, 3 sandboxes — distinct IPs, in-jail ifconfig matches, host↔jail + jail↔jail L2 + external NAT, teardown leaves no phantom epairs). See /appendix/vnet-jail.
Sandbox → external NATclosedpf NAT on vm-public; 18.7 ms ICMP to 1.1.1.1. parity-gaps.
External → sandbox port routingclosedrdr + cubeproxy + wildcard DNS. parity-gaps.
Per-sandbox firewall rules (allow_out / deny_out)closedpf tables + anchors, source-IP-scoped to the owning jail after #69 step 4 (commit 0a6d6fe): every rule is from <sandbox_ip> rather than from any, so one sandbox’s anchor can’t match host or sibling-jail traffic on the shared bridge. 250k mutations/sec via pfctl -T replace. REST handler (PUT /sandboxes/:id/network) implemented; legacy live-policy-update row in parity-gaps is the one still tracking additional wiring.
Air-gapped sandboxes (allow_internet_access=False)closedB3 (2026-04-22): allowInternetAccess=false on POST /sandboxes loads a default-deny fragment into coppice/sandbox-<short> at create time, with loopback and an optional DNS allowlist (COPPICE_DNS_ALLOWLIST) punched through. PUT /sandboxes/:id/network accepts an air_gapped field for live toggle. Rig: benchmarks/rigs/air-gapped-smoke.sh. See air-gapped.
Per-sandbox rate limitsclosedipfw + dummynet. 10-1000 Mbit/s caps each >95% of configured rate. parity-gaps § dummynet.
IPv6 parityclosedDual-stack fd77::/64 ULA + NAT66. v6 TCP_RR 8 µs tied with v4.
Cluster-level multi-node overlayopenCube’s docs advertise multi-node clusters via CubeMaster. Coppice runs single-node today. The FreeBSD answer is vxlan(4) or wireguard between hosts; not measured. Test rig: two-host lab, sandbox on A reaches sandbox on B via VXLAN on a shared overlay bridge. Explicitly outside the single-host-parity mandate but worth a row.

Observability & security

Cube featureCoppice statereceipt / note
Per-sandbox CPU / memory metricsclosedBackground sampler in e2b-compat/src/metrics_sampler.rs walks state.sandboxes every COPPICE_METRICS_SAMPLE_SEC (default 10 s), shells out to rctl -h -u jail:e2b-<id> for pcpu + memoryuse and zfs get -Hpo value used,quota for disk, and stores the result on AppState.samples. Prometheus /metrics emits labeled gauges: coppice_sandbox_cpu_percent, coppice_sandbox_memory_bytes, coppice_sandbox_memory_limit_bytes, coppice_sandbox_disk_used_bytes, coppice_sandbox_disk_limit_bytes, coppice_sandbox_uptime_seconds (all with sandbox=<short>, template=<name> labels), plus static coppice_racct_enabled. GET /sandboxes/:id now embeds a usage object with the same fields. Requires kern.racct.enable=1; on a host without racct the cpu/mem fields zero out but disk still works. Rig: benchmarks/rigs/per-sandbox-metrics-smoke.sh. Details: /appendix/per-sandbox-metrics.
Per-sandbox structured logsclosedEach Sandbox owns an Arc<LogBuffer> — bounded VecDeque<LogLine>, 1024 cap, drop-oldest, latching truncated flag (see e2b-compat/src/state.rs). kernel::spawn_kernel now pipes the ipykernel’s stdout and stderr (previously Stdio::null()) and spawns two tokio line readers that push each line as source=“kernel”, stream=“stdout”/“stderr”. GET /sandboxes/:id/logs and GET /v2/sandboxes/:id/logs return {“logs”:[…], “truncated”: bool} with ?limit=N and ?since=<rfc3339> query filters. When the buffer is empty and no since is set, the handler falls back to jexec e2b-<id> tail -n <limit|200> /var/log/messages and tags each line source=“syslog”. Per-sandbox Prometheus counter coppice_sandbox_log_lines_total{sandbox,template}. CLI: coppice sandbox logs <id> [—limit N] [—since 5m] [—follow] [—json] — human mode formats [ts] source/stream: text; —follow polls every 2 s with ?since=<last_ts>. Rig: five unit tests on LogBuffer (push/snapshot order, drop-oldest cap, limit, since, composed filters) plus three integration tests driving the compiled CLI against a wire-compatible mock gateway (human format, —json passthrough, —limit round-trip). See /appendix/per-sandbox-logs.
Host-side diagnose / triageclosedtools/diagnose.sh bundles pf state + anchors + tables + pflog + netstat, filterable by sandbox or IP. Cube has no obvious single-command equivalent. parity-gaps.
Trace export (OpenTelemetry)closedtracing-opentelemetry 0.28 bolted onto the existing tracing subscriber, gated on OTEL_EXPORTER_OTLP_ENDPOINT. A dozen #[tracing::instrument] sites cover sandbox.create, sandbox.kill, sandbox.pause/resume, sandbox.execute, backend.create, backend.kill_internal, kernel.spawn, reaper.sweep, and the six files.* handlers. Rig benchmarks/rigs/otel-smoke.sh runs an OTel Collector via docker/podman, drives a create/kill, and asserts a sandbox.create span reaches the collector; falls back to gateway-stderr grep on hosts without a container runtime. /appendix/tracing.
Hardware isolation (dedicated guest kernel)closedbhyve + vmm(4) for the microVM path; vmm-vnode patch for density. 1000 × 256 MiB in 9.1 GiB host. vmm-vnode-patch.
Bhyve microVM backend for code-execution sandboxesclosedGateway-side Rust BhyveBackend at e2b-compat/src/backend/bhyve.rs. TemplateRegistry discovers <bhyve-templates-root>/<name>.img as BackendKind::Bhyve; AppState::backend_for(template) dispatches create / kill / exec / pause / resume per template. create_with_limits shells out to coppice-bhyve-pool-ctl.sh checkout <tpl>, parses the one-line JSON, and returns the allocated 10.77.0.X IP; kill runs return <entry-id>. pause/resume now run owner-preserving pool-ctl pause/resume <entry-id> for SSH guests and console-pause/console-resume for host-console guests; paused pool entries count as in-use capacity. exec is a thin SSH shim against the pool’s baked-in ed25519 key + PerSourcePenalties no guest config. Unsupported in v1 (Other(…) → 501-style): snapshot / fork / delete_snapshot (Session C). Flags: —bhyve-templates-root (env COPPICE_BHYVE_TEMPLATES_ROOT), —bhyve-ssh-key, —bhyve-pool-size N (blocks startup on warm <first-bhyve-tpl> —count N, ~15 s for N=2). Metrics: coppice_bhyve_checkouts_total + coppice_bhyve_checkouts_errors_total + coppice_bhyve_checkout_ns_sum; per-template coppice_bhyve_pool_available + coppice_bhyve_pool_in_use gauges refreshed every 10 s from pool-ctl list —json. SIGINT handler best-effort-drains every bhyve template before exit. End-to-end wall: ~150 ms REST create → SSH-ready on honor (147 ms pool-ctl + ~5 ms shell-out overhead). Unit tests cover parser, map, pause/resume stub calls, and unsupported snapshot surface; tests/cli.rs integration keeps the jail path green. See /appendix/bhyve-backend.
Syscall-level confinement (seccomp / Capsicum)partialJails are the primary boundary; Capsicum exists and is used piecewise in FreeBSD userland but isn’t enforced on the sandbox’s user processes by default. envd-side Capsicum wrap would be a ~1-day pass. Test rig: benchmarks/rigs/capsicum-envd.sh — start envd under cap_enter, assert open-outside-sandbox fails with ECAPMODE.
Rootless operationopenJails and bhyve both need root on the host today. FreeBSD has security.bsd.unprivileged_chroot and rootless-bhyve patches (unmerged circa 2024) but nothing shippable. Cube requires root too; this is a genuine tie and arguably not a gap — noted for symmetry.
Image signing / template provenanceclosed#74-sign (2026-04-22). Every template snapshot has a companion <name>@<snap>.sig at /var/db/coppice/sigs/ (keyed on both template name and snapshot name so a fresh dated @base-dns-YYYYMMDD never races an older @base sig), signed with signify(1) over the snapshot’s ZFS guid (the stable 64-bit per-snapshot id) rather than the send stream — verify is two shell-outs (zfs get guid + signify -V), under 10 ms, no data movement. FreeBSDJailBackend::create_with_limits runs the verify before zfs clone: missing sig is a soft warn by default (or 403 with COPPICE_REQUIRE_SIGNED_TEMPLATES=1), invalid sig is always 403; a legacy-named <name>.sig is still honored as a fallback and surfaces as status=“ok_legacy”. Operator CLI coppice tpl sign|verify <name>[@snap] wraps tools/coppice-sign-template.sh and its verify sibling. Prometheus counter coppice_template_verifications_total{template,status} so tampering surfaces as an alert. Tests: signify_roundtrip_happy_and_tamper, verify_prefers_new_scheme_over_legacy, and verify_falls_back_to_legacy_with_warning in backend/signify.rs. See /appendix/image-signing.

Developer experience

Cube featureCoppice statereceipt / note
Python SDK (e2b + e2b-code-interpreter)closed7/7 code-interpreter checks, 10/10 lifecycle calls. Examples 0107 under examples/.
Node / JS SDK (@e2b/code-interpreter)closedVerified against the gateway on honor with the repo pin (e2b@2.19.0 + @e2b/code-interpreter@2.4.0). The runnable receipt is benchmarks/rigs/sdk-node-roundtrip.sh, which opens the loopback tunnel if needed, runs examples/c1-nodejs/01-lifecycle.mjs (REST create/list/kill + SDK Sandbox.list() paginator + runCode(“print(1+1)”)) and 02-code-interpreter.mjs (state persistence, numpy rich result, restartCodeContext clearing state into a NameError), and captures the latest transcript at benchmarks/results/sdk-roundtrip/latest-node.txt. Debug-mode caveat is unchanged: E2B_DEBUG=true short-circuits SDK create to a stub, so lifecycle is driven by raw fetch while envd traffic rides the gateway’s MRU fallback.
Go SDKclosedValidated in-repo with github.com/xerpa-ai/e2b-go v0.1.0 for paginator coverage plus raw HTTP for create/kill//execute//contexts/:id/restart on the same gateway surface the Python SDK uses. The runnable receipt is benchmarks/rigs/sdk-go-roundtrip.sh, which runs examples/c2-go/lifecycle (create + SDK paginator + print(1+1) + kill round-trip) and examples/c2-go/codeinterpreter (x = 42 survives across run_code, numpy rich result, kernel restart clears state into a NameError) and captures the latest transcript at benchmarks/results/sdk-roundtrip/latest-go.txt. The raw-HTTP pieces stay deliberate here because the exercised local debug path is loopback-based, not wildcard-DNS based.
cubemastercli (CLI)closedNew coppice binary at e2b-compat/src/bin/coppice.rs — sandbox create/list/kill/exec/logs all wired against the gateway; tpl and pool subcommand surface is shaped but returns exit 3 with a “not implemented” note where the server-side endpoint doesn’t exist yet (pool ops still live in coppice-pool-ctl). Gateway URL resolved from —url / $COPPICE_URL / $E2B_API_URL / http://localhost:3000. Receipt: examples/10-cli-roundtrip.sh (create → exec → kill) plus 5 integration tests in e2b-compat/tests/cli.rs driving the compiled binary against a wire-compatible mock gateway. Install: mise run coppice:install. See /appendix/coppice-cli.
MCP bridge (Coppice-only — Cube has none)closedAdditive capability beyond E2B parity. coppice-mcp (e2b-compat/src/bin/coppice-mcp.rs) is a stdio MCP server over rmcp 1.5 that exposes nine tools (coppice_sandbox_create, _exec, _read_file, _write_file, _run_code, _snapshot, _fork, _kill, _list) and four resources (coppice://sandboxes/, /{id}, coppice://templates/, /{name}) — each tool call is a thin translation to the gateway’s REST + envd surface. Principal isolation via a coppice_mcp_principal metadata tag the bridge adds at create time and verifies on every subsequent call; list filters on the tag server-side via /v2/sandboxes?metadata=. Receipts: 8 unit tests for the principal / token-hash / port-swap logic and one full JSON-RPC-over-stdio integration test (e2b-compat/tests/mcp.rs) spawning the compiled binary against a mock gateway — all nine tools enumerated, create → list → scoped-list → cross-principal kill refused. Manual: examples/15-mcp-demo.sh walks create → run_code → write_file → read_file → kill through the binary’s stdin/stdout. Wire up Claude Code with claude mcp add coppice — ssh honor /usr/local/bin/coppice-mcp after mise run coppice:install-mcp-honor. Transport: stdio only in v1; HTTP+SSE is flagged (—transport http) but returns “not yet implemented” — the single-user-per-process model sidesteps the auth question entirely until we have a reason not to. See /appendix/mcp.
Browser-sandbox demo (Playwright + CDP)closedRetry after #69 unblocked VNET. Chromium 147 runs inside a VNET jail cloned from zroot/jails/browser-template, bound on 10.78.0.251:9222 (socat proxy, because chromium 147’s —headless=new pins the DevTools listener to [::1] regardless of —remote-debugging-address). Host-side CDP client is pychrome (pure-Python — Playwright has no FreeBSD wheel, which sank the first attempt). Receipt: examples/09-browser-sandbox.py + tools/coppice-browser-demo.sh + reference screenshot at examples/fixtures/browser-example-screenshot.png (780×493 PNG, 15 KB, title “Example Domain”). Transcript, quirks, and security-posture caveats (—no-sandbox; jail is the real isolation) live in /appendix/browser-sandbox. Wiring this into the gateway-managed sandbox surface (so POST /sandboxes templateID=browser reaches a chromium jail) landed in #70 — see the templateID-routing row above.
VNC + RDP inner views + desktop templateclosed#101 (2026-04-23). New zroot/jails/desktop-template@base carries tigervnc-server, xrdp, xorgxrdp, openbox, firefox, xterm, xclock, dbus — built by benchmarks/rigs/desktop-template-build.sh (idempotent; clones _template@base-dns-*, pkg install, drops demo-only /etc/pam.d/xrdp-sesman permitting root with empty password, snapshots @base, now also smokes the xrdp bind). The gateway’s desktop proxies in e2b-compat/src/sandbox_proxy.rs split cleanly: /vnc-proxy/:id/ is a raw WS↔TCP bridge into jail:5900 (noVNC ↔ Xvnc), while /rdp-proxy/:id/ terminates IronRDP-web’s RDCleanPath handshake, opens TLS to jail:3389, and then proxies the post-handshake RDP stream into xrdp. React inner views in e2b-compat/ui-src/src/views/{VncView,RdpView}.tsx launch the in-jail processes on demand via POST /sandboxes/:id/exec with daemon -f, now with manual clipboard controls, dynamic resize, and openbox-session + Firefox startup so the first paint is a usable desktop. The RDP client ships from vendored, same-origin assets under e2b-compat/ui-src/public/vendor/ironrdp/ because the older npm releases and the GitHub-on-esm.sh fallback were not reliable enough on the tunneled /ui/ path. Outer + menu gains a “desktop” entry that pre-stages [shell, files, vnc, rdp] inner tabs. Mise task: mise run desktop-template:build-honor. Full story: /appendix/desktop-template.
nginx BYOI demo (cubesandbox-base-nginx)closedClosed by benchmarks/rigs/nginx-byoi-smoke.sh. The rig builds zroot/jails/cubesandbox-base-nginx-template@base from the DNS-aware FreeBSD jail base, installs www/nginx, launches a sandbox with templateID=cubesandbox-base-nginx, starts nginx inside the jail, curls http://<sandboxIP>/, then builds tools/coppiceproxy and curls the CubeProxy-shaped route with Host: 80-<short>.coppice.lan. Latest receipt: benchmarks/results/nginx-byoi/latest.txt. See /appendix/nginx-byoi.
SWE-bench / mini-rl-training democlosedClosed by benchmarks/rigs/mini-rl-training-smoke.sh. The rig creates a sandbox workspace, writes a deliberately broken policy.py plus trainer/test harness, runs the trainer through envd /commands and observes the expected reward failure, patches the policy through /sandboxes/:id/files, reruns the same command stream, then verifies checkpoint.txt records best_arm=1 and score=1.000. Latest receipt: benchmarks/results/agent-demos/latest-mini-rl.txt. See /appendix/agent-demos.
OpenAI-Agents SDK integration democlosedClosed by benchmarks/rigs/openai-agents-coppice-smoke.sh, the competitor-shaped benchmarks/rigs/openai-agents-e2b-client-smoke.sh, and benchmarks/rigs/openai-agents-code-interpreter-smoke.sh. The first receipt constructs a real Agents SDK Agent with function_tool wrappers backed by Coppice file writes and envd command streaming. The second ports Cube’s simple demo shape: SandboxAgent, SandboxRunConfig, E2BSandboxClient, E2BSandboxClientOptions, and the SDK Shell capability. The third ports Cube’s code-interpreter shape: manifest-seeded sales.csv, custom Capability tools for shell and Python execution, generated files under output/, and optional real-OpenAI mode via OPENAI_AGENTS_CODE_MODE=agent. Latest receipts: benchmarks/results/agent-demos/latest-openai-agents.txt, benchmarks/results/agent-demos/latest-openai-agents-e2b-client.txt, and benchmarks/results/agent-demos/latest-openai-agents-code-interpreter.txt. See /appendix/agent-demos.
VS Code browser IDE (code-server)closedClosed by templateID=vscode, the React portal’s + → vscode tab, and the same-origin /vscode-proxy/:id/ HTTP/WebSocket proxy into code-server on port 3333 inside the VNET jail. Receipt: examples/12-vscode.py launches code-server, waits for the proxy, writes /root/workspace/README.md, and prints the iframe URL. See /appendix/vscode-remote. Local desktop IDE attach over Remote-SSH is tracked on competitor-gaps, not as a Cube core row: the current Cube docs describe wildcard-DNS/dev-sidecar SDK routing, not a VS Code-specific remote IDE contract.
Hot-reload template authoringclosedPOST /templates/reload (+ coppice tpl reload) re-runs startup discovery against —templates-root and swaps the entry list atomically; a newly zfs clone’d <name>-template dataset shows up in the next GET /templates without a gateway restart. Receipt: beac1b2, 2026-04-22.

Summary rows by bucket

The rolling tally, counting this page’s rows:

bucketcountinterpretation
closed (measured, receipts attached)56Everything an agent developer hitting our gateway with the E2B Python/Node/Go SDK or the coppice CLI touches — code-interpretation, per-sandbox VNET isolation (distinct IP + pf anchor per sandbox + air-gap), pause/resume, port routing, filesystem ops including files.watchDir(), TTL reaper, resource caps, OTel traces, per-sandbox metrics + logs, template registry REST, templateID routing via registry (#70), warm-pool REST, browser-sandbox via CDP after #60, durable snapshot + fork after #durable, hot-reload template authoring after #72-tpl, persistent volumes via ZFS + nullfs, shell commands streaming via Connect-RPC (/process.Process/*), template image-signing via signify-on-guid (#74-sign), OCI template import via buildah pull + rsync → ZFS snapshot (#oci-import), bhyve microVM backend via coppice-bhyve-pool-ctl.sh (/appendix/bhyve-backend), and NVIDIA GPU passthrough into a Linux bhyve guest — works, measured, has a rig.
partial (shape present, edges missing)1The remaining partial row is substrate hardening rather than SDK surface: Capsicum confinement is tracked as an envd hardening pass.
open (rig proposed, not yet run)2Remaining open work-units: cluster overlay and rootless bhyve. Test rig named for each. (OCI template import closed via #oci-import; durable snapshot closed via #durable; hot-reload closed via #72-tpl; per-sandbox logs closed by the ring-buffer + jexec tail fallback wiring; persistent volumes closed via ZFS-dataset + nullfs mount registry; image signing closed via signify-on-guid gate at clone time; filesystem watch closed by benchmarks/rigs/files-watch.sh; agent demos closed by benchmarks/rigs/agent-demos.sh; VS Code browser IDE closed by examples/12-vscode.py and /vscode-proxy/:id/.)
N/A0No rows currently fall into pure “wrong substrate” territory.

Verdict

Cube advertises roughly 59 features across README, docs, and examples (counting the ones we’ve expanded into dedicated rows as they’ve been closed). 56 are measured closed on Coppice with receipts — including per-sandbox VNET isolation after #69 (see /appendix/vnet-jail), which now carries a distinct routable IP and a source-IP-scoped pf anchor per sandbox, the browser-sandbox demo (#60, see /appendix/browser-sandbox) which #69 unblocked, per-sandbox logs which the previous release closed via an in-memory ring buffer fed by the ipykernel’s piped stdout + stderr (see /appendix/per-sandbox-logs), and template image-signing via signify(1) over the ZFS guid, verified before every zfs clone (see /appendix/image-signing). One more is partial: Capsicum wrapping for envd as a hardening pass. Two are genuinely open: cluster overlay and rootless bhyve. Local VS Code Remote-SSH is still a broader-market developer-experience gap, but the Cube-shaped browser IDE/code-server path is now measured and closed.

The shape of this distribution is what a feature audit of a port that’s done the hard substrate work but not the exhaustive handler-plumbing should look like. The expensive rows (cold start, density, isolation, NAT, port routing, rate limiting, IPv6, L7 policy, persistent kernel) are closed. The cheap rows (accept the SDK field and wire it to the already-working mechanism) are where the remaining work lives. That’s an inversion of the usual software failure mode, where the surface is polished and the guts are brittle; here the guts are measured and the surface has known plumbing left to lay.

None of the open rows reveal a FreeBSD-shaped impossibility. Every proposed rig runs on the existing honor box, using primitives already in base (kqueue, rctl, signify, nullfs, bhyvectl) or trivially available from ports (Playwright via Chromium, OTel collector). The remaining places where a new piece of FreeBSD engineering would still be useful — Capsicum-wrapping envd, rootless-bhyve — are acknowledged and tracked (OCI ingest closed #oci-import via buildah), and none of them are on the critical path for an agent-sandbox deployment whose threat model is “LLM-generated Python must not eat the host.”

Cross-refs