E2B SDK compatibility surface

The “drop-in replacement” framing is approximately true for the common E2B SDK workflow (create → run code → close) and approximately false for anything observability-adjacent. The CubeAPI/README.md support matrix is the canonical source.

Implemented endpoints

From CubeAPI/README.md + cross-checked against CubeAPI/src/routes.rs:

MethodPathRole in common E2B flow
GET/healthreadiness probe
POST/sandboxesSandbox.create(template=…)
GET/sandboxesSandbox.list()
GET/v2/sandboxesv2 list w/ filters
GET/sandboxes/:idSandbox.get(id)
DELETE/sandboxes/:idsandbox.close()
POST/sandboxes/:id/pausesnapshot/pause for later resume
POST/sandboxes/:id/resume(deprecated; prefer connect)
POST/sandboxes/:id/connectattach client, auto-resume from pause

This is the lifecycle. Sandbox.create → run_code → close only touches POST /sandboxes, the WebSocket connect, and DELETE /sandboxes/:id. All three are in the ✅ column.

Not implemented (501 / not registered)

MethodPathWhen you’d miss it
GET/sandboxes/:id/logs (v1)Streaming sandbox-side log collection
GET/v2/sandboxes/:id/logsSame, cursor-paginated
POST/sandboxes/:id/timeoutAbsolute TTL management
POST/sandboxes/:id/refreshesRelative TTL extend
POST/sandboxes/:id/snapshotsDurable snapshot creation (distinct from pause)
GET/sandboxes/snapshotsList team-wide snapshots
GET/sandboxes/:id/metricsCPU/memory gauges per sandbox
PUT/sandboxes/:id/networkLive egress policy update

If you’re using e2b-code-interpreter for straightforward “run this Python and give me the result” workflows, none of these show up. If you’re integrating E2B into an operator dashboard, the metrics + logs gap is load-bearing.

WebSocket surface

E2B uses WebSocket for the Sandbox.run_code streaming path. axum 0.7 with the ws feature handles this; CubeAPI implements it. The byte-level protocol (JSON framing, message types) mirrors E2B’s. Client code written against the E2B SDK does not notice the switch.

Authentication

From the quickstart:

export E2B_API_URL="http://127.0.0.1:3000"
export E2B_API_KEY="dummy"

In the open-source single-node install, E2B_API_KEY is effectively off. Production deployments front CubeAPI with their own auth — CubeProxy is a natural place.

What ports to FreeBSD

Axum and tokio are portable. CubeAPI does not touch Linux-specific primitives in its HTTP layer; the calls it makes downward go to CubeMaster over gRPC. So rather than cross-compile Tencent’s CubeAPI, we built our own Axum service — e2b-compat/ — that speaks the same E2B REST surface but dispatches downward into jails + bhyve rather than containerd + cube-hypervisor. On honor, the E2B Python SDK passes 10/10 of its API-server calls against this service, and the envd-compat endpoint on :49999 streams run_code NDJSON via jexec python3 — see /appendix/run-code-protocol for the wire format and the round-trip transcript.

The complexity in a FreeBSD port lives below the API layer, not at the API layer itself — as expected.

Caveats

”E2B” is a moving target

E2B’s own API has been shifting — endpoints added, versioned, deprecated. CubeAPI pins against a specific E2B version (implicitly, via the routes it chose to implement). If the E2B SDK introduces a new required endpoint, CubeAPI will either return 501 on that endpoint or lag behind until Cube adds it. “Drop-in” is pinned to a moment in time.

Template semantics

CUBE_TEMPLATE_ID in the quickstart is a Cube-specific ID that maps to a stored VM template. E2B’s template field in Sandbox.create is per-team on E2B cloud and maps to E2B-managed templates. Cube accepts E2B-shaped template IDs by treating them as opaque strings and looking them up in CubeMaster’s template registry. If you have existing “template” IDs on E2B cloud and expect them to work unchanged on Cube, they won’t — you upload templates to Cube separately.

Snapshots are not snapshots

E2B has two distinct concepts: /pause (stateful pause, live memory snapshot) and /snapshots (create a durable snapshot image, e.g., to fork from later). Cube implements the former and not the latter. If your code calls sandbox.snapshot() expecting to fork later, it’ll get a 501. If it calls sandbox.pause()/sandbox.resume(), it works.