The E2B client SDK surface has drifted outward in small ways — new convenience endpoints, new blocks on the sandbox-detail response, new routes the paginator expects to exist even on a single-tenant backend. None of them are expensive to add on top of the substrate Coppice already has; each one that’s missing costs either a 404 in the SDK’s log or a user complaint of the form “the SDK said it could download_url(), yours can’t.”
This appendix documents the bundle that closes those gaps.
Adjacent market-parity work lives in
readiness probes: create/fork now accept
readinessProbe for TCP or shell readiness gates, and templates can
carry a default READINESS.json.
What landed
POST /files/batch
JSON-array batch writer. Body is
[{"path": "/tmp/a", "content_b64": "YQo="}, {"path": "/tmp/b", "content": "hi"}].
Each entry gets a {path, ok, status, bytes?, error?} result
in response order. Partial failures never abort the batch — a 100-file
upload with one bad path gets 99 written + 1 bad_path.
The E2B SDK’s own writeFiles() keeps using N parallel
POST /files (one octet-stream per entry) against envd; that path stays
canonical. /files/batch is additive — for custom clients that want
one HTTP round-trip instead of N, or for shell pipelines driving
uploads with curl --data @batch.json.
POST /files/download-url + GET /files/download-token/:token
Pre-signed download URLs, to match SDK v2.18+‘s
sandbox.files.download_url(path). The flow:
- Caller POSTs
{path}to/files/download-url. - Gateway returns
{url, expires_at, sandboxID}whereurlis/files/download-token/<token>?path=<p>. - The token is
<b64(sandbox)>.<exp_unix>.<hex(hmac_sha256(key, sandbox|path|exp))>. The signing key is 32 random bytes generated at gateway startup and held onAppState— it rotates every time the gateway restarts, so outstanding URLs become invalid. For a demo gateway that’s the right tradeoff; a production deploy that needs URL stability across restarts would persist the key. - A subsequent GET on the URL validates (constant-time compare) and streams the file bytes.
Token expiry defaults to one hour. The token encodes the sandbox id at mint time, so a URL issued for sandbox A can’t be reused against sandbox B even if the Host header would otherwise route to B.
SandboxDetail gains network, lifecycle, hostname
The E2B v2.16+ detail response embeds three blocks that were previously flat or absent on ours. The new blocks are additive — every existing top-level field is untouched, so no client breaks:
network—{allow_out, deny_out, air_gapped}.allow_out/deny_outare the resolved IP strings cached from the most recentPUT /sandboxes/:id/network; empty arrays on sandboxes that never had a policy pushed.air_gappedinverts the top-levelallowInternetAccess.lifecycle—{started_at, last_active_at, end_at, state}. Timestamps already on theSandboxrecord, just reshaped into the SDK-canonical sub-object.last_active_atis bumped on the HTTP handler path (create, network update) — best-effort, not millisecond-accurate.hostname—<sandbox-short>.<base-domain>, wheresandbox-shortis the first 12 hex chars of the sandbox id (same prefix used in pf anchor names for consistency with log lines). The SDK’ssandbox.get_host(3000)composes3000-<sandbox-short>.<base-domain>via the wildcard-DNS pattern documented in wildcard DNS.
Base domain is configurable via the new --base-domain CLI flag
(env COPPICE_BASE_DOMAIN, default coppice.lan). The rc.d script
(tools/rc.d/e2b-compat) exposes it as e2b_compat_base_domain in
/etc/rc.conf.
/tags + /teams
GET /tags now returns the sorted set of tags present on live
sandboxes. POST /sandboxes and POST /snapshots/:id/fork accept a
tags: string[] field; GET /sandboxes/:id returns it; and
GET /v2/sandboxes?tag=<name> / ?tags=a,b filters on it. GET /teams → [] and GET /teams/:id/metrics → {} remain stubs.
The E2B SDK pings these routes in its paginator + metrics-page code
paths on the assumption that a multi-tenant backend is on the other
end. Coppice is single-tenant — one gateway serves one host’s jails —
so teams are still “empty, but well-formed.” Tags are useful without a
tenant model, so they graduated from a stub to a real in-memory index.
Prior to this bundle, 404s on these routes surfaced in user logs as
E2B backend unreachable, which is both wrong and alarming.
Flagged partial in the audit, not closed: tags are closed, but
team-scoped auth and quotas are still the real multi-tenancy row.
Security posture for the pre-signed URL
The download-token is a small, short-lived HMAC-signed credential that grants read access to one file for up to an hour. Constraints worth being explicit about:
- Token binds sandbox + path + expiry. Changing any of the three
invalidates the signature. A caller can’t tamper with
?path=in the URL to escape into a different file. - Key rotates on restart. Outstanding URLs become invalid. This is a deliberate tradeoff — the gateway never persists signing material to disk.
- Constant-time compare on validation.
ct_eqinfiles.rsavoids the obvious timing oracle on the HMAC check. - Path traversal still gated. The token-handler calls the same
rewrite_path()the othersandbox.files.*handlers use — validates leading/, rejects.., canonicalizes the ancestor, verifies the rootfs prefix.
A production deploy that wants tokens to survive restart would swap
the random-key generation in main.rs for a read of
/var/db/coppice/download-sign.key (mode 0600), falling back to
generation on first start. The knob is a three-line change; we haven’t
made it because every Coppice deployment today is a single gateway
where “restart invalidates tokens” matches operator expectations.
Receipts
tests/cli.rs: 5 integration tests (files_batch_success_and_empty,files_batch_partial_failure_is_per_entry,files_download_url_shape,tags_teams_stubs_are_empty,sandbox_detail_has_network_lifecycle_hostname).src/routes.rs:tags_are_normalized_and_filterablecovers tag normalization plus single- and multi-tag filters.src/files.rsdownload_token_tests: 5 unit tests (roundtrip, tampered-fails, path-differentiation, sandbox-differentiation, malformed-rejection).- Honor deploy verification (manual):
curlagainst each new endpoint, new binary restarted viaservice e2b-compat restart.
The SDK pin stays at e2b@2.19.0 + @e2b/code-interpreter@2.4.0
(current-latest on npm as of 2026-04-22) — the 2.20.0 / 2.6.0
tags referenced in some release notes aren’t on the public index
yet. The gateway is forward-ready for either.