E2B provider

July 10, 2026 · View on GitHub

Provider status: shipped. Tasks 1–3 are all merged into main; E2B is a fully supported AgentBox cloud backend (--provider e2b) alongside docker / daytona / hetzner / vercel. Public docs live at /docs/e2b; the provider page (apps/web/content/docs/e2b.mdx), CLAUDE.md, docs/cloud-providers.md §3c, docs/cloud-create-flow.md, and the README are kept in sync with each change.

The standout differentiator vs. Daytona / Hetzner / Vercel: E2B is the only AgentBox cloud whose prepare builds the base image directly from a Dockerfile via the SDK's Template.build(). The others all bake a one-time snapshot.

Status tracker for adding E2B (--provider e2b) as a fifth AgentBox backend, alongside docker / daytona / hetzner / vercel.

E2B (https://e2b.dev) runs Firecracker microVMs with a TypeScript/Python SDK (e2b). Shape-wise it is closest to Vercel (microVM per box, SDK comms, no SSH, public preview hostnames, pause/resume persistence) and Daytona (snapshot tiers). The Vercel provider (packages/sandbox-vercel/) is the structural template for this work; read it first.

How E2B maps onto the CloudBackend abstraction

AgentBox conceptE2B primitive
provision a boxSandbox.create(template, { timeout, metadata, envs, ... })
resolve existing boxSandbox.connect(sandboxId) (auto-resumes if paused)
execsandbox.commands.run(cmd, { cwd, envs, user, background })
upload / download / lssandbox.files.write / read / list
preview URLsandbox.getHost(port){port}-{sandboxId}.{domain} (HTTPS)
pause / resumesandbox.betaPause() / Sandbox.resume(id) (persistence)
list (for prune)Sandbox.list() (paginator)
destroysandbox.kill()
session timeoutSandbox.create({ timeout }) (seconds) + setTimeout
base image (prepare)e2b template build from a Dockerfile — E2B CAN build images from a Dockerfile (key difference from Vercel/Hetzner, which can't)
egress policyallow_internet_access / network create opts
credentialsE2B_API_KEY (in .env.local and ~/.agentbox/secrets.env); optionally team id

Key open questions — answered in Task 1 (2026-06-02)

  1. Preview URL authPUBLIC by default. sandbox.getHost(port) returns {port}-{sandboxId}.e2b.app, served over HTTPS with no token. Smoke-verified with a python3 -m http.server 8080 in-box → host fetch returned HTTP 200 without any header. Optional gate: Sandbox.create({ network: { allowPublicTraffic: false } }) requires an e2b-traffic-access-token header (sandbox.trafficAccessToken). Task 1 leaves it public to match Vercel; revisit in Task 2/3 if the security model warrants gating.
  2. Checkpoint primitiveDeferred to Task 2. E2B's Sandbox.pause/ Sandbox.connect (auto-resume) is a single-resume cold-store, not a reusable immutable image. The reusable primitive is Template.build() from a Dockerfile — that lands in Task 2 alongside agentbox prepare --provider e2b. Task 1 ships a checkpoint stub that throws "not yet implemented for e2b (Task 2)".
  3. Privileged ports / port capgetHost(port) accepts any port; no documented cap. Task 1 sets webProxyPort: 8080 to mirror Vercel and keep the in-box AGENTBOX_WEB_PROXY_PORT flag uniform across cloud backends. Re-test :80 in Task 2.
  4. Nested containers — Ships launchDockerd: false. NOTE (corrected 2026-06-23): the "same family as Vercel → no DinD" inference here was WRONG. E2B's microVM actually supports nested containers (full root + cap_sys_admin + working namespaces); docker runs once installed. The false default is a deliberate AgentBox choice (docker not baked in), not a platform constraint. See the changelog entry + the "DinD on E2B" follow-up.
  5. Default resources — E2B base template ships node 20, sudo, git, tar on Debian 12. vCPU/RAM/disk are template-level (Template.build({ cpuCount, memoryMB })), NOT per-create. Task 1's defaultResources: { cpu: 2, memory: 4, disk: 8 } are advisory metadata for BoxRecord stats until Task 2's prepare bakes a sized custom template.

Additional empirical findings (smoke-tested 2026-06-02):

  • Default user is user (uid 1001), not vscode. Task 1's provision() runs a one-shot in-box fixup script that creates a vscode user (auto-assigned uid — 1000 is taken by E2B's code group on base), grants passwordless sudo, and chowns /workspace, /run/agentbox, /var/log/ agentbox so the rest of the cloud scaffold's hardcoded vscode references work. The vanilla base template otherwise has no agentbox-ctl, no /workspace, no vscode user.
  • agentbox-ctl runs from a single ~835 KB packages/ctl/dist/bin.cjs bundle. Task 1 uploads it at create-time via sb.files.write (~1s), installs to /usr/local/bin/agentbox-ctl with sudo cp+chmod. No template bake needed for Task 1.
  • Sandbox.getInfo is the non-resuming static existence check. Sandbox.connect auto-resumes a paused sandbox — state()/get() MUST use getInfo (not connect) so existence checks don't wake (and bill) a paused box. Only ops that need a live handle (exec, files, previewUrl, pause, destroy) call connect.
  • Sandbox.pause is the canonical pause APIbetaPause is deprecated.
  • sb.commands.run throws CommandExitError on non-zero exit. The CloudBackend contract returns {exitCode, stdout, stderr}, so the e2b backend catches the error and converts it back to a result.
  • Carry needs the same vercel root carve-out. Default exec runs as vscode, but vscode (uid 1000) cannot chown to other uids — the carry chain's chown -R errors out partway, the parent-chain loop never reaches its terminator. packages/sandbox-cloud/src/carry.ts now forces user: 'root' for both vercel AND e2b.

Task breakdown (each task = one PR, merged before the next starts)

Task 1 — Package scaffold + CloudBackend core · status: DONE 2026-06-02

Goal: agentbox create --provider e2b produces a ready box end-to-end, reusing the createCloudProvider scaffold.

  • packages/sandbox-e2b/ package (package.json, tsup, tsconfig). Adds e2b@^2.27.1.
  • env-loader.ts + credentials.ts — loads/ensures E2B_API_KEY from ~/.agentbox/secrets.env; agentbox e2b login [--status].
  • sdk.ts — re-exports Sandbox, resolves the API key, gates with actionable error.
  • backend.ts — every CloudBackend method over the E2B SDK (provision, get, list, start/stop/pause/resume/destroy, state, exec, uploadFile/downloadFile/listFiles, previewUrl, signedPreviewUrl). get/state use Sandbox.getInfo (non-resuming) per the orchestrator's review; only exec/files/pause/destroy use Sandbox.connect.
  • index.tscreateCloudProvider(e2bBackend, { defaultResources, launchDockerd: false }) + a checkpoint stub that throws "not yet implemented for e2b (Task 2)". Exports e2bProvider, e2bBackend, ensureE2bCredentials.
  • cli.tsagentbox e2b login [--status].
  • runtime-assets.ts — single-file resolver for packages/ctl/dist/bin.cjs, uploaded at create-time so the cloud scaffold's launchCloudCtlDaemon finds /usr/local/bin/agentbox-ctl. (Task 2 grows this to a full prepared-template bake.)
  • test/env-loader.test.ts — parser + lookup precedence unit tests.
  • Wired into apps/cli: provider/registry.ts, provider/cloud-backend.ts, index.ts, help.ts, commands/{checkpoint,prune,install,prepare,dashboard,fork}.ts, lib/doctor-checks.ts, packages/relay/src/host-actions.ts, packages/sandbox-cloud/src/carry.ts (root carve-out for the chown walk), packages/config/src/types.ts (ProviderKind + box.provider enum), test/help.test.ts.
  • Smoke: agentbox create --provider e2b -y -n e2bsmoke -w /tmp/e2bsmoke-repo reaches box cloud:<id> ready in ~15s. Backend-level smoke (/tmp/e2b-smoke-checks.mjs): - state → running - exec as vscode + as root → exit 0 - file round-trip (uploadFile / downloadFile) → byte-exact - listFiles → [{name,isDir}] - previewUrl → https://8080-<sbx>.e2b.app; HTTP 200 with no token - pause → 'paused'; start (connect auto-resume) → 'running'; in-box file survives the cycle - list → both live sandboxes seen - destroy → SandboxNotFoundError on subsequent getInfo (cleanly gone)

Task 2 — prepare (template build) + attach + checkpoints · status: DONE 2026-06-03

Goal: provider parity with Vercel — prepare bakes a custom template, interactive attach works, checkpoints capture/restore. ALL smoke steps verified end-to-end.

  • prepare.ts + prepared-state.tsagentbox prepare --provider e2b bakes the base template via the SDK's Template.build() (driven from TypeScript, not an on-disk Dockerfile). Reuses the staged docker runtime assets through runtime-assets.ts (mirror of vercel's resolver). Records template id in ~/.agentbox/e2b-prepared.json. Base-snapshot gate (ensureE2bBaseTemplate) lives inside backend.provision.
  • build-attach.ts + attach-helper.ts — SDK-streaming PTY bridge (no SSH). buildE2bAttach returns ['node', <helper>, '--sandbox-id', id, '--user', vscode] with E2B_API_KEY + AGENTBOX_E2B_INNER_CMD in env. The helper Sandbox.connects, opens pty.create({ cols, rows, onData, ... }), sendInputs the renderInnerCommand string (tmux ensure + attach), and bridges stdin / stdout / SIGWINCH.
  • checkpoint capability — Sandbox.createSnapshot(sandboxId, { name }) is the reusable-snapshot primitive (was the missing piece in Task 1; same shape as Vercel — store snapshotId in the manifest, restore boots via Sandbox.create({ template: <id> })). snapshotExists uses Template.exists(name).
  • previewUrl fix — drops Sandbox.connect (which would auto-resume a paused box), constructs the URL locally as {port}-{sandboxId}.{E2B_DOMAIN ?? 'e2b.app'}. Verified: agentbox list shows the URL of a paused box without waking it.
  • Per-provider config keys (box.imageE2b, box.defaultCheckpointE2b, box.sizeE2b) added so the prepare command pins the template id under the right key (avoids cross-provider collision).
  • CLI integration: apps/cli/scripts/stage-runtime.mjs stages runtime/e2b/{scripts/build-template.sh, attach-helper.cjs, ctl.cjs, agentbox-*-shim, custom-system-CLAUDE.md, ...}; apps/cli/src/commands/ prepare.ts renders the e2b status block; apps/cli/src/lib/doctor-checks.ts adds the base-template probe.
  • Unit tests: test/prepared-state.test.ts (round-trip + gate), test/build-attach.test.ts (argv shape + env wiring).
  • Smoke end-to-end (2026-06-03): - agentbox prepare --provider e2b -y → built agentbox-base:latest (template id o05kawibx9vcmxvgjnk4); 2m18s build, pinned to box.imageE2b in project config + recorded in ~/.agentbox/e2b-prepared.json. - agentbox create --provider e2b -y -n e2bt2a -w /tmp/e2bsmoke-repo → ready in ~10s (Task 1 was ~15s with the create-time fixup hop). - agentbox shell e2bt2a (via script -q -c …) → real vscode@e2b:/workspace$ prompt; the host PTY bridges in/out cleanly. (drive harness shows a display quirk where tmux's framing buffer doesn't render into the headless terminal — separate issue tracked below; the underlying PTY bridge works.) - agentbox stop e2bt2a → state paused; agentbox list --global still shows the URL (no resume); agentbox start e2bt2arunning; round-trip survives. - agentbox checkpoint create e2bt2aSandbox.createSnapshot minted marco6/agentbox-e2bt2a-…:default; manifest written under ~/.agentbox/cloud-checkpoints/e2b/…/manifest.json; agentbox checkpoint ls finds it. - agentbox create --provider e2b -y -n e2bt2b -w /tmp/e2bsmoke-repo --snapshot e2bt2a-150413 → booted from the snapshot id. - agentbox checkpoint rm e2bt2a-150413 → snapshot deleted on E2B, manifest cleared. - agentbox destroy -y e2bt2a e2bt2b → both sandboxes destroyed.

