Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AgentBox — context for Claude Code

`agentbox` is an npm CLI that spins up isolated sandboxes ("boxes") for coding agents (Claude Code, Codex, others) to work in, so they can't touch the host. Seven backends share one provider abstraction: **Docker** (the default — one local container per box, isolated by per-box git branch in an in-container worktree against the bind-mounted host `.git/`), **Daytona Cloud** (`--provider daytona` — a managed remote sandbox seeded from a host git bundle + per-agent credential volumes, reached via SSH-token attach and an in-sandbox bridge relay), **Hetzner Cloud** (`--provider hetzner` — a bare VPS per box, pure OpenSSH ControlMaster comms, locked-down Hetzner Cloud Firewall, baked from a one-time `agentbox prepare --provider hetzner` snapshot), **Vercel Sandbox** (`--provider vercel` — a Firecracker microVM per box, persistent snapshots, public HTTPS preview URLs; nested containers (in-box docker) now supported and baked in, no SSH, baked from a one-time `agentbox prepare --provider vercel` snapshot), **E2B Sandbox** (`--provider e2b` — a Firecracker microVM per box, SDK-only comms, public HTTPS preview URLs, persistent pause/resume; uniquely among the cloud providers, E2B builds its base template **directly from a Dockerfile** via `Template.build()` — `agentbox prepare --provider e2b` runs the build), and **Remote Docker** (`--provider remote-docker`, spelled `agentbox docker:<host> …` — one container per box on a machine the *user* already owns, reached over an OpenSSH ControlMaster and driving **that machine's** docker engine. Cloud-shaped despite being docker: a bind mount can't cross a network, so the workspace is synced (git clone + carried-over stash/untracked) exactly as for the clouds, while the image (Dockerfile), checkpoints (`docker commit`) and DinD stay docker-shaped. No credential — it connects as you, over your own `~/.ssh/config`).
`agentbox` is an npm CLI that spins up isolated sandboxes ("boxes") for coding agents (Claude Code, Codex, others) to work in, so they can't touch the host. Eight backends share one provider abstraction: **Docker** (the default — one local container per box, isolated by per-box git branch in an in-container worktree against the bind-mounted host `.git/`), **Daytona Cloud** (`--provider daytona` — a managed remote sandbox seeded from a host git bundle + per-agent credential volumes, reached via SSH-token attach and an in-sandbox bridge relay), **Hetzner Cloud** (`--provider hetzner` — a bare VPS per box, pure OpenSSH ControlMaster comms, locked-down Hetzner Cloud Firewall, baked from a one-time `agentbox prepare --provider hetzner` snapshot), **Vercel Sandbox** (`--provider vercel` — a Firecracker microVM per box, persistent snapshots, public HTTPS preview URLs; nested containers (in-box docker) now supported and baked in, no SSH, baked from a one-time `agentbox prepare --provider vercel` snapshot), **E2B Sandbox** (`--provider e2b` — a Firecracker microVM per box, SDK-only comms, public HTTPS preview URLs, persistent pause/resume; uniquely among the cloud providers, E2B builds its base template **directly from a Dockerfile** via `Template.build()` — `agentbox prepare --provider e2b` runs the build), **Remote Docker** (`--provider remote-docker`, spelled `agentbox docker:<host> …` — one container per box on a machine the *user* already owns, reached over an OpenSSH ControlMaster and driving **that machine's** docker engine. Cloud-shaped despite being docker: a bind mount can't cross a network, so the workspace is synced (git clone + carried-over stash/untracked) exactly as for the clouds, while the image (Dockerfile), checkpoints (`docker commit`) and DinD stay docker-shaped. No credential — it connects as you, over your own `~/.ssh/config`), and **Tenki Sandbox** (`--provider tenki` — a Firecracker microVM per box driven over the `@tenkicloud/sandbox` TypeScript SDK: a ConnectRPC control plane + a per-session data plane for `exec`/file transfer and `session.ssh()` for attach; boots from a registry image published into the Tenki workspace by `agentbox prepare --provider tenki`; free pause/resume + id-addressed snapshots).

## Architecture overview

