SDK parity quick-wins

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:

  1. Caller POSTs {path} to /files/download-url.
  2. Gateway returns {url, expires_at, sandboxID} where url is /files/download-token/<token>?path=<p>.
  3. 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 on AppState — 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.
  4. 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:

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:

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

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.