Empirical findings (Task 2, 2026-06-03)

  • Checkpoints capture RAM + disk, not just disk. E2B pause/resume is a full memory snapshot — Sandbox.pause persists RAM + filesystem and Sandbox.connect resumes with running processes intact. createSnapshot rides the same mechanism (it pauses the source while capturing), so an e2b checkpoint includes the memory state. This differs from the docker provider, whose checkpoint is docker commit (disk only). Restoring an e2b box can therefore bring back in-flight processes; a docker box cannot.
  • The platform DOES support DinD (empirically verified 2026-06-23 — see changelog; subsequently shipped on by default). Unlike Vercel, the E2B microVM grants full root with the complete capability set (CapEff: 000001ffffffffff, incl. cap_sys_admin), working unshare --user/--mount, and cgroup v2 — so docker.io + dockerd (overlay2) runs containers inside an agentbox e2b box. This is now baked into the base template with launchDockerd: true (see the shipped changelog entry); the original "same as Vercel → no DinD" inference was wrong.
  • Template.build requires RELATIVE source paths. The SDK's template.copy(src, dest) rejects absolute paths (Invalid source path "/foo": absolute paths are not allowed). We stage every resolved asset into a temp fileContextPath dir under its logical name, then pass the asset name as a relative src — same idea as Docker's COPY semantics.
  • Sandbox.create({ template }) auto-appends :default. A 404 fires with tag 'default' does not exist for template 'xxx' when the templateId we store omits the tag. We persist ${templateId}:${tag} (tag = info.tags[0] ?? 'latest') so the create path always lands on a built tag.
  • e2b SDK must be external to apps/cli's bundle. The TemplateBuilder constructor calls dynamicRequire('node:url') to resolve the caller directory; esbuild's ESM __require shim throws "Dynamic require of 'node:url' is not supported" when this is bundled. Added 'e2b' to apps/cli's external list (same pattern as @daytonaio/sdk and @vercel/sandbox) and added it as a real dep.
  • pty.create accepts a max timeoutMs of 1 hour on the Hobby tier (400: Timeout cannot be greater than 1 hours). The attach helper now caps at 55 minutes; longer sessions will need a keepalive ping that extends the timeout via Sandbox.setTimeout mid-session (future work).
  • Drive harness display quirknode-pty via the drive harness shows an empty screen when the spawned child is the agentbox shell attach, but a plain script -q -c … capture shows the same attach producing a real prompt. Likely a headless-terminal-vs-tmux passthrough mismatch in the drive harness rather than the attach helper itself. Filed for later.