Expand All @@ -10,7 +10,8 @@
- **hetzner** (cloud): one Hetzner VPS per box (default `cx23` / `nbg1`). Workspace seeded the same way (git bundle + stash + untracked tar). Per-box ed25519 SSH key minted on the host into `~/.agentbox/boxes/<sandboxId>/ssh/` and injected via cloud-init. Per-box Hetzner Cloud Firewall auto-locked to the host's egress IP (multi-probe fail-loud). All comms (exec, scp, port forwards, attach) flow over one persistent `ssh -fNT -M` ControlMaster per box; `previewUrl(port)` mints `ssh -O forward` on demand. No agent credentials volume — credentials pushed via scp at create time (Hetzner has no shared-volume primitive). `agentbox prepare --provider hetzner` bakes a one-time base snapshot since Hetzner can't build images from a Dockerfile.
- **vercel** (cloud): one Vercel Sandbox (Firecracker microVM, Amazon Linux 2023) per box. Workspace seeded the same way (git bundle + stash + untracked tar). Boots from a Vercel snapshot baked once by `agentbox prepare --provider vercel` (no Dockerfile build). Persistent sandboxes auto-snapshot on stop and auto-resume on `Sandbox.get({ resume: true })` → pause/resume for free. Comms via the SDK: `exec` runs as `vscode` (root → `sudo -u vscode`); `previewUrl(port)` returns the public `sandbox.domain(port)` (HTTPS, no token), so the host relay's `CloudBoxPoller` reaches the in-box bridge directly. **In-box docker (DinD)** is baked into the base snapshot and `dockerd` is auto-started on create/resume (`launchDockerd:true`, via the shared `agentbox-dockerd-start`) — Vercel Sandbox now supports nested containers. **No SSH** (attach is a custom `attach-helper.js` tmux bridge over the SDK). Max 4 exposed ports, region `iad1` only. See [`docs/vercel-backlog.md`](./docs/vercel-backlog.md).
- **e2b** (cloud): one E2B Sandbox (Firecracker microVM, Debian 12) per box. Workspace seeded the same way (git bundle + stash + untracked tar). **Key differentiator from Vercel/Hetzner: E2B builds its base image directly from a Dockerfile** via the SDK's `Template.build()` — `agentbox prepare --provider e2b` drives the build and pins the resulting template id to `box.imageE2b`. `Sandbox.pause`/`Sandbox.connect` (auto-resume) gives free pause/resume; `Sandbox.createSnapshot` is the reusable, id-addressed checkpoint primitive (same shape as Vercel). Comms via the SDK: `exec` runs as `vscode`; `previewUrl(port)` returns the public `{port}-{sandboxId}.e2b.app` URL (HTTPS, no token; constructed locally so it doesn't wake a paused sandbox). **In-box docker (DinD)** is baked into the base template and `dockerd` auto-starts on create/resume (`launchDockerd:true`) — E2B microVMs support nested containers (full root + cap_sys_admin, verified 2026-06-23), contrary to the original "same as Vercel" assumption. **No SSH** — attach is a custom `attach-helper.cjs` SDK-streaming PTY bridge over `pty.create`. 1-hour platform session cap on the Hobby tier (the attach helper caps at 55 minutes for headroom). See [`docs/e2b_backlog.md`](./docs/e2b_backlog.md).
- **In-box supervisor** (`@agentbox/ctl`) — reads `/workspace/agentbox.yaml` and runs the declared tasks/services under a DAG scheduler. Ships as `agentbox-ctl` inside every box (docker, daytona, hetzner, vercel, e2b).
- **tenki** (cloud): one Tenki sandbox (Firecracker microVM) per box. Workspace seeded the same way (git clone + stash + untracked tar). Boots from a **registry image** published into the Tenki workspace by `agentbox prepare --provider tenki` (built from the GHCR `agentbox/box` parent via Tenki's template build), or a checkpoint snapshot id. Comms via the `@tenkicloud/sandbox` TS SDK (ConnectRPC control plane + per-session data plane): `exec` = `session.run(['bash','-c',cmd])`, file transfer = `writeFileStream`/`readFileStream`, preview URLs = `session.exposePort(port)` (public HTTPS, WebProxy on 8080). Free pause/resume (`session.pause`/`resume`); `session.extend` renews the deadline (host keepalive); `createSnapshotAndWait` is the id-addressed checkpoint primitive (same shape as vercel/e2b). **No SSH for exec/files**; interactive attach bridges the host PTY to `session.ssh()` (custom `attach-helper.cjs`). `CloudHandle.sandboxId` is the Tenki session id. In-box docker is **off** (`launchDockerd:false`) pending DinD verification. Newly added — see [`docs/tenki-backlog.md`](./docs/tenki-backlog.md) for the live-verify checklist.
- **In-box supervisor** (`@agentbox/ctl`) — reads `/workspace/agentbox.yaml` and runs the declared tasks/services under a DAG scheduler. Ships as `agentbox-ctl` inside every box (docker, daytona, hetzner, vercel, e2b, tenki).
- **Host relay** (`@agentbox/relay`) — a host node process boxes call for things they have no credentials for (`git push`, checkpoint capture, `cp`/`download`) and to push status events. Keeps SSH keys out of the box. The cloud path drives the same relay via `CloudBoxPoller` + `executeCloudAction`.
- **Checkpoints** — `docker commit` (+ periodic `FROM scratch` flatten) for docker; Daytona snapshots (`sb._experimental_createSnapshot`) for daytona; Hetzner `create_image` snapshots (no-pause default — matches `docker commit`) for hetzner; Vercel `sb.snapshot()` (id-addressed; stores the snapshot id in the cloud-checkpoint manifest) for vercel; E2B `Sandbox.createSnapshot` (id-addressed template, same shape as Vercel) for e2b. All flow through `provider.checkpoint.create`. `box.defaultCheckpoint` is the cross-provider fallback; `box.defaultCheckpointDocker` / `box.defaultCheckpointDaytona` / `box.defaultCheckpointHetzner` / `box.defaultCheckpointVercel` / `box.defaultCheckpointE2b` override per provider.
- The full design — file-handling rationale, the checkpoint model, pause/resume strategy, what we explicitly rejected — lives in [`docs/architecture.md`](./docs/architecture.md) and [`docs/create-and-checkpoints.md`](./docs/create-and-checkpoints.md). Cloud-specific status lives in [`docs/daytona-backlog.md`](./docs/daytona-backlog.md), [`docs/hertzner_backlog.md`](./docs/hertzner_backlog.md), [`docs/vercel-backlog.md`](./docs/vercel-backlog.md), and [`docs/e2b_backlog.md`](./docs/e2b_backlog.md). **Read them before making non-trivial changes to the lifecycle code.**
Expand Down Expand Up @@ -127,6 +128,7 @@ Each topic has a dedicated file under [`docs/`](./docs). Read the relevant one b
- [`docs/hertzner_backlog.md`](./docs/hertzner_backlog.md) — Hetzner provider build-out status: phase-by-phase progress, the live e2e smoke results, deferred follow-ups (per-project snapshot tier, `--pause` checkpoint flag, `agentbox prune --provider hetzner`, the install-script post-Chromium trace mystery). Filename uses the user-requested spelling.
- [`docs/vercel-backlog.md`](./docs/vercel-backlog.md) — Vercel provider build-out status: why Vercel's shape differs (no Dockerfile, no containers, no SSH, persistent snapshots), phase-by-phase progress, and the live-verify checklist (user mapping, attach latency / ttyd upgrade, snapshot-vs-delete cascade, VNC on AL2023, published-CLI asset staging).
- [`docs/e2b_backlog.md`](./docs/e2b_backlog.md) — E2B provider build-out status: how the shape maps onto `CloudBackend`, why E2B is the only cloud that builds the base **from a Dockerfile** (`Template.build()`), task-by-task progress, and shipped/deferred items.
- [`docs/tenki-backlog.md`](./docs/tenki-backlog.md) — Tenki provider build-out status: how the `@tenkicloud/sandbox` ConnectRPC SDK maps onto `CloudBackend`, what's implemented + unit-tested, and the **live-verify checklist** (the e2e path is unverified against a live Tenki workspace — no token was available at build time).
- [`docs/linux-host-backlog.md`](./docs/linux-host-backlog.md) — Linux (Ubuntu) **host** support: what's done (`agentbox doctor` is Linux-aware), how to test on a persistent Hetzner Ubuntu VM (`scripts/linux-dev-vm.sh` — `up`/`deploy`/`ssh`/`doctor`/`down`), and the remaining macOS-only host assumptions (browser `open`→`xdg-open`, iTerm2/AppleScript terminal spawning, OrbStack-only fast paths).
- [`docs/control-plane-roadmap.md`](./docs/control-plane-roadmap.md) — the **Control Hub** architecture + roadmap (the forward direction; "Control Hub" / `agentbox hub` is the new name for the control-plane, renamed in milestone M1). Covers the three shifts (in-box create/bake + poll to unify the local + serverless paths; hub-anywhere/PC-first with a SQLite local hub; custody + 3-way sync), the four deployment topologies (local host, mac-mini, server/container, serverless Vercel→Cloudflare), the two capability profiles (serverless "control plane" vs full-host "control-box"), the source-of-truth constraint, the CLI rename + `hub install/update/uninstall`, and milestones M0–M8. Pairs with [`docs/control-plane-backlog.md`](./docs/control-plane-backlog.md) (what shipped).
- [`docs/control-plane-guide.md`](./docs/control-plane-guide.md) — the **feature guide** for the hosted control plane (a.k.a. the legacy "control-box"): the one-relay-core/three-topologies model, how it works (Store seam, GitHub-App token leasing, block-vs-poll approvals, dual-mode server + bridge + CloudBoxPoller, the create-job worker, the in-box clone/relay-env/lease-and-push), and how to use it (`agentbox control-plane setup|set-url|status|add|worker` with examples). Read this before the roadmap/backlog for the high-level "what + how to use".
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ Uses `portless` to give box web apps the same URL from inside the box and on the

## Cloud Providers

| | local docker | remote docker | hetzner | daytona | vercel | e2b |
| ------------------- | ------------------------- | ---------------------- | ---------------------- | ------------------ | ------------------ | ------------------ |
| Support | ✅ | ✅ | ✅ | ⚠️ Partial | ✅ | ✅ |
| Base image | Dockerfile | Dockerfile (on the remote) | Setup script (Ubuntu) | Dockerfile | Setup script | Dockerfile (`Template.build`) |
| Live snapshots | ✅ | ✅ (`docker commit`) | ✅ | 🧪 Experimental | ✅ | ✅ |
| Private preview URLs| ✅ (portless or OrbStack) | ✅ (portless over SSH) | ✅ (portless) | ✅ (native) | ✅ (native) | ✅ (native) |
| | local docker | remote docker | hetzner | daytona | vercel | e2b | tenki |
| ------------------- | ------------------------- | ---------------------- | ---------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
| Support | ✅ | ✅ | ✅ | ⚠️ Partial | ✅ | ✅ | 🧪 Experimental |
| Base image | Dockerfile | Dockerfile (on the remote) | Setup script (Ubuntu) | Dockerfile | Setup script | Dockerfile (`Template.build`) | Registry image |
| Live snapshots | ✅ | ✅ (`docker commit`) | ✅ | 🧪 Experimental | ✅ | ✅ | ✅ |
| Private preview URLs| ✅ (portless or OrbStack) | ✅ (portless over SSH) | ✅ (portless) | ✅ (native) | ✅ (native) | ✅ (native) | ✅ (native) |

**Cloud setup** (optional — skip for local Docker)

Expand All @@ -95,9 +95,10 @@ Uses `portless` to give box web apps the same URL from inside the box and on the
- `agentbox hetzner login` — interactive Hetzner Cloud token setup, saved to `~/.agentbox/secrets.env`
- `agentbox daytona login` — interactive Daytona API key setup, saved to `~/.agentbox/secrets.env`
- `agentbox e2b login` — interactive E2B API key setup, saved to `~/.agentbox/secrets.env`
- `agentbox tenki login` — interactive Tenki auth token setup, saved to `~/.agentbox/secrets.env`
- `agentbox digitalocean login` — interactive DigitalOcean Personal Access Token setup, saved to `~/.agentbox/secrets.env`
- `agentbox remote-docker doctor <host>` — run boxes on a machine you already own, over SSH. No login and no token: it connects as you, using your own `~/.ssh/config`. Then `agentbox docker:<host> claude`.
- `agentbox prepare [--provider daytona|hetzner|vercel|e2b|digitalocean|docker:<host>]` — build the image and initial snapshot (e2b builds from a Dockerfile via `Template.build()`)
- `agentbox prepare [--provider daytona|hetzner|vercel|e2b|digitalocean|tenki|docker:<host>]` — build the image and initial snapshot (e2b builds from a Dockerfile via `Template.build()`; tenki publishes a registry image)
- `agentbox hetzner claude`, `agentbox hetzner codex`, `agentbox hetzner create`, etc.

## How to use
Expand Down Expand Up @@ -156,7 +157,7 @@ Full documentation lives at **[agent-box.sh/docs](https://agent-box.sh/docs)**:
- [Quickstart](https://agent-box.sh/docs) and [Core concepts](https://agent-box.sh/docs/core-concepts)
- [Teleport a project](https://agent-box.sh/docs/teleport-a-project), [Run an agent](https://agent-box.sh/docs/run-an-agent), [Access your box](https://agent-box.sh/docs/access-your-box)
- [Configuration](https://agent-box.sh/docs/configuration), [Services & tasks](https://agent-box.sh/docs/services-and-tasks), [Sync & git](https://agent-box.sh/docs/sync-and-git)
- Cloud providers: [Hetzner](https://agent-box.sh/docs/hetzner), [Daytona](https://agent-box.sh/docs/daytona), [Vercel](https://agent-box.sh/docs/vercel), [E2B](https://agent-box.sh/docs/e2b), [DigitalOcean](https://agent-box.sh/docs/digitalocean)
- Cloud providers: [Hetzner](https://agent-box.sh/docs/hetzner), [Daytona](https://agent-box.sh/docs/daytona), [Vercel](https://agent-box.sh/docs/vercel), [E2B](https://agent-box.sh/docs/e2b), [DigitalOcean](https://agent-box.sh/docs/digitalocean), [Tenki](https://agent-box.sh/docs/tenki)
- Full [CLI reference](https://agent-box.sh/docs/cli)

## Development
Expand Down
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@agentbox/sandbox-docker": "workspace:*",
"@agentbox/sandbox-e2b": "workspace:*",
"@agentbox/sandbox-hetzner": "workspace:*",
"@agentbox/sandbox-tenki": "workspace:*",
"@agentbox/sandbox-vercel": "workspace:*",
"@types/node": "^22.10.1",
"tsup": "^8.3.5",
Expand Down
14 changes: 13 additions & 1 deletion apps/cli/scripts/stage-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,18 @@ for (const [srcRel, destRel, exec] of e2bFiles) {
copy(srcRel, join(e2bCtx, destRel), exec);
}

// Tenki provider — the only on-disk asset is the PTY attach helper bundle the
// provider's `buildAttach` spawns at runtime (`node attach-helper.cjs`). Boxes
// boot from a prepared Tenki registry image, so unlike docker/e2b there is no
// build context or shim tree to stage. The tenki package is bundled into
// dist/index.js (its dist/ isn't shipped), so stage the helper explicitly to
// runtime/tenki/attach-helper.cjs where resolveAttachHelperPath() looks.
const tenkiCtx = join(runtime, 'tenki');
const tenkiFiles = [['packages/sandbox-tenki/dist/attach-helper.cjs', 'attach-helper.cjs', false]];
for (const [srcRel, destRel, exec] of tenkiFiles) {
copy(srcRel, join(tenkiCtx, destRel), exec);
}

// README — npm reads the published package's README only from the package
// root (apps/cli/), and there's no package.json field to point elsewhere.
// Mirror the repo-root README here so npmjs.com has a landing page, rewriting
Expand Down Expand Up @@ -284,6 +296,6 @@ if (missing > 0) {
);
} else {
console.log(
'[stage-runtime] staged runtime/ (relay bin + docker build context + hetzner install assets + digitalocean install assets + daytona overlay + vercel assets + e2b assets)',
'[stage-runtime] staged runtime/ (relay bin + docker build context + hetzner install assets + digitalocean install assets + daytona overlay + vercel assets + e2b assets + tenki attach helper)',
);
}
Loading
Loading