Vercel provider
July 10, 2026 · View on GitHub
Status of the @agentbox/sandbox-vercel backend (Vercel Sandbox — Firecracker
microVMs + snapshots). Same CloudBackend shape as Daytona/Hetzner, composed by
@agentbox/sandbox-cloud's createCloudProvider. Maintained live during
implementation (per the project convention), not as end-of-PR cleanup.
Why Vercel is shaped differently
- No custom image. Vercel Sandbox is Amazon Linux 2023 only; there's no
Dockerfile build. The base environment is a Vercel snapshot baked once by
agentbox prepare --provider vercel(boot fresh node24 → runprovision.sh→sandbox.snapshot()), exactly the hetzner-style one-time prerequisite. - Nested containers (in-box docker) supported. Vercel added Docker support
in 2026 (https://vercel.com/changelog/run-docker-containers-inside-vercel-sandbox),
reversing the earlier "no containers" finding (memory
project-vercel-sandbox-no-containers). The base snapshot now bakes the docker engine and the provider setslaunchDockerd: true, so the cloud scaffold auto-startsdockerd(via the sharedagentbox-dockerd-start) on create/resume. The socket is widened to 0666 so the unprivileged box user and the ctlimage:services drivedockerwithout sudo. Pulled images carry over across pause/resume (persistent snapshot). - No SSH.
sandbox.domain(port)is an HTTPS(+WebSocket) proxy only. There's noattachArgv; attach goes through a custom SDK-streaming helper. - Persistent by default. Stopping a sandbox auto-snapshots; the next
Sandbox.get({ resume: true })resumes from it. That maps cleanly to pause/resume —pause == stop,resume == start. - Hard limits: region
iad1only, 32 GB fixed ephemeral disk, 2048 MB RAM per vCPU (coupled), ≤4 exposed ports (we use 80 / 6080 / 8788, one free), 45 min (Hobby) / 5 hr (Pro+) max session.
Phase status
- Phase 0 — package scaffold.
packages/sandbox-vercel(tsup/tsconfig/ vitest),@vercel/sandboxdep, registry + argv-prefix + CLI registration, configProviderKind/defaultCheckpointVercel, relayresolveCloudBackend. - Phase 1 — credentials + SDK loader. OIDC (
VERCEL_OIDC_TOKEN) and access-token trio (VERCEL_TOKEN/VERCEL_TEAM_ID/VERCEL_PROJECT_ID);agentbox vercel login+--status; env auto-load from~/.agentbox/secrets.envonly (project.env/.env.localare not harvested, matching daytona/hetzner). - Phase 2 —
CloudBackend. provision/get/list/start/stop/pause/resume/ destroy/state/exec/uploadFile/downloadFile/listFiles/previewUrl/ signedPreviewUrl + snapshot helpers, all mapped to@vercel/sandbox2.x. - Phase 3 — prepare + provision.sh. Base-snapshot bake with context
fingerprinting + skip-fast; AL2023 installer (dnf, vscode user, ctl/vnc/shims,
Claude native installer, codex/opencode). 2026-07-10: compose + buildx CLI
plugins added (official release binaries — AL2023 ships neither); snapshots
baked before this need a re-
prepare --provider vercel. - Phase 4 — attach.
buildVercelAttachdrives the VercelsandboxCLI's real PTY (sbx exec -i … -- sudo -u vscode -H bash -lc '<tmux attach>'). (Was a customattach-helper.jssend-keys/capture-pane bridge — replaced; see #8.) - Phase 5 — checkpoints. Provider-level
checkpointoverride storing the Vercel snapshot id in the cloud-checkpoint manifest (Vercel snapshots are id-addressed, not name-addressed). - Phase 6 — unit tests. env-loader, credentials, prepared-state,
backend (mocked SDK), build-attach.
pnpm build && lint && typecheck && testall green. - Phase 7 — "Sign in with Vercel" (CLI-login) auth. A third, default
agentbox vercel loginmode that drives the official Vercelsandbox/sbxCLI (npm i -g sandbox) through its browser OAuth, then reuses the CLI's own credentials for our SDK calls — no token to paste, and no 12h OIDC friction.- Token is never copied to
secrets.env: only the markerVERCEL_AUTH_SOURCE=cli+VERCEL_TEAM_ID+VERCEL_PROJECT_IDare cached.resolveCredentials()reads the livevca_access token straight from the Vercel CLI store (auth.json) every call (cli-store.ts), so the CLI is the single self-refreshing source of truth. ensureFreshCredentials()(sdk.ts, called at the top of every backend op + prepare + attach-helper) detects a near-expiry token (120s skew) and triggers the CLI's own lazy refresh by runningsbx list(sbx-cli.ts), then re-reads the rotated token. In-process single-flight collapses concurrent ops.- Project resolution: the OAuth token is team-scoped with no project, so after
login we list the team's projects (
vercel-rest.ts) and the user picks one (clack select, pre-selectingagentbox/vercel-sandbox-default-project, with a create-new option) — cached asVERCEL_PROJECT_ID. - Team resolution (fixed 2026-06-16). The current
sbx/sandboxCLI no longer writescurrentTeamto itsconfig.json, so the post-login harvest used to dead-end with "no credentials were found in the Vercel CLI store" even on a successful sign-in.runCliLoginnow resolves the team in precedence orderVERCEL_TEAM_ID→ CLIcurrentTeam→ the account'sdefaultTeamId(fromGET /v2/user, whichgetUsernow surfaces) — the same teamsbx loginscopes its default sandbox project to. Verified live against the real CLI store + API (currentTeamnull,defaultTeamIdcarried the team through). - Store path resolver is platform-aware (macOS
~/Library/Application Support, Linux$XDG_DATA_HOME/~/.local/share, Windows%APPDATA%) with anAGENTBOX_VERCEL_CLI_DIRoverride for tests. - PoC-validated 2026-05-29:
sbxrefreshes a stale token fully non-interactively (no browser); thevca_token works as a Bearer againstapi.vercel.comand the SDK;GET/POST /v9/projectsaccept it. New unit tests:cli-store,cli-auth(resolve + ensureFreshCredentials),vercel-rest. - PAT-trio and OIDC modes are unchanged, so headless/CI is unaffected.
- Live-verified end-to-end 2026-05-29.
agentbox vercel login→ browser OAuth → project pick wrote only the marker + team + project tosecrets.env(no token);--statusshowed the live token (expires in 8h) from the CLI store.prepareskip-fast confirmed the base snapshot via a real API call on the new auth;createbooted a box (provision/exec/uploadFileall on the live token) anddestroytore it down — all without a stored token.
- Token is never copied to
What's still missing
The code builds/lints/typechecks and the unit suite (pure, mocked SDK) is green.
Two live e2e passes ran 2026-05-28, both with a VERCEL_TOKEN access-token
trio (VERCEL_TOKEN/VERCEL_TEAM_ID/VERCEL_PROJECT_ID) — every dev OIDC token
kept arriving already-expired, so the access-token trio is the practical path for
anything long-running like prepare. The two passes (in-box, then re-validated
from the host repo via scripts/vercel-live-e2e.sh) confirmed prepare → create →
boot → pause/resume → checkpoint round-trip → destroy, and surfaced three real
bugs, all now fixed (see "Bugs found live" below). Only the relay round-trip (#4)
is still unconfirmed (it's interactive — needs a pushable origin). The list below
is the actionable backlog, roughly in priority order.
P0 — first live smoke pass
Confirmed live 2026-05-28:
-
prepare/provision.shcompletes on AL2023. Bakes a base snapshot (~1.3 GB) in a few minutes; the snapshot comes back usable. claude / codex / opencode are all present in a booted box. - User mapping. A booted box runs as
vscode(uid 1001) with/workspacechecked out onagentbox/<box>. vscode passwordless sudo now works (see bug #3). (In-boxdockeris now baked in + auto-started —launchDockerd:true; this note historically tracked the now-reversed "no docker" state.) - Workspace seed. The shallow-clone seed (
$SUDO rm/mkdir/chown+ tar-extract as vscode) lands/workspaceon the box branch — gated on the sudoers fix (#3). Agent-credential / carry / env-file ownership beyond this was not separately audited but the box boots with the agent CLIs present. - Relay round-trip. Confirmed live 2026-05-29 by ground truth (not a
wrapper exit code). On box
relayv1: an in-boxagentbox-ctl git pushof commit7f8eea5dtraveled vercel box → in-box bridge onsandbox.domain(8788)→ hostCloudBoxPoller→runGitRpc(git-bundle pull-back + hostgit push origin), andgit ls-remote origin refs/heads/agentbox/relayv1returned7f8eea5d— the commit reached GitHub. The relay log showsrpc box=40ebb05d method=git.push. The push was driven viabackend.exec(real exit code), not the attach pump. Getting here required fixing several real issues found live (see "Bugs found live 2026-05-29" below): secrets.env-only credentials, the missing attach-helper chunk in the staged runtime, the #19 PATH/shim ordering, and a stale host-relay process — the running relay predated vercel support, soresolveCloudBackendreturnedno host executor for cloud backend 'vercel';ensureRelayonly reclaims a relay whencliEntry===false, not when it lacks a provider executor, so it silently reused the old one. Killing it (nextagentboxcall respawns a capable relay) fixed it — see the follow-up item below to make this self-healing. Notes on the earlier false positive: the originalvercel-live-e2e.shPhase D trusted theagentbox shell … git pushexit code, but on vercelshellis the laggy send-keys/capture-pane attach pump whose exit code reflects the wrapper, not the in-box command — it reported PASS while nothing reached origin. Phase D now gates ongit ls-remote(the probe branch is absent before, present after); it also needs theVERCEL_TOKENtrio in env (it predates CLI-login auth, so underVERCEL_AUTH_SOURCE=clidrive the round-trip via the CLI/backend.execdirectly).- Update (relay token now read from a file, not login-shell env). The in-box
relay token reaches
agentbox-ctlvia a0600 /run/agentbox/relay.envwritten by the ctl daemon (read byresolveRelayEnvinpackages/ctl/src/relay-env.ts), not via login-shell env. This fixed the regression (b9e4ebf55) where the in-box agent'sagentbox-ctl git pushfailed "no relay configured" because the daemon'sbox.envoverwrite dropped the token. Applies to all cloud providers; the bridge token used by thesandbox.domain(8788)/bridge/*round-trip above stays daemon-only. Original plan (host — must run on a real host, not a nested box): - Why nested doesn't work: a vercel box's
CloudBoxPollerruns wherever theagentboxCLI runs; from inside a docker agentbox the relay/git creds chain is doubly-nested and the vercel box's bridge can't cleanly reach the laptop's git identity. Run this from the laptop host. - The public-URL plumbing is already validated:
sandbox.domain(8788)is a stable public HTTPS+WS endpoint (the #17 expose work + the bridge both rely on it; preview URLs proved stable across stop/start in P0 #5). So the transport the poller needs is known-good. - Runbook:
agentbox create --provider vercelfrom a repo with a pushable origin →agentbox shell <box>→ commit in/workspace→agentbox-ctl git push→ on the host confirmgit ls-remote origin agentbox/<box>shows the commit → thenagentbox-ctl git pulland agh prop.scripts/vercel-live-e2e.shgates this behindE2E_RELAY=1. - Note:
agentbox-ctl git pushvia the host relay has been exercised continuously on the docker provider (the relay's git-RPC path is the samegit.push/git.fetchhandler); what's unconfirmed is specifically the vercel box →domain(8788)bridge → host poller →git.pushchain end-to-end.
- Update (relay token now read from a file, not login-shell env). The in-box
relay token reaches
- Lifecycle semantics.
stopauto-snapshots (live statusrunning → stopping → stoppedin ~18 s);startresumes (get({resume:true})) with the same/workspace(marker survived);destroypreserves the base; the public*.vercel.runpreview URL is stable across a stop/start (did not rotate). - Checkpoint round-trip.
agentbox checkpoint createsnapshots, the manifest stores the Vercel snapshot id, andcreate --snapshot <ref>boots from it with the captured/workspaceintact.
Bugs found live 2026-05-29 (fixed — during the #4 relay round-trip pass)
- Vercel creds in
.env.localwere invisible to the host CLI. The env-loader harvested onlyVERCEL_OIDC_TOKENfrom.env.localand read the access-token trio solely from~/.agentbox/secrets.env, so a trio sitting in.env.localleftprepare/createfailing with "Vercel credentials not configured." Fixed by dropping.env.localreading entirely — Vercel creds now come only from the shell env or~/.agentbox/secrets.env, matching the daytona/hetzner loader (project.env/.env.localbelong to the app, not the host CLI). The access-token trio is now the primaryagentbox vercel loginpath. agentbox shellon a vercel box diedERR_MODULE_NOT_FOUND. The stagedruntime/vercel/attach-helper.js(a tsup entry) imports a shared./chunk-<hash>.js, butstage-runtime.mjscopied onlyattach-helper.js, not the chunk — and the hash changes every build so it can't be listed statically. This broke the attach path the e2e uses to run the in-boxagentbox-ctl git push, failing Phase D before the relay was even exercised. Fixed by walking the import graph from the stagedattach-helper.jsand staging every chunk it (transitively) pulls in.- #19 — relay shims lost PATH on AL2023.
/opt/git/binprecedes/usr/local/binon Vercel's base, so the relay-routinggit/ghshims were inert for agent-typed commands. Fixed inprovision.sh's login-shell shim (/etc/profile.d/agentbox.sh) by force-prepending/usr/local/bin. Baked into the base snapshot (provision.sh is in the prepare context fingerprint).
Bugs found live 2026-05-28 (fixed)
- vscode had no working passwordless sudo → workspace seed failed. Vercel's
AL2023 base ships
/etc/sudoerswith no@includedir /etc/sudoers.d(and non-0440 perms), so provision.sh's/etc/sudoers.d/90-agentbox-vscodedrop-in was silently ignored andsudo -nas vscode failed with "a password is required" — breaking the workspace-seed$SUDO rm/mkdir/chown(and it would break ctl-launch / carry too). provision.sh now appends the includedir, normalises/etc/sudoersto 0440, andvisudo -cf-validates the result. destroynuked the shared base snapshot. A box created from a snapshot hascurrentSnapshotId === sourceSnapshotIduntil it pauses/snapshots itself, so a naive "deletecurrentSnapshotIdon destroy" deleted the shared base and broke every latercreatewith a 410.destroynow purges only a box's own auto-snapshot (snapId !== source && snapId !== base). Covered by a unit test inpackages/sandbox-vercel/test/backend.test.ts.prepareskip-fast treated a deleted snapshot as present.Snapshot.getresolves deleted/failed tombstones (status: 'deleted'|'failed',sizeBytes: 0) instead of throwing, so "get didn't throw" wrongly meant "exists." The skip check now requiresstatus === 'created'(prepare.ts).- Per-box snapshot eviction nuked the shared base/checkpoint (410 on next create).
Same root cause as the
destroybug but via the automatic retention policy: a box boots from a shared snapshot (base or asetupcheckpoint), which Vercel reports as the box'scurrentSnapshotIduntil its first auto-snapshot — so it's the first member of the box'skeepLastSnapshotswindow. Withcount: 1, deleteEvicted: true, the box's first stop/snapshot evicted and deleted that shared source, and every latercreatehit410 Snapshot expired or deleted.Fixed bydeleteEvicted: false+ a sandbox-levelsnapshotExpiration: 0so evicted snapshots are never deleted or re-stamped with a finite expiry (backend.ts). Trade-off: a little snapshot accumulation across many pause cycles instead of ever nuking a snapshot another box depends on. - Stale checkpoint now self-heals instead of crashing create. If a checkpoint snapshot
is gone anyway (reaped before this fix, or deleted from the dashboard),
createno longer dies with a raw 410. The wizard probes liveness (backend.snapshotExists) before announcing "starting from checkpoint …", prunes the dangling local manifest, and re-asks the setup wizard (checkpoint-lookup.ts→probeCloudCheckpoint). The non-interactive /-ypath is covered by a reactive fallback incloud-provider.ts: a snapshot-gone error fromprovision()prunes the manifest and re-provisions from the base image (start from scratch). Detection isisSnapshotGoneError(snapshot-error.ts); covered by unit tests inpackages/sandbox-cloud/test/checkpoint.test.tsandpackages/sandbox-vercel/test/backend.test.ts.
The platform-side root causes (the AL2023 sudoers gap, the Snapshot.get
tombstone behavior, the currentSnapshotId === sourceSnapshotId aliasing, the
list/get inconsistency, and the headless-OIDC refresh failure) are written up
for the Vercel team in docs/vercel-sandbox-findings.md.
Running the remaining P0 checks
scripts/vercel-live-e2e.sh automates items #5 (pause/resume + /workspace
survival) and #6 (checkpoint round-trip), plus a regression for the destroy/base
guard. It must run from a context that holds a VERCEL_TOKEN trio — e.g. the host
repo checkout (with pnpm build run) or a box with the repo built, and the trio
in env. Pass AGENTBOX_BIN="node <repo>/apps/cli/dist/index.js" since the
published CLI can't do --provider vercel yet (backlog #9). It avoids the laggy
attach bridge: the /workspace marker travels over agentbox cp (the
relay-backed provider transfer), the snapshot id is read from the checkpoint
manifest, and box state is read from the live Vercel SDK
(packages/sandbox-vercel/test/live-state.mjs) — not agentbox list, which
reports cloud boxes as optimistically running with no live probe
(sandbox-docker/src/lifecycle.ts, "tracked for Phase 6").
VERCEL_TOKEN=… VERCEL_TEAM_ID=… VERCEL_PROJECT_ID=… \
AGENTBOX_BIN="node $PWD/apps/cli/dist/index.js" bash scripts/vercel-live-e2e.sh
Item #4 (relay round-trip) is inherently interactive and needs a pushable origin
the host relay can reach, so it's opt-in (E2E_RELAY=1) and otherwise printed as
a manual runbook: agentbox shell <box> → commit in /workspace →
agentbox-ctl git push → confirm on the host that git ls-remote origin agentbox/<box> shows the commit, then try agentbox-ctl git pull and a gh pr.
P1 — known functional gaps
- VNC on AL2023. Root-caused live: every piece was actually present
(Xvnc, vncpasswd, websockify, noVNC) and Xvnc bound 5901 fine — the only bug
was
agentbox-vnc-starthardcoding--web=/usr/share/novncwhile the AL2023 bake git-clones noVNC to/usr/local/share/novnc. websockify runsos.chdir(--web)at startup, so the missing dir raised FileNotFoundError and it never bound 6080. Fixed the (shared) script to resolve the noVNC dir from[/usr/share/novnc, /usr/local/share/novnc](Debian/Ubuntu first → docker + hetzner unaffected). Verified live on a snapshot box: 6080 binds,vnc.htmlreturns HTTP 200, web root/usr/local/share/novnc. The script is in the prepare context fingerprint, so the nextagentbox prepareauto-rebakes. Clipboard tooling resolved:xclipandautocutselare absent from the AL2023 repos (unlike the Debian/Ubuntu apt path), which left the host->box Ctrl+V image paste (apps/cli/src/lib/paste-image.ts) silently broken on vercel and VNC clipboard sync degraded.provision.shnow builds both from source (all deps in the AL2023 default repos), fail-loud. Requires a re-prepareto take effect. (Follow-up: building from source pulls a full toolchain into the bake and adds ~minutes + snapshot size. We should instead ship pre-builtxclip/autocutselbinaries for AL2023 — bake them once and stage them as runtime assets, the way the relay/ctl bins are staged — so the per-prepare bake just drops the binaries in rather than compiling. Optionallydnf removethe build toolchain after compiling in the meantime.) - Attach is laggy. Done — replaced the
send-keys/capture-panepump with the official VercelsandboxCLI's real PTY.buildVercelAttachnow emitssbx exec --sudo [-i] --project <p> --scope <team> <name> -- sudo -u vscode -H bash -lc '<inner>'(interactive-ifor shell/agent; non-interactive for detached pre-start + logs, which stream live), with the token passed via the child envVERCEL_AUTH_TOKEN(addedAttachSpec.env, threaded through the host PTY wrapper + the three spawn sites).<inner>reuses the shared cloudrenderInnerCommand(same tmux ensure + footer-aware config +exec tmux attachas hetzner/daytona). The customattach-helper.tsbridge + its stage-runtime chunk staging are deleted.sbxis ensured atagentbox vercel login(all modes). The ttyd/WebSocket upgrade below is obsolete —sbx execgives the real PTY with no re-bake and no extra port. PoC-validated 2026-05-29 (default uservercel-sandbox→--sudo+sudo -u vscode; live streaming; env-token auth; tmux-as-vscode) and live e2e (detached pre-start creates a reattachableagentsession). Interactive typing/resize/ detach is the remaining manual TTY check. - Published-CLI asset staging. Done —
stage-runtime.mjsnow stages aruntime/vercel/tree (attach-helper.js + provision.sh + ctl/shims + baked config) mirroring the candidatesruntime-assets.tsalready resolved, andbuild-attach.ts'sresolveAttachHelperPath()gained theruntime/vercel/(next-to-dist) candidate for the bundled CLI. Verified: all 11 runtime assets resolve from the staged tree with the monorepo fallback disabled. - Builder cleanup after
prepare. Done — verified live that a Vercel snapshot is independent of its source: aftersnapshot({expiration:0})→builder.delete(), the snapshot staysstatus: 'created'(256 MB) and boots a fresh sandbox.prepare.tsnow deletes the builder (step 8) best-effort after persisting the snapshot id, so a delete failure never breaks the bake. - OIDC 12h expiry friction. Done (doc) —
docs/cloud-providers.mdnow has a "Which to use" note recommending the access-token trio for long ops (prepare, CI) since OIDC dev tokens expire ~12h with no headless refresh, and theagentbox vercel loginprompt/labels say the same at the decision point. (Auto-refresh itself remains unbuilt — this is the documentation half.) - Per-provider vercel resource/timeout config. Done — added
box.vercelVcpus+box.vercelTimeoutMs(flat keys, matching the existingbox.defaultCheckpointVercelconvention rather than a one-off nestedbox.vercelobject). Threaded config →providerOptions(vercel-only) →cloud-provideroverrides →CloudProvisionRequest.timeoutMs+resources.cpu→Sandbox.create({ resources: { vcpus }, timeout }). Verified live:vercelVcpus=4yieldssandbox.vcpus === 4(default 2). Region stays fixediad1(Vercel constraint). Note: Vercel only accepts specific vcpu counts (1/2/4/8); an unsupported value (e.g. 3) fails create with a 400. 13b. [x] Per-box agent credential push. Done —vercelBackend.provisionnow pushes the host's renewable credentials (.credentials.jsonfor claude,auth.jsonfor codex/opencode) into/home/vscode/.agentbox-creds/<agent>/at create time, mirroring Hetzner's scp push over the Vercel SDK (writeFiles+sudo -u vscode tar). Reuses the sharedstage{Claude,Codex,Opencode}CredentialsForUploadstagers from@agentbox/sandbox-cloud; the credential-pivot symlinks already baked byprovision.shroute~/.claude/.credentials.jsonetc. to that dir. Pushes every create (renewable tokens), best-effort (a failure logs + falls back to interactive login). Closes the agent-credential gap from the no-volume short-circuit. Unit-tested intest/push-credentials.test.ts. 14b. [x] Chromium baked foragent-browser. The login-shell shim exportedAGENT_BROWSER_EXECUTABLE_PATH=/usr/local/bin/chromiumandprovision.shinstalled theagent-browserCLI, but never installed Chromium nor created that symlink — soagent-browser openfailed withFailed to launch Chrome at "/usr/local/bin/chromium": No such file or directory.provision.shnow mirrors docker/hetzner: installs the AL2023 (dnf) Chrome runtime libs —nss nspr atk at-spi2-atk at-spi2-core cups-libs libdrm libxkbcommon libX{composite,damage,fixes,randr,ext,11} libxcb mesa-libgbm pango cairo alsa-lib liberation-fonts(the dnf equivalents of the Ubuntut64deps; the Ubuntu names don't exist on AL2023) — thenplaywright install chromiumas vscode and symlinks the resolved binary to/usr/local/bin/chromium. The bake fails loud (exit 70if the binary doesn't resolve,exit 71iflddshows unresolved libs) so an incomplete AL2023 dep set surfaces at prepare time, not at first launch. Headless launch is the success bar;--headedstill needs the VNC X server on:1. Live-verified 2026-05-30: re-baked snapshot,lddclean, headless--dump-dom https://example.comworks,agent-browser openreturns ok, and a headed Chromium renders over the noVNC desktop. 14c. [x]agentbox urlfallback when :80 isn't exposable. Vercel rejects privileged ports, so the WebProxy:80is never exposed andsb.domain(80)throws "No route for port 80" —agentbox urlerrored instead of opening the app. The shared cloudresolveUrl(packages/sandbox-cloud/src/cloud-provider.ts) now catches a failedkind:'web'resolve and falls back to the first exposedexpose:service port (from the box record'spreviewUrls, else re-read fromagentbox.yaml). Daytona/Hetzner expose:80directly so the branch never runs for them. Live-verified 2026-05-30:agentbox url --printon the express-ready box returns the service preview URL and curlsHTTP/2 200(Express) instead of erroring. (Superseded as the primary path by 14d — kept as a harmless safety net.) 14d. [x] WebProxy on a non-privileged port (8080) sourl/screenwork for an in-box-onlyagentbox.yaml. When the web service is declared by anagentbox.yamlthat lives only inside the box (host workspace has none), no app port is exposed at create, soagentbox urlhad nothing to reach. Empirically established (PoC, 2026-05-30): Vercel rejects privileged ports (update({ports:[...,80]})→ 400) andsandbox.updatecan't add a routable port to a running box (create-timedomain(6080)=200 vs update-addeddomain(3000)=502, same instant, both with live listeners; docs confirmdomain()is for "a port you exposed during creation"). So ports must be inSandbox.create({ ports }). Fix: run the in-box WebProxy on 8080 and expose it at create. The WebProxy (owned by the supervisor) forwards8080 → 127.0.0.1: <expose.port>from the in-box yaml, so it works regardless of where the yaml lives. Changes: ctl WebProxy listen port is configurable viaAGENTBOX_WEB_PROXY_PORT(supervisor.ts/daemon.ts);CloudBackend.webProxyPort(default 80) — vercel sets 8080;VERCEL_EXPOSED_PORTS = [8080, 6080, 8788];cloud-providerusesbackend.webProxyPortfor the web preview / recordedwebPort/resolveUrl, and threads it to the box viactl-launch. Alsoagentbox screennow opens the in-box Chromium onto the VNC desktop at the samedomain(8080)URL the host uses (the box reaches its own*.vercel.run— hairpin verified 200 host + in-box). Requires a re-bake (ctl is baked into the snapshot) and applies to newly-created boxes (ports are fixed at create). Live-verified 2026-05-30: WebProxy logs:8080 -> 127.0.0.1:3000,agentbox url→domain(8080)curlsHTTP/2 200(Express), and the noVNC screenshot shows the in-box Chromium on the same URL rendering the app. Out of scope: multiple exposed service ports; docker/hetzner/daytona stay on:80.
P2 — deferred (parity niceties, not blocking)
-
agentbox checkpoint listaggregate view. Done —lsnow merges all four providers (docker + daytona + hetzner + vercel) via aCLOUD_BACKENDSloop, andset-default/rmare provider-complete too (set-default acceptshetzner/vercel; rm removes their snapshots and sweeps thedefaultCheckpointVerceldangling pointer).apps/cli/src/commands/checkpoint.ts. Per-project snapshot tier (Closed — redundant. The per-project snapshot tier already exists: it's theprojects[<hash>]inprepared-state.ts).agentbox checkpoint create --set-default+box.defaultCheckpointVercelflow (validated live in P0 #6). On a repeatcreate,resolveDefaultCheckpoint(cfg, 'vercel')→req.checkpointRef→resolveCloudCheckpoint→backend.provision({ snapshot })boots from the project snapshot andcloud-provider.tslogs "skipping workspace seed — snapshot already contains /workspace"; the supervisor restarts services fromagentbox.yaml. The cloud-checkpoint store is already keyed byhashProjectPath(projectRoot), so it is per-project. Seedocs/cloud-create-flow.md§"base vs project snapshot".- Auto-capture is already handled by the
/agentbox-setupskill, which runsagentbox-ctl checkpoint --name setup --replace --set-defaultat the end of setup (apps/cli/share/agentbox-setup/SKILL.md) — cross-provider, so no coreafterFirstCreatehook is needed. - The proposed
projects[<hash>]prepared-state map would be a second per-project snapshot registry duplicating the checkpoint store. It was modelled on a never-wired Hetznerprojectsfield (since removed). Not building it.
- Auto-capture is already handled by the
-
agentbox prune --provider vercel. Done — generalized the daytona-onlypruneDaytonainto a provider-agnosticpruneCloudover aCLOUD_PRUNE_PROVIDERSlist, so--provider vercel(andhetzner, also previously unwired) now enumerate orphan sandboxes viabackend.list()and offer to delete the ones absent fromstate.json.apps/cli/src/commands/prune.ts. Sandbox.fork()— won't build (SDK fork is strictly weaker than checkpoint + create). Investigated 2026-05-29; decision: shelved, no code. The proposal was a faster Vercel-native "branch from a running box" than snapshot + create. Reading the SDK kills the premise. Finding.Sandbox.forkin@vercel/sandbox@2.0.1(dist/sandbox.js:347-386) is, in full:
It reads the source's last existing snapshot and creates from it. It does not take a fresh snapshot, not capture memory, and not read the live filesystem. Theconst source = await Sandbox.get({ name: sourceSandbox, resume: false }); // does NOT touch the source const snapshotId = source.currentSnapshotId; // the LAST existing snapshot return Sandbox.create({ ...copiedConfig, ...overrides, source: snapshotId ? { type: 'snapshot', snapshotId } : undefined, runtime: snapshotId ? undefined : source.runtime });.d.tsphrase "inherits the source's current filesystem snapshot" is technically true but misleading — "current snapshot" means last snapshot, not live FS. Why that makes it useless here, given Vercel's snapshot model (persistent boxes only snapshot on stop; while runningcurrentSnapshotId === sourceSnapshotId, the base — see the destroy-guard inpackages/sandbox-vercel/test/backend.test.tsand theproject-vercel-snapshot-lifecyclefinding):- Forking a running, never-stopped box yields the base snapshot — none of the agent's work. Surprising and dangerous.
- Capturing the source's current work requires snapshotting it first, which on
Vercel stops the VM — at which point
agentbox checkpoint create+agentbox create --snapshot <ref>(already implemented, all four providers) does exactly the same thing. So fork collapses to "create from the source's last snapshot," strictly weaker than the existing path. The only thing that would justify a new box→box primitive — branching a live, mid-flight agent (process + memory) without disturbing the source — is not offered by the Vercel platform (cold, VM-stopping snapshots only). Revisit condition. Only worth building if a provider ships live / memory snapshots (fork the running VM's RAM, source untouched). Until then the box→box workflow we actually want (start from a previous version → self-update viaagentbox.yaml→ agent re-branches from main) is served by checkpoints +create. Platform-side write-up:docs/vercel-sandbox-findings.md.
- Per-service
exposeports. Done — the cloud scaffold already minted a preview URL perservices.*.expose.port, but on Vercel a URL only routes to a port declared atSandbox.create({ ports }), and only[6080, 8788]were declared, so those URLs 404'd. Now the cloud provider reads the expose ports before provision and threads them viaCloudProvisionRequest.exposePorts; the vercel backend'sbuildExposedPortsmerges the non-privileged ones (Vercel 400s on <1024) onto the base set, capped at Vercel's 4-port limit (so up to 2 service ports). daytona/hetzner ignore the field (one WebProxy routes all ports). Verified live: provisioning withexpose 3000declares[6080, 8788, 3000]and the publicdomain(3000)URL routes to an in-box service. Unit-tested (buildExposedPorts: privileged-drop, dedupe, 4-cap). Port 80 (the in-box WebProxy) still can't be exposed on Vercel — services must listen on a non-privileged port to get a preview URL. -
networkPolicyegress locking. Done — addedbox.vercelNetworkPolicyconfig (allow-alldefault /deny-all/ comma-separated domain allowlist), threaded config →providerOptions(vercel-only) →CloudProvisionRequest.networkPolicy→Sandbox.create({ networkPolicy }).parseNetworkPolicymaps the string to the SDK shape (pass-through literals; else{ allow: [...] }). daytona/hetzner ignore it (hetzner locks egress via its own firewall). Verified live: adeny-allbox can't reachexample.com; anexample.com-allowlist box reachesexample.combut notapi.github.com. Unit-testedparseNetworkPolicy.extendTimeoutis now wired viaCloudBackend.renewTimeout+ the host relaycloud-keepaliveloop: while the in-box agent is active the box's session timeout is renewed (anchored atlastActivity + autopause.idleMinutes) so a long agent run isn't killed mid-work. Bounded by the plan cap (mainly benefits Pro+). Same loop covers E2B (Sandbox.setTimeout). - gh/git relay shims don't win PATH on Vercel AL2023. Found 2026-05-29 while
verifying the git-shim-ordering fix: in a booted vercel box
command -v gitresolves to/opt/git/bin/git, not/usr/local/bin/git— Vercel's base prepends/opt/git/binahead of/usr/local/bin. So the relay-routing shimsprovision.shinstalls at/usr/local/bin/{git,gh}are inert at runtime: an agent that typesgit push/gh pr ...inside the box hits the real binaries (no host creds) instead of routing through the relay. (The relay-driven path —agentbox-ctl git push, the host poller — is unaffected; it callsctldirectly, not via thegitshim. So this is about agent-initiated git/gh, and is closely tied to #4.) Plan (host):- Make
/usr/local/binwin for the agent's shells. The login-shell shim/etc/profile.d/agentbox.sh(provision.sh step "login-shell shim") is the natural place — prepend/usr/local/binahead of/opt/git/binin PATH there. Agent sessions run through a login shell (tmux), so this covers them; confirm it also covers non-loginexecpaths if any rely on the shim. - Alternative/back-stop: also drop the shims into
/opt/git/bin/{git,gh}(or symlink) so they win regardless of PATH order — but that dir is Vercel's, so prefer the PATH fix. - Verify live: in a booted box
command -v git→/usr/local/bin/gitandgit pushwith no relay errors out via the shim (not real git). - Note: docker/hetzner are unaffected (their base PATH puts
/usr/local/binfirst); this is Vercel-base-specific.
- Make
-
AGENTBOX_BOX_HOSTresolves to the real preview host. Done — the{{AGENTBOX_BOX_HOST}}placeholder used to derive<box-name>.localhost, which is unreachable on Vercel (no portless). The shared cloud create/start flow now resolves the web preview URL beforelaunchCloudCtlDaemonand passes the bare host (sb.domain(8080)→<sub>.vercel.run) asAGENTBOX_BOX_HOST(deriveCloudBoxHost: loopback preview →<name>.localhost; public preview →URL.host). Exported into the daemon env (so first-bootagentbox-ctl rendertasks see it) and written to/etc/agentbox/box.envfor login-shell parity. The placeholder engine already prefers an explicit value over the derive, sohttps://{{AGENTBOX_BOX_HOST}}now matchesagentbox url. Generic across the public-URL clouds (vercel/daytona/e2b); docker/hetzner keep<name>.localhost. Unit-tested (deriveCloudBoxHost,launchCloudCtlDaemonscript). Seedocs/cloud-providers.md§1.0.1. - Boxes from a checkpoint were frozen on the source box's branch. Done —
cloud create restored a snapshot and skipped workspace seeding entirely, so
every checkpoint-derived box reused the snapshot's
/workspace/.gitverbatim: sameagentbox/<source-box>branch, none of the new box's uncommitted/untracked host state. NowseedCloudWorkspacegains an overlay mode used on the snapshot path (overlay: Boolean(snapshotName)): it keeps the snapshot's gitignored warm tree (node_modules, build caches), moves the box onto a freshagentbox/<new-box>branch at the host base ref (git checkout -f -B+git reset --hard, dropping the checkpoint's own commits — matching docker), and replays the host stash + untracked carry-over. Transport is incremental: ships only the commits the checkpoint lacks (checkpointTip..hostTarget) as a git bundle fetched into the existing.git, with a full-clone.git-swap fallback when the box diverged / the tip is unknown. Carry-over conflicts are box-wins + reported (host change skipped, never left unmerged) viaCreatedBox.resync→ the existingbuildResyncWarninginjects the same "conflicting host changes SKIPPED …agentbox-ctl reload" prompt into claude/codex/opencode that docker does. The cloud analogue of docker'sregenerateRestoredWorktrees+resyncWorkspaceFromHost. Generic across the cloud providers (the sharedcloud-provider.tscreate path); non-git workspaces keep the snapshot tree as-is. Verified live on Vercel: a box from a checkpoint landed on a freshagentbox/<box>branch with the host's new commit (delta shipped it), carried a host untracked file created after the checkpoint, and kept the snapshot's warm root marker. Seedocs/cloud-create-flow.md+docs/create-and-checkpoints.md.