Task 3 — prune + docs + polish · status: DONE 2026-06-03

Goal: close the provider out — wire prune, finish doctor, propagate E2B everywhere the docs still said "four backends", and finalize the backlog as shipped.

  • agentbox prune --provider e2bCLOUD_PRUNE_PROVIDERS already included e2b from Task 1; live-verified the orphan-sweep path (sandbox spawned via the SDK with the agentbox marker tag, then prune --provider e2b --dry-run and -y cleanly removed it). Templates are sandbox-only — E2B's SDK exposes no Template.list(), so prune covers sandboxes only (documented in the provider page).
  • agentbox doctor --provider e2b — credentials + prepared-template checks were added in Tasks 1–2; Task 3 fixed the stale --provider help/error strings in apps/cli/src/commands/doctor.ts that still listed only docker | daytona | hetzner | vercel.
  • Internal docs synced: CLAUDE.md (five backends, ctl shipping list, checkpoint primitive list, ops paragraph, doc-map), docs/cloud-providers.md (intro table + §3c "The E2B shape" + §4 auth + §8 file-map + footer), docs/cloud-create-flow.md (cross-provider callout), README.md (provider table + login + prepare + docs link).
  • Public site (Fumadocs) synced: new apps/web/content/docs/e2b.mdx (mirrors vercel.mdx, leads with the Dockerfile/Template.build() differentiator), meta.json providers list updated, cli.mdx provider section updated.
  • Backlog finalized — "provider status: shipped" header above; deferred follow-ups summarized below.

