feat(sandbox): add an E2B sandbox backend - #458
Conversation
3204aa5 to
39acb25
Compare
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||
39acb25 to
e4d4a07
Compare
e4d4a07 to
50fc09a
Compare
5a6bded to
45e04fb
Compare
45e04fb to
02ec42a
Compare
c99e89d to
ef5366d
Compare
aivong-openhands
left a comment
There was a problem hiding this comment.
Review summary
Taste: 🟢 good. This is a clean, well-scoped new backend that closely mirrors the existing docker/remote sandbox services, keeps ownership authoritative in the DB row (not E2B metadata), rotates a per-sandbox session key, stores it encrypted at rest, and enforces user scoping at every mutating boundary. Tests are behavior-driven and thorough. CI is green on the current head. No material findings — approving.
Verification performed
- ✅ Template & triage: Why / Summary / Issue Number (None) / How to Test / Video (N/A) / Type all present and filled. Not a draft.
- ✅ CI on current head
ef5366d: all checks pass (Python tests, lint, build amd64/arm64, coverage 92.69% on new lines). - ✅ Supply chain (
e2b==2.46.0, new dependency → full scrutiny): published 2026-08-25 (~30 days, well past the 7-day window), not yanked, established SDK,requires_python >=3.10. No install hooks of concern. Clears the- ✅ Supply chain (e2b==2.46.0, new dependency .add/flushon create- ✅ **Supply chain** (e2b==2.46.0, new dependency → full scrutiny): published 2026-08-25 (~30 days, well past the 7-day window), not yanked, established SDK,requires_python >=3.10. No install hooks of concern. Clears the- ✅ **Supply chain** (e2b==2.46.0, new dependency .add/flushon create- ✅ Supply chain (e2b==2.46.0, new dependency → full scrutiny): published 2026-08-25 (~30 days, well past the 7 callsrequire_user_idbeforepause_old_sandboxes(which would reach all users asADMIN).- Create → flush row →
/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake (the - Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake (the - Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake (the - Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake (the - Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake; anyBaseException- Create → flush row →/api/inithandshake (the - Create → flush rercised by CI: the template build, the webhook event callback path (no polling fallback; requires a publicly reachableOH_WEB_URL), and the VSCode link (answers 403 until the SDK pin includes OpenHands/software-agent-sdk#5282). These are documented as known limitations insandbox/README.md, but a live smoke test before enablingRUNTIME=e2bin any environment would be worthwhile.
- Create → flush row →
- Both
OH_SANDBOX_KINDandOH_SANDBOX_SPEC_KINDmust be set together; with only the former the app silently falls back to docker templates. Already called out in the PR body — no change requested.
[RISK ASSESSMENT]
The change introduces a new external-service integration and handles secrets (init key, per-sandbox session keys, encryption at rest), which are sensitive areas. Risk is contained because the backend is purely additive and gated behind RUNTIME=e2b / the E2B injectors, and it faithfully reuses the established docker/remote ownership, scoping, and lifecycle patterns with stroThe change introduces a new external-service integration and handles secrets (init key, per-sandbox session keys, encryption at rest), which are sensitive areas. Risk is contained because the backend is purely additive and gated behind RUNTIME=e2b / the E2B injectors, and it faithfully reuses the established docker/remote ownership, scoping, and lifecycle patterns with stroTheThe change introduces a new external-service integration and handles secrets (init key, per-sandbox session keys, encryption at rest), which are sensitive areas. Risk is contained because the backend i hold and keeps the secuThe change introduces a new externalImprove this review? If feedback seems incorrect or irrelevant, update the repository's .agents/skills/custom-codereview-guide.md (with the /codereview trigger), then re-request review. The reviewer reads the guide from the PR head.
Resolve with AI? Install the iterate skill and run
/iterate.Was this review helpful? React with 👍 or 👎.
This review was generated by an AI agent (OpenHands) on behalf of the requesting user.
Mutation review of the E2B testsI hand-wrote a set of mutants from the diff and ran them against the two touched Controls — these all died (the suite works)
What makes these land: Survivors — these mutants passed the suite
M1 — an empty init key is not treated as "missing" (
|
ef5366d to
2aea137
Compare
|
🚀 Released in 1.66.0. |
HUMAN:
AGENT:
Why
Runtime-api is the only production sandbox backend today. This adds E2B as a second one, so a deployment can run sandboxes as Firecracker microVMs without running its own control plane.
E2B starts a sandbox by resuming a template snapshot. The agent server is already running at that point, so its environment is fixed when the template is built. The app server cannot inject a per-sandbox session key the way docker does.
The agent server's deferred-init mode handles this. The template boots the agent server dormant, holding a static
OH_SECRET_KEY. Every/api/*route returns 503 until the server is claimed. The app server makes a random session key for each sandbox and sends it toPOST /api/init, authenticated with the static key.Summary
E2BSandboxServiceandE2BSandboxSpecService, selected withRUNTIME=e2b. A spec id is an E2B template name, so the sandbox-spec dropdown lists templates with no frontend change.v1_remote_sandbox, with backende2b). The key is stored encrypted, because E2B cannot return it after create. A row whose sandbox E2B no longer has reportsMISSING, so its conversation shows as archived.start_sandboxrefuses a caller without a user id.scripts/e2b/build_template.pybuilds the template from the published agent-server image, socreate()resumes a snapshot of a server that is already listening.Configuration
Build the template once:
It prints
E2B_INIT_API_KEY=...at the end. On a self hosted cluster, a build can fail part way withinternal error occurred. An identical re-run usually succeeds.Then set these on the app server:
Set both
KINDlines. With onlyOH_SANDBOX_KIND, the app falls back to docker templates.RUNTIME=e2b, which the build script prints, selects both, but it ignores theOH_SANDBOX_*settings.Issue Number
None.
How to Test
TestUserScopingcovers cross-user access.test_e2b_metadata_cannot_override_the_rowshows that E2B metadata naming another owner does not change the owner.For a live check against an E2B cluster, build the template and set the configuration above. Run
make local-db, start the app server and start a conversation. Then:SELECT id, created_by_user_id FROM v1_remote_sandbox WHERE backend = 'e2b';shows the row.Video/Screenshots
N/A, backend only.
Type
Notes
e2b==2.46.0is a new dependency.Metadata. Sandboxes carry
oh_managed,oh_user_idandoh_spec_idmetadata. It tags managed sandboxes so that one with no row can be found. The row decides ownership, not the metadata. Search asks E2B only for sandboxes withoh_managedand the caller'soh_user_id, so the list stays small.ADMINlists without the owner filter.Session keys. Each sandbox gets its own random key. The key is kept on pause.
auto_resumecan wake a sandbox on the next request without callingresume_sandbox, so the key must keep working.delete_sandboxremoves the row, and the key with it.Errors are reported, not hidden. An E2B
AuthenticationExceptionis reported as aSandboxErrornamingE2B_API_KEY. It is the one E2B error outside theSandboxExceptionhierarchy, so it would otherwise escape every handler. A failedlistraises instead of returning an empty page, which would tellpause_old_sandboxesthe limit is clear. Deleting a sandbox that E2B already removed succeeds and removes the row. If the row fails to write after create, the sandbox is killed.VSCode URL. The VSCode link is built from the session key, as docker's is. That needs an agent server that switches VSCode to the session key on
POST /api/init, which is OpenHands/software-agent-sdk#5282.build_template.pydefaults to the image for the installed SDK (1.49.4), which doesn't have it yet, so the link answers 403 until the SDK pin includes that fix.Limitations. Both are documented in
openhands/app_server/sandbox/README.md. Both come down to headless flows not being fully supported yet.OH_WEB_URLmust be publicly reachable. The agent server posts events back over a webhook, and this backend has no polling fallback. A sandbox started against a localhost app server runs, but its events never arrive.timeout_seconds, not an E2B limit. The plan sets the ceiling (one hour on Hobby, 24 hours on Pro), or the operator does on a self hosted cluster. On expiry the sandbox pauses. An interactive session wakes it on the browser's next request. A headless run has no such request, so it can stall when the lease expires. The follow-up fix is to renew the lease when the sandbox delivers a webhook.Enterprise server image for this PR: