Cube ships a cubemastercli binary that covers the
operator’s daily surface area: create a sandbox, list what’s
running, kill the one that ate all the RAM, build a template from
a directory, warm a pool. We didn’t have that. We had
curl | jq and coppice-pool-ctl. This page
is about closing that gap with a single Rust binary that ships
from the same Cargo workspace as the gateway and speaks the
gateway’s JSON wire format directly.
The shape
The binary is called coppice. It lives at
e2b-compat/src/bin/coppice.rs in the same crate as the
gateway, so it compiles against whatever the gateway’s Cargo.lock
says — no protocol drift between client and server. Install with
mise run coppice:install, which shells out to
cargo install —path e2b-compat —bin coppice —locked
and drops the binary in ~/.cargo/bin.
The subcommand surface mirrors Cube’s CLI deliberately, so an operator who’s been doing Cube for a year finds what they expect:
| command | gateway route | note |
|---|---|---|
coppice sandbox create [—template] [—ttl] [—cpu] [—mem] [—json] | POST /sandboxes | Prints the id on stdout (or full JSON with —json). —ttl follows up with POST /sandboxes/:id/timeout. |
coppice sandbox list [—json] | GET /sandboxes | Aligned columns by default: id, template, state, started, end. |
coppice sandbox kill <id> | DELETE /sandboxes/:id | Exit 2 on 404, so shell pipelines can branch on it. |
coppice sandbox exec <id> — <argv…> | POST /sandboxes/:id/exec | Relays stdout verbatim; the — separator is mandatory when argv has flags. |
coppice sandbox logs <id> | GET /sandboxes/:id/logs | Snapshot only; —follow is advertised-but-unimplemented and exits 3. |
coppice tpl list / show / create-from-dir | — | The gateway has no /templates endpoint yet. The CLI surface is shaped and returns exit 3 with a “not implemented” stderr note; scripts don’t have to change when the endpoint lands. |
coppice pool status / warm / drain | — | Same story — coppice-pool-ctl on the host handles this today; CLI stubs exit 3 until the endpoints move into the gateway. |
Gateway URL resolution
Four sources, in priority order: —url <u>,
$COPPICE_URL, $E2B_API_URL,
http://localhost:3000. The $E2B_API_URL rung
matters — it’s what the official Python SDK honours, and picking it up
means an operator can point their shell at a remote gateway once and
have both coppice and any SDK scripts agree without
re-exporting. The flag strips trailing slashes so
—url http://localhost:3000/ and
—url http://localhost:3000 are the same thing.
There’s a reserved —token flag that’s a no-op today.
When B3 (air-gapped sandboxes) ships auth, the CLI will read
$COPPICE_TOKEN and emit a bearer header; we wanted the
flag shape in place now so scripts forward-compat cleanly.
A worked session
From a laptop, tunnelled into honor via
ssh -L 3000:localhost:3000 -L 49999:localhost:49999 -L 49983:localhost:49983 honor,
talking to the live gateway rc.d service (2026-04-22, verbatim):
$ coppice sandbox create --template python --json | jq -r .sandboxID
609a21703c314940b66532095ac74ad9
$ coppice sandbox list
SANDBOX_ID TEMPLATE STATE STARTED END
609a21703c314940b66532095ac74ad9 python running 2026-04-22T23:57:06.487003909Z 2026-04-23T00:12:06.487004230Z
$ coppice sandbox exec 609a21703c314940b66532095ac74ad9 -- python3 -c 'print("hello from CLI")'
hello from CLI
$ coppice sandbox kill 609a21703c314940b66532095ac74ad9
$ echo $?
0
Note the interpreter is python3, not python:
the FreeBSD python template pkg-installs
python3.11 as python3 with no
python alias. The E2B Python SDK’s
run_code path is unaffected (it reaches the in-jail
ipykernel via /execute); this only bites on ad-hoc
coppice sandbox exec invocations.
That’s the critical path. Creating with resource limits is one flag
each: —cpu 50 —mem 256 forwards cpuCount=50
(rctl pcpu in percent) and memoryMB=256 to the
same handler path the SDK uses, so the limits bind before
jexec’s first syscall — same story as the
feature-audit row.
Against cubemastercli
Cube’s CLI is bigger than ours: cluster-admin subcommands, auth
flows, log streaming with cursor state. We don’t yet have clusters,
auth, or streaming logs. The CLI surface reflects that honestly —
the endpoints we have are fully wired, the ones we don’t exit 3 with
a named reason. An operator running coppice pool warm python
—count 20 is told explicitly that the route isn’t in the
gateway yet, not given a silent failure or a fake success.
The rows we cover outright, though, cover the day-to-day:
sandbox create / list / kill / exec is 90% of what hits a CLI in
anger. examples/10-cli-roundtrip.sh in this repo runs
that full cycle as a single shell script and is what we point at in
CI and in demo scripts.
Gaps the CLI surfaced
Writing the CLI flushed out two small gateway rough edges that weren’t visible from the SDK’s happy path:
DELETE /sandboxes/:idon a nonexistent id returns HTTP 500, not 404. The backend error wrapper treats “jail not found” as a generic backend error rather than translating it to the shape the SDK already handles for other 404 paths. The CLI handles either (exit 1 on 500, exit 2 on 404) so it’s not a blocker; just worth fixing the next time someone’s inroutes.rs.- No
/templatesendpoint family on the gateway yet.coppice tpl list / show / create-from-dirandcoppice pool warm / drain / statusall map onto surface area that currently only exists ascoppice-pool-ctlhost-side tooling. The CLI emits an explicit “not implemented” stderr note and exits 3 for each, rather than silently succeeding or returning a generic HTTP error — so scripts fail loudly and for the right reason.
Receipts
- Binary:
e2b-compat/src/bin/coppice.rs, compiles against the workspace Cargo.lock;cargo build —release —bin coppiceon honor produces a ~8 MiB statically-linked binary (rustls, not OpenSSL, so no libssl drift). - Tests:
e2b-compat/tests/cli.rsstands up a wire-compatible mock gateway in-process (Axum on a random 127.0.0.1 port), drives the compiled CLI viastd::process::Command, and asserts exit-code and stdout shape for create / list / exec / kill,—ttldouble-hop, env-var URL fallback, and the exit-3 unimpl paths. Five tests, all green locally. - Example:
examples/10-cli-roundtrip.sh— the end-to-end transcript above, minus the shell prompt. - Install:
mise run coppice:install.
One more row closed in the feature audit, one more binary that new operators can run on day one instead of learning the gateway’s JSON by hand.