Task 4 — credential seed unification + bypass-permissions accept · status: DONE 2026-06-03

Goal: fix two showstoppers that landed inside the post-ship agentbox e2b claude flow — E2B's ~/.agentbox-creds/<agent>/ ended up empty (Claude reported "Not logged in") and every fresh box hit Claude Code's one-time "Bypass Permissions mode … Yes, I accept" gate.

  • Bug 1 — credentials seeding. cloud-provider.ts's seedAgentVolumesIfFresh was gated on agentVolumes.agents.length > 0, and ensureAgentVolumesForCloud returned agents: [] for any backend without an ensureVolume primitive. Vercel and Hetzner each duplicated the push in their own backends (pushVercelAgentCredentials, pushHetznerAgentCredentials); E2B had no equivalent. Unified the path: ensureAgentVolumesForCloud now returns the full agent list (['claude', 'codex', 'opencode']) for non-volume backends too, and seedCredentialsOne branches on backend.ensureVolume: volume backends keep the FUSE-friendly cp-via-staging + .agentbox-seeded-at marker; ephemeral backends do sudo -u vscode tar -xzf … --no-same-permissions --no-same-owner -m straight into the box-baked ~/.agentbox-creds/<agent>/ dirs every create (tokens are renewable, the box FS is ephemeral). Dropped the Vercel + Hetzner custom pushers.
  • Bug 2 — bypass-permissions accept gate. Added "skipDangerousModePermissionPrompt": true to packages/sandbox-docker/scripts/claude-managed-settings.json. Disassembly of ~/.local/share/claude/versions/2.1.150 showed the gate is suppressed when any of userSettings | localSettings | flagSettings | policySettings sets that key — policySettings is /etc/claude-code/managed-settings.json, which the docker and cloud installers already bake. Docker boxes happened to skip the gate because the host's ~/.claude/settings.json (mounted via the shared agentbox-claude-config volume) had the user-side equivalent; cloud boxes had no such overlay. Policy precedence covers every provider.
  • Live verified on real E2B. Fresh agentbox prepare --provider e2b, then create from a tiny scratch repo; in-box ~/.agentbox-creds/{claude,codex,opencode}/ carried the seeded files (vscode-owned, 600 mode, content matching ~/.agentbox/<agent>-credentials.json); interactive claude --dangerously-skip-permissions landed straight at the ready prompt — no "Bypass Permissions" accept screen, no theme picker.

Deferred follow-ups (intentional, not blocking)

  • E2B checkpoints are full-state (disk + RAM), not disk-only — disk-only is future work. checkpoint create uses Sandbox.createSnapshot, which pauses the box and persists the entire VM state (memory + filesystem); booting a box from the checkpoint (Sandbox.create({ template: snapshotId })) resumes that paused state rather than booting fresh. Verified live (2026-06-23): a box created from a checkpoint kept the source box's running process (same PID), tmpfs//dev/shm contents, frozen boot_id, and continuing uptime. The E2B SDK (2.27.1 and 2.30.5) exposes no disk-only snapshot option — the snapshot API takes only { name }, and the model is inherently pause-based. This is mostly a cleanliness/fragility concern (a fresh box resumes the source's stale ctl/dockerd/agent processes), NOT a correctness blocker: the create-from-checkpoint flow re-seeds /workspace from the host git bundle (overlay delta), so the per-box branch still updates to the host's current tip — verified: a box from a checkpoint picked up an unpushed host-main commit made after the checkpoint, via checkpoint restore: /workspace: delta bundle (<oldTip>..<newTip>). A true disk-only checkpoint would need Template.build().fromTemplate(base).copy(<box state>) (boots fresh, like the base snapshot) — feasible (the SDK has fromTemplate + copy) but the open question is what FS state to capture (E2B has no docker commit-style rootfs diff), and it is slower to create than the seconds-fast memory snapshot. Tracked as future work; ships as full-state.
  • No Template.list() in the SDK. prune --provider e2b enumerates sandboxes only; built templates have to be removed from the E2B dashboard. Document this in the provider page; ship as-is.
  • 1-hour platform session cap on Hobby. The attach helper caps timeoutMs at 55 minutes (see packages/sandbox-e2b/src/attach-helper.ts
    • the staged apps/cli/runtime/e2b/attach-helper.cjs) to leave headroom under the platform ceiling. NOTE: the box lifetime keepalive IS done — the host relay's cloud-keepalive loop calls renewTimeoutSandbox.setTimeout while the in-box agent is working, so a long agent run no longer dies at the create timeout (bounded by the plan's max session: ~1 h on Hobby). What's still capped is the interactive attach PTY itself (pty.create ≤ 1 h on Hobby) — after 55 min the attach disconnects but the box stays alive and the agent keeps working; just reattach. Extending the live PTY mid-stream remains future work.
  • Drive harness display quirk on the e2b shell attach (Task 2 finding). A plain script -q -c … capture shows the prompt; the headless drive harness shows an empty screen. Underlying PTY bridge works.
  • box.e2bTimeoutMs config key — DONE. Mirrors box.vercelTimeoutMs: the session timeout a new --provider e2b box is created with (default 45 min), threaded via cloudSizingProviderOptions (apps/cli/src/lib/cloud-sizing.ts) into create and the agent-create commands, and recorded as cloud.sessionTimeoutMs so the keepalive seeds its tracked deadline precisely.
  • No e2bNetworkPolicy config key — and it CAN be wired (backlog correction). An earlier note here claimed E2B's network knobs are "template-level (set at Template.build() time)" with "no per-create override." That is wrong: the SDK exposes them at Sandbox.create() time, not build time. The create signature is Sandbox.create(..., allowInternetAccess = true, network?: SandboxNetworkOpts, ...). Only cpuCount / memoryMB are genuinely template-level (baked at Template.build()).
    • What a network policy covers (none of it is ports):
      • EgressallowInternetAccess (default true): whether the box can reach the outside internet at all (the air-gapped-run lockdown).
      • Ingressnetwork: { allowPublicTraffic: false }: makes the preview URL require an e2b-traffic-access-token header instead of being open HTTPS (Task 1 §1 chose public to match Vercel).
      • Domain allow-listing lives under the same SandboxNetworkOpts.
    • Ports are unrelated. Any port is exposed on demand via getHost(port){port}-{sandboxId}.e2b.app; there is no documented port cap and the network policy does not gate individual ports. AgentBox fixes the WebProxy at 8080.
    • Action if picked up: add a box.e2bNetworkPolicy (egress on/off + public-traffic gating) create-time config key and thread it through backend.provisionSandbox.create.
  • box.image cross-provider migration was already handled in Task 2 (per-provider box.imageE2b lives in packages/config/src/types.ts).

Post-ship polish

  • Clean prepare-gate error output (2026-06-03). The "no E2B base template found" gate now throws UserFacingError (from @agentbox/core) and the CLI's top-level catch renders it as a one-line message instead of a raw stack trace. Same treatment for the parallel vercel/hetzner gates.
  • Checkpoint manifest records baseFingerprint (2026-06-03). The custom e2bCheckpoint.create (mirrors vercelCheckpoint.create) now passes baseProvider, baseFingerprint, and cliVersion to writeCloudCheckpointManifest — matching the scaffold default that daytona/hetzner inherit. Before, every e2b/vercel default checkpoint tripped the wizard's "captured before checkpoint versioning; base snapshot unverifiable" branch and stale-prompted forever; now the fresh-vs-stale verdict is real for all four clouds.
  • Login nudge + create-time base-freshness check (2026-06-03). Two paper- cuts off the e2b first-run path:
    • agentbox e2b login now prints a one-line nudge pointing at agentbox prepare --provider e2b when no base template is recorded yet, so the login-only path doesn't silently leave the next create to trip the (newly-clean) "no base template found" error.
    • agentbox create --provider e2b / agentbox claude --provider e2b now detect a stale base template at create time. Decision is made purely on contextSha256 — the SHA over the baked runtime files (CLI version strings are informational and never gate freshness, so a CLI patch that touches nothing baked stays fresh). TTY → merged "rebuild the base?" confirm; -y/non-TTY → loud warn + boot on the existing base (no auto-bake). Verified live: fresh-base TTY skips the prompt; mutating one hex char of base.contextSha256 fires the prompt; -y mode warns and proceeds. Same plumbing covers daytona/hetzner/vercel via the new Provider.baseFingerprint?() capability.

Coordination notes (orchestrator)

  • Each task is assigned to an agent via agentbox claude -i "<prompt>" -- --permission-mode=plan (background box). The agent: plans → (orchestrator answers questions from the other providers) → implements → smoke-tests → opens a PR → fixes bugbot/CI → merges. Orchestrator then pulls main and starts the next task.
  • E2B_API_KEY is in .env.local and reachable inside every box; boxes run the relay and can launch boxes on other providers, so they can fully smoke-test.

Changelog

  • 2026-07-10: Compose + buildx CLI plugins baked into the template (build-template.sh downloads the official release binaries into /usr/local/lib/docker/cli-plugins — bookworm's apt docker-compose is the deprecated python v1, so no distro package). Also fixed the stale custom-system-CLAUDE.md "No containers" paragraph (DinD has worked since 2026-06-23). Templates built before this need a re-prepare --provider e2b.
  • 2026-06-23: Fix: cloud -i background jobs reported done even when the seeded agent session never came up (all cloud providers). The -i queue worker's cloud path (runCloudJobcloudAgentStartDetachedrunDetached in apps/cli/src/commands/_cloud-attach.ts) spawned the detached session-start helper with stdio: 'ignore' and resolved on any exit, discarding the exit code and stderr — so a helper failure (a transient SDK connect/exec error, the agent crashing at launch, or stale in-box credentials) was invisible and the job still wrote status: done. The only error that surfaced was the unrelated best-effort queue.openIn step (e.g. herdr tab gave no pane id), which misled diagnosis toward openIn even though session start is independent of it. Fixes: (1) runDetached now captures the exit code + stderr; (2) cloudAgentStartDetached throws (→ failed job with reason) when the session-create command exits non-zero; (3) new verifyDetachedSession polls tmux has-session after start — a session that's already gone means the agent exited immediately at launch (the exact "no tmux session running" symptom) — and best-effort-scrapes the pane for credential-rejection markers (Please run /login / API Error: 401 / …), failing the job with an actionable agentbox <agent> login hint. Both callers handle the throw correctly: the queue worker marks the job failed; restoreAgentSessions (resume-on-restart) catches + logs. Cloud-wide (daytona/hetzner/vercel/e2b). Verified live on e2b: a job whose box claude creds had lapsed now reports failed with the login hint instead of a false done; a healthy job still reports done with a live session. Unit-tested in test/cloud-attach.test.ts. The seeded in-box claude OAuth token is a point-in-time snapshot that expires independently of the host session (see docs note on box-cred expiry) — agentbox claude login re-seeds it; this fix makes that condition discoverable instead of silent.
  • 2026-06-23: Fix: cloud create with a relative -w path failed the git-clone workspace seed (all cloud providers). agentbox create --provider e2b -w ../repo died with git clone … 'file://../repo' … fatal: '/repo' does not appear to be a git repositoryfile:// URLs require an absolute path, so the relative one resolved to host /repo. The docker provider already does resolve(opts.workspacePath); the cloud Provider.create did not. Fixed by resolving req.workspacePath to absolute at the top of createCloudProvider's create (in packages/sandbox-cloud/src/cloud-provider.ts), mirroring docker — which also keeps the box record's workspacePath cwd-independent. Found while testing e2b checkpoints; the fix is cloud-wide (daytona/hetzner/vercel/ e2b). Verified live: -w ../agentbox-test-repo-gh now clones + completes, and the box record stores the resolved absolute path.
  • 2026-06-23: DinD shipped — docker baked into the base, auto-launched on create/resume (mirrors vercel). Following the empirical verification below, enabled in-box docker by: (1) adding a "docker engine (in-box DinD)" step to packages/sandbox-e2b/scripts/build-template.sh (apt-get install docker.io iptables, groupadd -f docker, usermod -aG docker vscode, systemd docker disabled); (2) baking the shared agentbox-dockerd-start helper — added it to RUNTIME_ASSETS in runtime-assets.ts, the e2bFiles stage list in apps/cli/scripts/stage-runtime.mjs, and the install + trim steps in build-template.sh; (3) flipping launchDockerd: false → true in packages/sandbox-e2b/src/index.ts. The launch path itself (launchCloudDockerdDaemon, called on create + resume in cloud-provider.ts) was already provider-agnostic. Like vercel, only docker.io + iptables are installed (no fuse-overlayfs) — agentbox-dockerd-start's runtime probe picks overlay2, which works on E2B. Cost: ~+200MB base template, ~5-15s dockerd warm-up at create; always-on (not gated behind a config key). Pulled images + the running daemon carry across pause/resume (snapshots capture full disk+RAM). Docs synced: e2b.mdx, docker-in-docker.mdx, cloud-providers.md (intro table + §3c), CLAUDE.md.
  • 2026-06-23: DinD empirically verified to work on E2B — overturns the Task 1 §4 "no nested containers" inference. Two-stage live test on the user's E2B account:
    1. Raw Sandbox.create() (vanilla E2B base): root has CapEff: 000001ffffffffff (full set incl. cap_sys_admin), unshare --user/--mount both rc=0, cgroup v2 mounted. Then apt-get install docker.iodockerd (Server 20.10.24, overlay2) → docker run hello-world and docker run alpine:3 both succeeded; the container reports the host kernel 6.1.158+.
    2. Real agentbox e2b box (agentbox create --provider e2b, baked agentbox-base template): as the vscode user (uid 1002, passwordless sudo) sudo apt install docker.io + sudo dockerd + sudo docker run hello-world|alpine:3 all worked end-to-end. Conclusion: the Vercel "Firecracker → no DinD" reasoning does NOT transfer to E2B; the blocker on Vercel was its stripped capability set + seccomp, not Firecracker. AgentBox's launchDockerd: false is a default (docker not baked in), not a platform limit. Filed "DinD on E2B" as an enable-able follow-up above. (Test note: when running this from inside an agentbox-in-agentbox, the in-box git shim rejects the create's git clone --no-checkout seed step — put the real /usr/bin/git ahead of /usr/local/bin/git on PATH for the host-side create.)
  • 2026-06-23: Fix agentbox e2b claude hanging on a blank screen at attach (then agentbox attach works). Root cause: the create→attach path runs cloudAgentAttach with extraArgs (always --dangerously-skip-permissions) and a new-terminal openIn (iTerm2 split / tmux / cmux), which triggers startDetachedSession to pre-create the agent's tmux session before spawning the new pane (so the re-invoked attach, which carries no extraArgs, finds the session via tmux has-session). startDetachedSessionrunDetached spawns the provider's detached attach argv and awaits its exit. On SSH/Vercel the detached argv runs the inner command (tmux new-session -d …; <config>) as the remote process and exits — but the E2B attach-helper always opens a persistent interactive in-box PTY shell and blocks on handle.wait(); with no trailing exec tmux attach to replace the shell, it idles at a prompt forever, so await startDetachedSession never returns and the CLI hangs right after box ready (no pane ever opens). The tmux new-session had already run, so the session + agent existed — which is why Ctrl+C then agentbox attach rendered fine. Fix: buildE2bAttach now appends --detached to the helper argv when opts.detached is set, and the helper, in that mode, runs the inner command once via the non-interactive sb.commands.run and exits with its code instead of opening a PTY. Verified live: the detached helper now exits in ~1s (was: hung >30s) after creating the session + launching claude, and agentbox claude start <e2b-box> -- --dangerously-skip-permissions completes (Attached in new iTerm2 split.) in ~1s instead of hanging. Regression tests in test/build-attach.test.ts.
  • 2026-06-17: Fix ERR_REQUIRE_ESM crash on Node < 20.19 (e.g. 20.18, which our engines.node >=20.10 permits). The e2b SDK ships no exports map (only main→CJS / module→ESM), so an ESM import 'e2b' falls back to the CJS dist/index.js, which require("chalk"). Its declared chalk@^5.3.0 resolves to the ESM-only chalk@5, and on Node < 20.19 (before require(esm) was unflagged) that require throws ERR_REQUIRE_ESM — crashing the whole CLI at command registration (cli.tscredentials.tssdk.tsimport 'e2b'), so every agentbox command died, not just e2b ones. Fix: a scoped pnpm override e2b>chalk: ^4.1.2 in the root package.json pins e2b's chalk to the CJS chalk@4 (same red/hex/gray/dim API e2b uses), so require("chalk") works on every supported Node. chalk is the only pure-ESM (no require condition) package e2b's CJS build requires — its other ESM deps (@bufbuild/protobuf, @connectrpc/*, openapi-fetch) are dual-package. The rest of the monorepo keeps chalk@5. Verified live on Node v20.18.0: import 'e2b', agentbox daytona login, agentbox e2b login --status, and agentbox --help all load cleanly. Do not remove the override on a dep cleanup — newer e2b (2.30.1) still has the broken packaging.
  • 2026-06-03: Cold-attach "blank pane looks hung" fix (shared cloud path). Symptom: agentbox e2b claude reached box ready, showed the recap panel, then the attach sat on a featureless blank screen long enough that the user Ctrl+C'd; a later agentbox claude attach <n> rendered fine. Investigated live against a running box and ruled out the usual suspects:
    • The PTY bridge is fine — attaching to an existing session paints the first byte in ~275ms and the full frame within seconds.
    • tmux is fine — a session whose program delays its first draw correctly receives the frame the instant the program paints (no redraw bug).
    • The dashboard probe is fine — tmux list-sessions -F '#{session_name}' runs as vscode and returns claude; agentbox list shows claude:working. The earlier "dashboard shows no agent session" was the same transient cold-start window (session not yet created / not yet drawn).
    • claude itself is fast on e2b — native-installer binary, --version 99ms, warm interactive first-paint ~750ms; box has free RAM, no swap. Root cause: the freshly-attached tmux client shows a blank pane during the agent's cold first-run (238MB binary fault-in + first-time config/plugin/skill/MCP init), which on the 2-vCPU microVM is several seconds with zero feedback. Fix: buildCloudAttachInnerCommand (apps/cli/src/commands/_cloud-attach.ts) now prints agentbox: starting <agent> (first paint may take a few seconds)... into the pane before exec-ing the agent, so the attach is never blank. Plain ASCII (no escape codes) so it survives every shell-quoting layer; the agent clears it on first draw. Applies to every cloud provider (e2b/vercel are the slowest cold-starters so they benefit most). Verified live: banner paints at ~380ms against a 6s slow-start stand-in. Tests in test/cloud-attach.test.ts.
  • 2026-06-03: E2B usability pass — onboarding seed + shell exec + VNC + tag. Live-verified end-to-end against agentbox-base:latest.
    • Onboarding (the blocker): cloud creates were dropping into Claude's first-run theme picker. Fixed by overlaying ~/.claude/_claude.json from the host's current ~/.claude.json on every cloud create (across e2b/vercel/hetzner/daytona) — see packages/sandbox-cloud/src/claude-json-overlay.ts. The default fallback (host has no ~/.claude.json) now sets hasCompletedOnboarding: true.
    • One-shot agentbox shell: agentbox shell <cloud-box> -- cmd used to hang because the cloud path always opened a PTY attach via provider.buildAttach. One-shot now routes through provider.exec (the same primitive used at create-time); interactive agentbox shell still uses the PTY attach.
    • VNC: two issues — Debian 12 (E2B's base) doesn't ship vncpasswd (Ubuntu-only tigervnc-tools), and E2B's Python-venv websockify startup needs ~7-9s to bind. Fix: graceful -SecurityTypes None fallback in agentbox-vnc-start when vncpasswd is missing (signed preview URL is the access boundary, same effective model), and bump the cloud probe ceiling 5s → 15s in vnc-launch.ts. Other providers (docker/hetzner/daytona/vercel) keep VncAuth.
    • Doubled :latest:latest: prepare status read tmpl agentbox-base:latest:latest because info.name (already "agentbox-base:latest") got :${tag} re-appended. Now stores info.name verbatim.
  • 2026-06-03: Task 3 done — agentbox prune --provider e2b live-verified end-to-end (orphan spawned via the E2B SDK, --dry-run listed it, -y deleted it); doctor --provider help/error strings extended to include e2b; CLAUDE.md / docs/cloud-providers.md / docs/cloud-create-flow.md / README.md propagated to "five backends"; new apps/web/content/docs/e2b.mdx page added with the Dockerfile/Template.build() differentiator front and center. Provider marked as shipped; deferred follow-ups (templates not enumerable, 55-min/1-hr session ceiling, drive-harness display quirk, no e2bTimeoutMs/e2bNetworkPolicy parallels) recorded above.
  • 2026-06-02: Backlog created; task breakdown + E2B↔CloudBackend mapping drafted.
  • 2026-06-02: Task 1 done — package + CloudBackend core, smoke-tested end-to-end. Open questions answered (preview URL is public by default; checkpoint deferred to Task 2; webProxyPort=8080; no dockerd; resources are template-level). Three cloud-scaffold gaps surfaced and fixed here: a Buffer→ArrayBuffer conversion for sb.files.write, a CommandExitError→CloudExecResult catch, and the carry root carve-out extended from vercel to e2b.
  • 2026-06-03: Task 2 done — prepare (Template.build), interactive attach via the SDK's PTY API, and checkpoint via Sandbox.createSnapshot. Smoke-tested end-to-end against /tmp/e2bsmoke-repo. previewUrl no longer wakes paused boxes. Per-provider config keys (box.{image,defaultCheckpoint,size}E2b) added. Three SDK gotchas surfaced: template.copy rejects absolute paths (staged via temp fileContextPath); Sandbox.create auto-appends :default so we store the tagged ${id}:${tag}; the e2b SDK must be external in apps/cli bundles (dynamicRequire on node:url).