@madarco/agentbox 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_cloud-attach-T727ZPRV.js +13 -0
- package/dist/chunk-67N47KUS.js +1640 -0
- package/dist/chunk-67N47KUS.js.map +1 -0
- package/dist/chunk-6OZDFNBF.js +8114 -0
- package/dist/chunk-6OZDFNBF.js.map +1 -0
- package/dist/chunk-BGK32PZE.js +455 -0
- package/dist/chunk-BGK32PZE.js.map +1 -0
- package/dist/chunk-FODMEHD3.js +1200 -0
- package/dist/chunk-FODMEHD3.js.map +1 -0
- package/dist/chunk-G3H2L3O2.js +288 -0
- package/dist/chunk-G3H2L3O2.js.map +1 -0
- package/dist/chunk-I24B6AXR.js +600 -0
- package/dist/chunk-I24B6AXR.js.map +1 -0
- package/dist/chunk-LEV3KICD.js +738 -0
- package/dist/chunk-LEV3KICD.js.map +1 -0
- package/dist/cloud-poller-SUNA6ZQC-2RG5WPRN.js +10 -0
- package/dist/dist-L4LCG5SJ.js +293 -0
- package/dist/dist-L4LCG5SJ.js.map +1 -0
- package/dist/dist-LOZBWMBF.js +447 -0
- package/dist/dist-ZODPD2I6.js +1407 -0
- package/dist/dist-ZODPD2I6.js.map +1 -0
- package/dist/index.js +7281 -2134
- package/dist/index.js.map +1 -1
- package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js +18 -0
- package/package.json +8 -3
- package/runtime/daytona/custom-system-CLAUDE.md +39 -0
- package/runtime/docker/Dockerfile.box +120 -14
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +15 -8
- package/runtime/docker/packages/ctl/dist/bin.cjs +11310 -816
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +68 -0
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +9 -9
- package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
- package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
- package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
- package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
- package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
- package/runtime/hetzner/agentbox-codex-hooks.json +68 -0
- package/runtime/hetzner/agentbox-dockerd-start +132 -0
- package/runtime/hetzner/agentbox-open +28 -0
- package/runtime/hetzner/agentbox-setup-skill.md +196 -0
- package/runtime/hetzner/agentbox-vnc-start +77 -0
- package/runtime/hetzner/claude-managed-settings.json +115 -0
- package/runtime/hetzner/ctl.cjs +23397 -0
- package/runtime/hetzner/custom-system-CLAUDE.md +39 -0
- package/runtime/hetzner/gh-shim +263 -0
- package/runtime/hetzner/git-shim +131 -0
- package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
- package/runtime/hetzner/scripts/install-box.sh +374 -0
- package/runtime/relay/bin.cjs +10017 -817
- package/share/agentbox-setup/SKILL.md +15 -8
- package/share/host-skills/agentbox/SKILL.md +29 -0
- package/share/host-skills/agentbox-info/SKILL.md +211 -0
- package/share/host-skills/codex/agentbox.md +35 -0
- package/share/host-skills/opencode/agentbox.md +26 -0
- package/dist/chunk-BBZMA2K6.js +0 -238
- package/dist/chunk-BBZMA2K6.js.map +0 -1
- package/dist/chunk-HHMWQNLF.js +0 -1709
- package/dist/chunk-HHMWQNLF.js.map +0 -1
- package/dist/chunk-HPZMD5DE.js +0 -106
- package/dist/chunk-HPZMD5DE.js.map +0 -1
- package/dist/chunk-HTTKML3C.js +0 -2655
- package/dist/chunk-HTTKML3C.js.map +0 -1
- package/dist/chunk-KJNZP6I3.js +0 -586
- package/dist/chunk-KJNZP6I3.js.map +0 -1
- package/dist/chunk-M7I247BK.js +0 -525
- package/dist/chunk-M7I247BK.js.map +0 -1
- package/dist/create-6PWXI6HO-OWAMHBAK.js +0 -15
- package/dist/lifecycle-EMXR46DI-DUVBXNTV.js +0 -38
- package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
- package/dist/stats-SZXOJE3D-N7OODCHW.js +0 -19
- /package/dist/{create-6PWXI6HO-OWAMHBAK.js.map → _cloud-attach-T727ZPRV.js.map} +0 -0
- /package/dist/{lifecycle-EMXR46DI-DUVBXNTV.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
- /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-LOZBWMBF.js.map} +0 -0
- /package/dist/{stats-SZXOJE3D-N7OODCHW.js.map → prepared-state-CL4CWXQA-ME4HSKDE.js.map} +0 -0
|
@@ -7,16 +7,22 @@ description: Generate an agentbox.yaml for the current AgentBox workspace. Invok
|
|
|
7
7
|
|
|
8
8
|
## Box layout (what you're configuring against)
|
|
9
9
|
|
|
10
|
-
Your user i `vscode` and you can use
|
|
10
|
+
Your user i `vscode` and you can use `sudo` to run commands as root.
|
|
11
11
|
|
|
12
|
-
`/workspace` is the
|
|
12
|
+
`/workspace` is where the user code lives, a per-box git worktree on a fresh `agentbox/<box-name>` branch (or a tar-piped copy of the host workspace for non-git projects).
|
|
13
|
+
Run `agentbox checkpoint --set-default` (similar to `docker commit`) to save any changes make to the system and workspace so that new boxes will start from a warm state. Everything is wiped on `agentbox destroy`.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Some special folders:
|
|
15
16
|
|
|
16
|
-
- **Host main repo's `.git/`** — bind-mounted RW at its identical absolute host path. In-box commits land on the host's branch refs (visible to `git log` on the host immediately); the box itself carries no SSH/git creds, so `git push` goes through the host relay (`agentbox-ctl git push`). The host's **working tree is never written to** — only refs/objects under `.git/`.
|
|
17
|
-
- **`~/.claude`** —
|
|
17
|
+
- **Host main repo's `.git/`** — If the box bind-mounted RW at its identical absolute host path. In-box commits land on the host's branch refs (visible to `git log` on the host immediately); the box itself carries no SSH/git creds, so `git push` goes through the host relay (`agentbox-ctl git push`). The host's **working tree is never written to** — only refs/objects under `.git/`. GitHub PR ops (`agentbox-ctl git pr create|view|list|comment|review|merge|close|reopen|checkout`) flow the same way through host `gh`; write ops require host confirmation (deny → exit 10), `merge` and `checkout` have additional opt-in guards.
|
|
18
|
+
- **`~/.claude`** — and similar home folders for coding agents are seeded from the host's `~/.claude` on each create so auth, skills, and plugins persist without leaking the host's home dir.
|
|
18
19
|
- **`agentbox.yaml`** — read by `agentbox-ctl` from `/workspace`. Tasks and services declared here are what the supervisor will run.
|
|
19
20
|
|
|
21
|
+
Exposed ports and services:
|
|
22
|
+
- **portless** - every port with `expose:` setting in agentbox.yaml, will be exposed not only as a local port but also as a special domain name `https://<name>.localhost` (so on https) using `portless` cli and proxy. This will be also mapped to the host where also `portless` proxy is running so users can access the same service on the same looking url.
|
|
23
|
+
- **vnc** - the webVNC server exposed on 6080 will be proxies to the host on a random port.
|
|
24
|
+
- **vscode** - the vscode server is proxied to the host on a random port.
|
|
25
|
+
|
|
20
26
|
## Goal
|
|
21
27
|
|
|
22
28
|
Produce a `/workspace/agentbox.yaml` that captures this project's services, tasks, and box defaults so the in-box supervisor (`agentbox-ctl`) can boot the workspace deterministically.
|
|
@@ -64,7 +70,7 @@ The box's primary web app (the dev server / Next.js / API the user opens in a br
|
|
|
64
70
|
as: 80 # must be 80 — the container port AgentBox publishes
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
At most **one** service may set `expose:`. AgentBox forwards container `:80` to `127.0.0.1:<port>` and publishes it on the host, so `agentbox list`/`status` show it as the box's main URL on every engine (no OrbStack dependency). Set this on the same service whose `ready_when:` you just wrote (a DB or worker should **not** get `expose:`).
|
|
73
|
+
At most **one** service may set `expose:`. AgentBox forwards container `:80` to `127.0.0.1:<port>` and publishes it on the host with `portless` proxy to a <boxname>.localhost url, so `agentbox list`/`status` show it as the box's main URL on every engine (no OrbStack dependency). Set this on the same service whose `ready_when:` you just wrote (a DB or worker should **not** get `expose:`).
|
|
68
74
|
|
|
69
75
|
## 4. Restart + backoff
|
|
70
76
|
|
|
@@ -179,11 +185,12 @@ Tell the user (verbatim):
|
|
|
179
185
|
```
|
|
180
186
|
|
|
181
187
|
your box is ready, you can start more sessions with `agentbox claude`
|
|
188
|
+
you can access the web app at https://<boxname>.localhost
|
|
182
189
|
|
|
183
190
|
## 10. Known issues
|
|
184
191
|
|
|
185
192
|
- For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
|
|
186
193
|
|
|
187
|
-
-
|
|
194
|
+
- Service like flask, nextjs, BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL should use the <boxname>.localhost url for the local development so that on the host it will use the same url as the box.
|
|
188
195
|
|
|
189
|
-
-
|
|
196
|
+
- The `install` task is intentionally a no-op once `node_modules/.agentbox-installed` exists. Do **not** remove the marker guard to "force a fresh install" — that reinstalls on every box start. To force a one-off rebuild, delete `node_modules` (or just the marker) then run `agentbox-ctl reload`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentbox
|
|
3
|
+
description: "Fork the current agent session into a new VM or local Docker container with all the project files, agent settings and session teleported into."
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
context: fork
|
|
6
|
+
agent: general-purpose
|
|
7
|
+
allowed-tools: Bash
|
|
8
|
+
---
|
|
9
|
+
<!-- agentbox-managed:v1 -->
|
|
10
|
+
|
|
11
|
+
Fork the current Claude Code session into a fresh AgentBox box.
|
|
12
|
+
|
|
13
|
+
1. **Resolve the provider flag from `$ARGUMENTS`:**
|
|
14
|
+
- empty → no flag (uses the default docker provider)
|
|
15
|
+
- `docker` | `daytona` | `hetzner` → pass `--provider $ARGUMENTS`
|
|
16
|
+
- anything else → stop and tell the user the valid values are `docker`, `daytona`, `hetzner`
|
|
17
|
+
|
|
18
|
+
2. **Fork.** Run, via the Bash tool, exactly one command:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
agentbox fork --session ${CLAUDE_SESSION_ID} [--provider $ARGUMENTS]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. **Report.** In one line, give the user the new box name (parse it from the command output) and confirm their host session is unaffected. Do not summarize the conversation — the fork already carries it.
|
|
25
|
+
|
|
26
|
+
## Troubleshooting
|
|
27
|
+
|
|
28
|
+
- If agentbox command fails, tell the user to install AgentBox by writing `! npm -g install @madarco/agentbox` in the chat.
|
|
29
|
+
- If `AGENTBOX_RELAY_URL` is set in the environment, you are running *inside* a box. This command is host-only in v1; tell the user box→box fork is not supported yet.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentbox-info
|
|
3
|
+
description: "Spin up isolated sandboxes (\"boxes\") for coding agents, run them in parallel, queue background runs with -i, and push commits safely through the host relay. Use when the user wants to run Claude Code / Codex / OpenCode in a sandbox, start more boxes, attach to a running box, or otherwise operate the `agentbox` CLI on their laptop."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- agentbox-managed:v1 -->
|
|
7
|
+
|
|
8
|
+
# AgentBox (host-side)
|
|
9
|
+
|
|
10
|
+
You are operating on the **user's host machine** (laptop / dev workstation), not inside a box. Use the `agentbox` CLI to provision isolated sandboxes for coding agents and to attach to them.
|
|
11
|
+
|
|
12
|
+
If you find yourself *inside* a box (`/workspace` exists and `AGENTBOX_RELAY_URL` is set in the env), this is the wrong skill — use the in-box `/agentbox-setup` skill instead.
|
|
13
|
+
|
|
14
|
+
## What AgentBox is, in one paragraph
|
|
15
|
+
|
|
16
|
+
AgentBox spins up one isolated sandbox per agent run — a local Docker container (default), a Daytona cloud sandbox (`--provider daytona`), or a Hetzner VPS (`--provider hetzner`). Each box has its own `/workspace`, but the host's `.git/` is shared, so commits made inside the box land on the host immediately. The agent inside the box has **no host credentials** — `git push`, opening URLs in the host browser, capturing checkpoints, and all other host-side operations flow through a small host process called the **relay** that runs alongside the CLI.
|
|
17
|
+
|
|
18
|
+
## The two starting commands
|
|
19
|
+
|
|
20
|
+
### `agentbox create`
|
|
21
|
+
|
|
22
|
+
Provision a box and stop. The box exists and is ready, but nothing is launched inside it.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
agentbox create # docker, auto-named after the workspace
|
|
26
|
+
agentbox create -n review # docker, friendly name
|
|
27
|
+
agentbox create --provider hetzner # cloud VPS (requires `agentbox prepare --provider hetzner` once)
|
|
28
|
+
agentbox create --attach # drop into a shell inside the box after create
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Useful flags: `-n <name>` (friendly box name), `--provider docker|daytona|hetzner`, `--attach`, `-w <path>` (workspace to mount; defaults to `cwd`), `--snapshot <ref>` (start from a checkpoint).
|
|
32
|
+
|
|
33
|
+
Non-docker providers require a one-time `agentbox prepare --provider <name>` to bake the base image / snapshot.
|
|
34
|
+
|
|
35
|
+
### `agentbox claude`
|
|
36
|
+
|
|
37
|
+
Provision (same as `create`) and launch **Claude Code** inside the box, in a detachable tmux session. This is the main entry point most users want.
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
agentbox claude # docker, attaches your terminal
|
|
41
|
+
agentbox claude -n review # second box, named
|
|
42
|
+
agentbox claude --provider hetzner # cloud
|
|
43
|
+
agentbox claude -- --model sonnet # extra args after `--` go to claude itself
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
While attached: **`Ctrl+a d`** detaches without killing claude. The box keeps running. Reattach with `agentbox claude attach <name|n>`.
|
|
47
|
+
|
|
48
|
+
Variants with the same shape for other agents: **`agentbox codex`**, **`agentbox opencode`**.
|
|
49
|
+
|
|
50
|
+
## `-i` / `--initial-prompt`: background queue
|
|
51
|
+
|
|
52
|
+
With `-i "<prompt>"`, `agentbox claude` (and `codex` / `opencode`) does **not** attach. It writes a job manifest to `~/.agentbox/queue/<id>.json` and exits immediately, printing the job id and log path. The host relay's queue loop drains these manifests respecting `queue.maxConcurrent` (global config; override per invocation with `--max-running <n>`).
|
|
53
|
+
|
|
54
|
+
Use this to fan out parallel agent runs:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
agentbox claude -i "fix the failing test in src/auth and open a PR"
|
|
58
|
+
agentbox claude -i "draft a CHANGELOG entry from the last 20 commits"
|
|
59
|
+
agentbox claude -i "audit our dependencies for known CVEs"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each call returns instantly. The queue drains them concurrently up to `queue.maxConcurrent`. Inspect / attach later:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
agentbox dashboard # TUI with status + leader-key actions
|
|
66
|
+
agentbox claude attach <name|n> # reattach to a specific box
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Caveats: `-i` is currently **docker-only** (cloud sessions only start on attach, so background-mode has no place to seed the prompt). The host must have valid Claude Code credentials.
|
|
70
|
+
|
|
71
|
+
## Forking the current session into a box
|
|
72
|
+
|
|
73
|
+
From host Claude, run the **`/agentbox`** slash command (optional arg: `docker` | `daytona` | `hetzner`) to snapshot the *current* Claude Code session into a brand-new box that resumes it. With tmux or iTerm it opens in a new terminal tab; otherwise it starts in the background. The host session is unaffected — you get two parallel timelines. The underlying CLI is `agentbox fork` (`agentbox fork --help`); `/agentbox` requires `agentbox install` to have been run once. This is distinct from `-i`, which seeds a *new* prompt rather than resuming the live conversation.
|
|
74
|
+
|
|
75
|
+
## Driving one agent from another (`drive`, `agent`, `queue wait-for`)
|
|
76
|
+
|
|
77
|
+
When *you* are the host-side agent and want to orchestrate other agents running inside boxes — read what they're doing, send them a prompt, wait until they're done or need input — use these three command families. Everything is stateless / one-shot, and the human-text default switches to machine-friendly JSON with `--json`.
|
|
78
|
+
|
|
79
|
+
### `agentbox drive <box>` — terminal driving
|
|
80
|
+
|
|
81
|
+
Targets the running tmux session inside a box (auto-picks the agent session: `claude` → `codex` → `opencode` → the only running session; override with `--session <name>`). Provider-uniform — works the same on docker / daytona / hetzner.
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
agentbox drive snapshot 1 # print rendered TUI as plain text
|
|
85
|
+
agentbox drive snapshot 1 --with-cursor # JSON envelope: { session, cols, rows, cursor, screen }
|
|
86
|
+
agentbox drive snapshot 1 --ansi --rows -200:-1 # include color, walk into scrollback
|
|
87
|
+
agentbox drive keypress 1 "<C-c>" # DSL: <Enter>, <C-x>, <Tab>, <F5>, <Up>, etc.
|
|
88
|
+
agentbox drive send-text 1 "hello" # literal text, no DSL parsing, no trailing Enter
|
|
89
|
+
agentbox drive prompt 1 "summarize /workspace/README" # type + Enter (the convenience action)
|
|
90
|
+
agentbox drive wait 1 --text "✓" --timeout 60000 # block until <text> appears on screen
|
|
91
|
+
agentbox drive resize 1 200 60
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`keypress` uses a small DSL: `<Enter>`, `<Tab>`, `<Esc>`, `<Space>`, `<BS>`, `<Del>`, `<Up>/<Down>/<Left>/<Right>`, `<Home>/<End>/<PageUp>/<PageDown>`, `<F1>`–`<F12>`, `<C-a>`..`<C-z>`. Use `<<` for a literal `<`. Multiple args concatenate with no spaces (`"ls" "<Enter>"` → `ls\r`).
|
|
95
|
+
|
|
96
|
+
### `agentbox agent <box>` — agent state introspection (Claude / Codex / OpenCode)
|
|
97
|
+
|
|
98
|
+
Sub-second latency. State source by agent:
|
|
99
|
+
|
|
100
|
+
- **Claude Code**: lifecycle hooks (`UserPromptSubmit`, `PreToolUse`, `Stop`, `Notification`, `ExitPlanMode`, `AskUserQuestion`, `PreCompact`/`PostCompact`, `StopFailure`, `SubagentStart`/`SubagentStop`).
|
|
101
|
+
- **Codex**: tmux-pane scraper inside the box (codex 0.134.0's own hook firing is unreliable; staged hooks remain for the day that's fixed upstream).
|
|
102
|
+
- **OpenCode**: a plugin (`agentbox-state.js`) seeded into the OpenCode config volume, subscribing to OpenCode's event bus.
|
|
103
|
+
|
|
104
|
+
All three feed the same status pipeline; `agent state` / `agent wait-for` work the same regardless of which agent runs inside the box. Reports come from `~/.agentbox/boxes/<id>/status.json` and the relay event stream.
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
agentbox agent state 1 # → working | idle | waiting | end-plan | question | prompt | compacting | error
|
|
108
|
+
agentbox agent state 1 --json # full BoxStatusClaude (state, updatedAt, sessionTitle, plan?, question?)
|
|
109
|
+
|
|
110
|
+
agentbox agent wait-for prompt 1 --timeout 600000 # block until Claude is at the input box, no pending plan/question
|
|
111
|
+
agentbox agent wait-for end-plan 1 # Claude just called ExitPlanMode; user has to approve
|
|
112
|
+
agentbox agent wait-for question 1 # AskUserQuestion picker is up
|
|
113
|
+
agentbox agent wait-for idle 1 # Stop hook fired (turn complete)
|
|
114
|
+
agentbox agent wait-for compacting 1 # Claude is summarizing context (PreCompact fired)
|
|
115
|
+
agentbox agent wait-for error 1 # Claude's turn ended with a failure (StopFailure)
|
|
116
|
+
|
|
117
|
+
agentbox agent get-plan-question 1 # print the plan body OR question + options (human)
|
|
118
|
+
agentbox agent get-plan-question 1 --json # structured payload
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `prompt` state is derived: `idle` AND tmux session alive AND no pending plan/question — i.e. "ready for the next user message". Use it as the natural sync point after sending a new prompt.
|
|
122
|
+
|
|
123
|
+
The `end-plan` and `question` matchers tolerate the race where Claude's `Notification:permission_prompt` hook flips the raw state to `waiting` immediately after the matcher hook fires — both states still match while the plan/question payload is pending, and only the matching `PostToolUse` (handled internally with `--clear-pending`) resets them.
|
|
124
|
+
|
|
125
|
+
### `agentbox queue wait-for <event>` — queue + box lifecycle
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
agentbox queue wait-for new-box # any new box gets registered
|
|
129
|
+
agentbox queue wait-for empty-queue --timeout 1800000 # all queued/running jobs settled
|
|
130
|
+
agentbox queue wait-for box-running --box review
|
|
131
|
+
agentbox queue wait-for box-paused --box 2
|
|
132
|
+
agentbox queue wait-for box-stopped --box 2
|
|
133
|
+
agentbox queue wait-for job-done --job b45f1603841bd2b5 # terminal status (done/failed/cancelled)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
All wait-for commands exit 0 on match, exit 1 on timeout, and accept `--json` for parseable output.
|
|
137
|
+
|
|
138
|
+
### Recipe: queue a plan, then act per turn
|
|
139
|
+
|
|
140
|
+
This is the canonical "drive a Claude Code from another Claude Code" loop. You queue an initial planning prompt, wait for the plan to land, capture it, decide, send the next message, repeat.
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
# 1. Kick off a box with a planning prompt.
|
|
144
|
+
agentbox claude -n design -i "Plan how to add an OAuth login flow to apps/web, then enter plan mode. Don't start coding."
|
|
145
|
+
|
|
146
|
+
# 2. Wait until Claude is at the ExitPlanMode approval prompt.
|
|
147
|
+
agentbox agent wait-for end-plan design --timeout 600000
|
|
148
|
+
|
|
149
|
+
# 3. Read the plan back as text (or JSON) and decide.
|
|
150
|
+
PLAN=$(agentbox agent get-plan-question design)
|
|
151
|
+
echo "$PLAN"
|
|
152
|
+
|
|
153
|
+
# 4. Approve via tmux — option 1 ("Yes, and use auto mode") is already highlighted.
|
|
154
|
+
agentbox drive keypress design "<Enter>"
|
|
155
|
+
|
|
156
|
+
# 5. Wait for the turn to finish.
|
|
157
|
+
agentbox agent wait-for prompt design --timeout 1200000
|
|
158
|
+
|
|
159
|
+
# 6. Fan out follow-up work to a fresh box, in background, while reviewing this one.
|
|
160
|
+
agentbox claude -i "Write the OAuth provider unit tests in apps/web/test/auth/"
|
|
161
|
+
|
|
162
|
+
# 7. Block until everything settles before reporting back.
|
|
163
|
+
agentbox queue wait-for empty-queue --timeout 3600000
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The same shape covers `agent wait-for question` + `agent get-plan-question` (read the choices, send the answer index via `drive keypress 1 "<Down><Enter>"`, then `wait-for prompt`).
|
|
167
|
+
|
|
168
|
+
### Quick mental model
|
|
169
|
+
|
|
170
|
+
- `drive` = "send keystrokes / read screen" — provider-uniform tmux capture-pane / send-keys.
|
|
171
|
+
- `agent` = "what is the Claude TUI currently doing" — hook-driven, race-free, machine-readable.
|
|
172
|
+
- `queue wait-for` = "block on queue or box lifecycle transitions" — poll-based, no new endpoint.
|
|
173
|
+
- All three commands are **stateless** — safe to invoke from any script, any agent, in parallel.
|
|
174
|
+
- `--json` everywhere. Default human text is for the operator; an agent should pass `--json`.
|
|
175
|
+
|
|
176
|
+
## Git through the host relay
|
|
177
|
+
|
|
178
|
+
**The box has no SSH keys, GPG keys, or git remote credentials.** Don't ask the user to add any. When an in-box agent (or a script you run inside the box) does `git push` or `git pull`, the AgentBox-provided `agentbox-ctl git` wrapper POSTs a JSON-RPC call to the host relay (`POST /rpc`, bearer-auth, loopback-only). The relay runs the **real** `git push origin …` on the host, using the user's `SSH_AUTH_SOCK`, `~/.gitconfig`, and identity — and streams stdout/stderr back into the box's terminal. The box's exit code matches the host's.
|
|
179
|
+
|
|
180
|
+
Implications for you, the host-side agent:
|
|
181
|
+
|
|
182
|
+
- Inside the box you can `git commit … && git push` exactly as normal. No setup needed.
|
|
183
|
+
- Pushes are gated host-side: the relay can require a confirm prompt for destructive operations (the user sees it in the dashboard footer, ~25 s TTL). If a push appears to hang, tell the user to check the dashboard.
|
|
184
|
+
- The relay process is started lazily by the first `agentbox create` / `agentbox claude` and persists across runs (PID at `~/.agentbox/relay.pid`, log at `~/.agentbox/relay.log`). You normally don't need to manage it.
|
|
185
|
+
|
|
186
|
+
## Other commands worth knowing
|
|
187
|
+
|
|
188
|
+
| Command | What it does |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `agentbox dashboard` | TUI status + switcher across all boxes. The leader is **`Ctrl+a`** (e.g. `Ctrl+a u` opens the box's web URL; `Ctrl+a s` opens the in-box browser; `Ctrl+a q` quits). |
|
|
191
|
+
| `agentbox shell [n\|name]` | Interactive `bash -l` inside the box (also wrapped in tmux by default — detach with `Ctrl+a d`). |
|
|
192
|
+
| `agentbox url [n\|name]` | Open the box's web app URL (`<box-name>.localhost` via Portless) in the host browser. |
|
|
193
|
+
| `agentbox screen [n\|name]` | Open the box's **own** Chromium via VNC — useful for OAuth flows the agent inside the box initiates. |
|
|
194
|
+
| `agentbox code [n\|name]` | Open VS Code / Cursor pointed at the box. |
|
|
195
|
+
| `agentbox prepare --provider <name>` | One-time base image / snapshot build for `daytona` or `hetzner`. With no `--provider`, prints status across all providers. |
|
|
196
|
+
| `agentbox prune --provider <name>` | Clean up orphan boxes / images / snapshots for a provider (docker + daytona supported; hetzner pending). |
|
|
197
|
+
|
|
198
|
+
Per-project numeric index (`1`, `2`, …) and friendly name (`review`, `smoke`) both work wherever `<box>` is accepted. Index `1` is the first box created in the current workspace.
|
|
199
|
+
|
|
200
|
+
## Operating principles
|
|
201
|
+
|
|
202
|
+
1. **Never assume the host needs SSH keys forwarded into a box** — git is handled by the relay, by design.
|
|
203
|
+
2. **Use `-i` whenever the user asks for parallel agent work** rather than spawning multiple foreground sessions. Then point them at `agentbox dashboard` to watch progress.
|
|
204
|
+
3. **Pick the provider deliberately.** `docker` is the fast default. `--provider hetzner` gives a real VPS (heavier, isolated, requires `agentbox prepare --provider hetzner` once). `--provider daytona` is the managed cloud option.
|
|
205
|
+
4. **Cross-check before recommending a command.** If a flag isn't listed here, run `agentbox <command> --help` (it's safe and read-only) before suggesting it to the user.
|
|
206
|
+
5. **`/agentbox-setup` is a different skill.** It runs *inside* a box to generate `/workspace/agentbox.yaml`. Don't conflate it with `/agentbox` (host-side fork) or this reference skill.
|
|
207
|
+
|
|
208
|
+
## Reference
|
|
209
|
+
|
|
210
|
+
- Full docs live in the repo at `docs/` — start with `docs/architecture.md` and `docs/create-and-checkpoints.md` for the model, `docs/host-relay.md` for the relay, `docs/cloud-providers.md` for the cloud paths.
|
|
211
|
+
- npm package: `@madarco/agentbox` — `npm -g install @madarco/agentbox` (or `npx @madarco/agentbox <command>`).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Fork the current Codex session into a new AgentBox box and resume it there (opens in a new terminal tab).
|
|
3
|
+
argument-hint: [provider]
|
|
4
|
+
---
|
|
5
|
+
<!-- agentbox-managed:v1 -->
|
|
6
|
+
|
|
7
|
+
Fork the current Codex session into a fresh AgentBox box running Codex.
|
|
8
|
+
|
|
9
|
+
Optional provider argument: `$ARGUMENTS` (docker | daytona | hetzner; default docker).
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. **Pre-flight (stop on either):**
|
|
14
|
+
- If `AGENTBOX_RELAY_URL` is set in the environment, you are running *inside* a box — box→box fork is not supported yet; stop and tell the user.
|
|
15
|
+
- If `which agentbox` fails, tell the user to install AgentBox (`npm -g install @madarco/agentbox`) and stop.
|
|
16
|
+
|
|
17
|
+
2. **Find the current Codex session id.** Codex exposes no session-id variable, so resolve it from the most recently written rollout file (that is the live session). Run via your shell tool:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
ls -t "$HOME"/.codex/sessions/*/*/*/rollout-*.jsonl 2>/dev/null | head -1 \
|
|
21
|
+
| xargs -I{} basename {} .jsonl \
|
|
22
|
+
| grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That prints the session `<uuid>`. If it prints nothing, stop and tell the user no Codex session was found for this machine.
|
|
26
|
+
|
|
27
|
+
3. **Resolve the provider flag from `$ARGUMENTS`:** empty → none; `docker` | `daytona` | `hetzner` → `--provider $ARGUMENTS`; anything else → stop and report the valid values.
|
|
28
|
+
|
|
29
|
+
4. **Fork.** Run, via your shell tool:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
agentbox fork --agent codex --session <uuid> [--provider <from step 3>]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
5. **Report** the new box name from the command output. Your current Codex session is unaffected — you now have two parallel timelines.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Spawn a parallel AgentBox box running OpenCode for this project (opens in a new terminal tab). Note - the current OpenCode session is not resumed yet; this starts a fresh session.
|
|
3
|
+
---
|
|
4
|
+
<!-- agentbox-managed:v1 -->
|
|
5
|
+
|
|
6
|
+
Spawn a new AgentBox box running OpenCode for the current project, in a new terminal tab.
|
|
7
|
+
|
|
8
|
+
Optional provider argument: `$ARGUMENTS` (docker | daytona | hetzner; default docker).
|
|
9
|
+
|
|
10
|
+
**Note:** resuming an OpenCode session into a box isn't supported yet (sessions live in a shared SQLite DB), so this starts a **fresh** OpenCode session in the box — it does not carry the current conversation.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. **Pre-flight (stop on either):**
|
|
15
|
+
- If `AGENTBOX_RELAY_URL` is set in the environment, you are running *inside* a box — not supported; stop and tell the user.
|
|
16
|
+
- If `which agentbox` fails, tell the user to install AgentBox (`npm -g install @madarco/agentbox`) and stop.
|
|
17
|
+
|
|
18
|
+
2. **Resolve the provider flag from `$ARGUMENTS`:** empty → none; `docker` | `daytona` | `hetzner` → `--provider $ARGUMENTS`; anything else → stop and report the valid values.
|
|
19
|
+
|
|
20
|
+
3. **Fork.** Run, via your shell tool:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
agentbox fork --agent opencode [--provider <from step 2>]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
4. **Report** the new box name from the command output.
|
package/dist/chunk-BBZMA2K6.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
CHECKPOINT_IMAGE_PREFIX,
|
|
4
|
-
checkpointImageTag,
|
|
5
|
-
detectEngine,
|
|
6
|
-
inspectContainer,
|
|
7
|
-
inspectContainerStatus,
|
|
8
|
-
inspectVolumeMountpoint
|
|
9
|
-
} from "./chunk-HHMWQNLF.js";
|
|
10
|
-
|
|
11
|
-
// ../../packages/sandbox-docker/dist/chunk-RKWE53VF.js
|
|
12
|
-
import { homedir } from "os";
|
|
13
|
-
import { join } from "path";
|
|
14
|
-
import { execa } from "execa";
|
|
15
|
-
function parseDockerSize(raw) {
|
|
16
|
-
const s = raw.trim();
|
|
17
|
-
if (!s || s === "--" || s === "N/A") return null;
|
|
18
|
-
const m = /^([\d.]+)\s*([A-Za-z]*)$/.exec(s);
|
|
19
|
-
if (!m) return null;
|
|
20
|
-
const n = Number(m[1]);
|
|
21
|
-
if (!Number.isFinite(n)) return null;
|
|
22
|
-
const unit = (m[2] ?? "").toLowerCase();
|
|
23
|
-
const mult = {
|
|
24
|
-
"": 1,
|
|
25
|
-
b: 1,
|
|
26
|
-
kb: 1e3,
|
|
27
|
-
mb: 1e6,
|
|
28
|
-
gb: 1e9,
|
|
29
|
-
tb: 1e12,
|
|
30
|
-
kib: 1024,
|
|
31
|
-
mib: 1024 ** 2,
|
|
32
|
-
gib: 1024 ** 3,
|
|
33
|
-
tib: 1024 ** 4
|
|
34
|
-
};
|
|
35
|
-
const factor = mult[unit];
|
|
36
|
-
return factor === void 0 ? null : n * factor;
|
|
37
|
-
}
|
|
38
|
-
function parsePercent(raw) {
|
|
39
|
-
if (!raw) return null;
|
|
40
|
-
const n = Number(raw.replace("%", "").trim());
|
|
41
|
-
return Number.isFinite(n) ? n : null;
|
|
42
|
-
}
|
|
43
|
-
function splitPair(raw) {
|
|
44
|
-
if (!raw) return null;
|
|
45
|
-
const parts = raw.split("/");
|
|
46
|
-
if (parts.length !== 2) return null;
|
|
47
|
-
return [parts[0].trim(), parts[1].trim()];
|
|
48
|
-
}
|
|
49
|
-
async function duBytes(path) {
|
|
50
|
-
const result = await execa("du", ["-sk", path], { reject: false });
|
|
51
|
-
if (result.exitCode !== 0) return null;
|
|
52
|
-
const kb = Number.parseInt((result.stdout ?? "").split(/\s+/)[0] ?? "", 10);
|
|
53
|
-
return Number.isNaN(kb) ? null : kb * 1024;
|
|
54
|
-
}
|
|
55
|
-
async function volumeSizeBytes(name) {
|
|
56
|
-
if (!name) return null;
|
|
57
|
-
const engine = await detectEngine();
|
|
58
|
-
if (engine === "orbstack") {
|
|
59
|
-
const live = join(homedir(), "OrbStack", "docker", "volumes", name);
|
|
60
|
-
const sz = await duBytes(live);
|
|
61
|
-
if (sz !== null) return sz;
|
|
62
|
-
}
|
|
63
|
-
const df = await execa(
|
|
64
|
-
"docker",
|
|
65
|
-
["system", "df", "-v", "--format", "{{json .Volumes}}"],
|
|
66
|
-
{ reject: false }
|
|
67
|
-
);
|
|
68
|
-
if (df.exitCode === 0) {
|
|
69
|
-
try {
|
|
70
|
-
const vols = JSON.parse(df.stdout || "[]");
|
|
71
|
-
const hit = vols.find((v) => v.Name === name);
|
|
72
|
-
const parsed = hit?.Size ? parseDockerSize(hit.Size) : null;
|
|
73
|
-
if (parsed !== null) return parsed;
|
|
74
|
-
} catch {
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const mp = await inspectVolumeMountpoint(name);
|
|
78
|
-
if (mp && !mp.startsWith("/var/lib/docker")) {
|
|
79
|
-
return duBytes(mp);
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
async function imageBytes(tag) {
|
|
84
|
-
const r = await execa("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
|
|
85
|
-
reject: false
|
|
86
|
-
});
|
|
87
|
-
if (r.exitCode !== 0) return null;
|
|
88
|
-
const n = Number.parseInt((r.stdout ?? "").trim(), 10);
|
|
89
|
-
return Number.isFinite(n) ? n : null;
|
|
90
|
-
}
|
|
91
|
-
async function projectCheckpointImageBytes(projectRoot, name) {
|
|
92
|
-
return imageBytes(checkpointImageTag(projectRoot, name));
|
|
93
|
-
}
|
|
94
|
-
async function allCheckpointImagesBytes() {
|
|
95
|
-
const r = await execa(
|
|
96
|
-
"docker",
|
|
97
|
-
[
|
|
98
|
-
"image",
|
|
99
|
-
"ls",
|
|
100
|
-
"--format",
|
|
101
|
-
"{{.Repository}}:{{.Tag}} {{.Size}}",
|
|
102
|
-
`${CHECKPOINT_IMAGE_PREFIX}*`
|
|
103
|
-
],
|
|
104
|
-
{ reject: false }
|
|
105
|
-
);
|
|
106
|
-
if (r.exitCode !== 0) return null;
|
|
107
|
-
const lines = (r.stdout ?? "").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
108
|
-
if (lines.length === 0) return null;
|
|
109
|
-
let total = 0;
|
|
110
|
-
let any = false;
|
|
111
|
-
for (const line of lines) {
|
|
112
|
-
const [, size] = line.split(" ");
|
|
113
|
-
const n = size ? parseDockerSize(size) : null;
|
|
114
|
-
if (n !== null) {
|
|
115
|
-
total += n;
|
|
116
|
-
any = true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return any ? total : null;
|
|
120
|
-
}
|
|
121
|
-
async function agentboxHomeBytes() {
|
|
122
|
-
return duBytes(join(homedir(), ".agentbox"));
|
|
123
|
-
}
|
|
124
|
-
function limitsFromRecord(record) {
|
|
125
|
-
const r = record.resourceLimits;
|
|
126
|
-
return {
|
|
127
|
-
memoryBytes: r?.memoryBytes && r.memoryBytes > 0 ? r.memoryBytes : null,
|
|
128
|
-
cpus: r?.cpus && r.cpus > 0 ? r.cpus : null,
|
|
129
|
-
pidsLimit: r?.pidsLimit && r.pidsLimit > 0 ? r.pidsLimit : null,
|
|
130
|
-
disk: r?.disk || null
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
function reconcileLimits(persisted, dockerJson) {
|
|
134
|
-
const hc = dockerJson?.HostConfig;
|
|
135
|
-
if (!hc) return persisted;
|
|
136
|
-
const mem = typeof hc.Memory === "number" && hc.Memory > 0 ? hc.Memory : null;
|
|
137
|
-
const nano = typeof hc.NanoCpus === "number" && hc.NanoCpus > 0 ? hc.NanoCpus : null;
|
|
138
|
-
const pids = typeof hc.PidsLimit === "number" && hc.PidsLimit > 0 ? hc.PidsLimit : null;
|
|
139
|
-
return {
|
|
140
|
-
memoryBytes: mem ?? persisted.memoryBytes,
|
|
141
|
-
cpus: nano ? nano / 1e9 : persisted.cpus,
|
|
142
|
-
pidsLimit: pids ?? persisted.pidsLimit,
|
|
143
|
-
disk: persisted.disk
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
async function containerWritableBytes(container) {
|
|
147
|
-
const r = await execa(
|
|
148
|
-
"docker",
|
|
149
|
-
["ps", "-a", "--filter", `name=^${container}$`, "--format", "{{.Size}}", "--size"],
|
|
150
|
-
{ reject: false }
|
|
151
|
-
);
|
|
152
|
-
if (r.exitCode !== 0) return null;
|
|
153
|
-
const first = (r.stdout ?? "").split("\n")[0]?.trim();
|
|
154
|
-
if (!first) return null;
|
|
155
|
-
const m = /^([^()]+?)(?:\s*\(.*\))?$/.exec(first);
|
|
156
|
-
const sz = m ? m[1].trim() : first;
|
|
157
|
-
return parseDockerSize(sz);
|
|
158
|
-
}
|
|
159
|
-
async function boxResourceStats(record) {
|
|
160
|
-
const warnings = [];
|
|
161
|
-
const dockerJson = await inspectContainer(record.container);
|
|
162
|
-
const limits = reconcileLimits(limitsFromRecord(record), dockerJson);
|
|
163
|
-
const [diskContainer, diskDocker, snapshotDiskBytes, checkpointImageBytesValue] = await Promise.all([
|
|
164
|
-
containerWritableBytes(record.container),
|
|
165
|
-
record.dockerVolume ? volumeSizeBytes(record.dockerVolume) : Promise.resolve(null),
|
|
166
|
-
record.snapshotDir ? duBytes(record.snapshotDir) : Promise.resolve(null),
|
|
167
|
-
record.checkpointImage ? imageBytes(record.checkpointImage) : Promise.resolve(null)
|
|
168
|
-
]);
|
|
169
|
-
const diskUsedBytes = diskContainer === null && diskDocker === null ? null : (diskContainer ?? 0) + (diskDocker ?? 0);
|
|
170
|
-
if (diskUsedBytes === null) {
|
|
171
|
-
warnings.push("disk usage unavailable on this engine");
|
|
172
|
-
}
|
|
173
|
-
const base = {
|
|
174
|
-
source: "docker",
|
|
175
|
-
live: false,
|
|
176
|
-
cpuPercent: null,
|
|
177
|
-
memUsedBytes: null,
|
|
178
|
-
memLimitBytes: limits.memoryBytes,
|
|
179
|
-
memPercent: null,
|
|
180
|
-
pids: null,
|
|
181
|
-
diskUsedBytes,
|
|
182
|
-
snapshotDiskBytes,
|
|
183
|
-
checkpointVolumeBytes: checkpointImageBytesValue,
|
|
184
|
-
netRxBytes: null,
|
|
185
|
-
netTxBytes: null,
|
|
186
|
-
blockReadBytes: null,
|
|
187
|
-
blockWriteBytes: null,
|
|
188
|
-
limits,
|
|
189
|
-
warnings
|
|
190
|
-
};
|
|
191
|
-
if (await inspectContainerStatus(record.container) !== "running") {
|
|
192
|
-
return base;
|
|
193
|
-
}
|
|
194
|
-
const proc = await execa(
|
|
195
|
-
"docker",
|
|
196
|
-
["stats", "--no-stream", "--format", "{{json .}}", record.container],
|
|
197
|
-
{ reject: false }
|
|
198
|
-
);
|
|
199
|
-
if (proc.exitCode !== 0 || !proc.stdout.trim()) {
|
|
200
|
-
return base;
|
|
201
|
-
}
|
|
202
|
-
let line;
|
|
203
|
-
try {
|
|
204
|
-
line = JSON.parse(proc.stdout.trim().split("\n")[0]);
|
|
205
|
-
} catch {
|
|
206
|
-
return base;
|
|
207
|
-
}
|
|
208
|
-
const memPair = splitPair(line.MemUsage);
|
|
209
|
-
const memUsedBytes = memPair ? parseDockerSize(memPair[0]) : null;
|
|
210
|
-
const memEngineTotal = memPair ? parseDockerSize(memPair[1]) : null;
|
|
211
|
-
const netPair = splitPair(line.NetIO);
|
|
212
|
-
const blkPair = splitPair(line.BlockIO);
|
|
213
|
-
return {
|
|
214
|
-
...base,
|
|
215
|
-
live: true,
|
|
216
|
-
cpuPercent: parsePercent(line.CPUPerc),
|
|
217
|
-
memUsedBytes,
|
|
218
|
-
// The applied limit when set; otherwise docker stats' own denominator
|
|
219
|
-
// (the engine/host total).
|
|
220
|
-
memLimitBytes: limits.memoryBytes ?? memEngineTotal,
|
|
221
|
-
memPercent: parsePercent(line.MemPerc),
|
|
222
|
-
pids: line.PIDs ? Number.parseInt(line.PIDs, 10) || null : null,
|
|
223
|
-
netRxBytes: netPair ? parseDockerSize(netPair[0]) : null,
|
|
224
|
-
netTxBytes: netPair ? parseDockerSize(netPair[1]) : null,
|
|
225
|
-
blockReadBytes: blkPair ? parseDockerSize(blkPair[0]) : null,
|
|
226
|
-
blockWriteBytes: blkPair ? parseDockerSize(blkPair[1]) : null
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export {
|
|
231
|
-
parseDockerSize,
|
|
232
|
-
volumeSizeBytes,
|
|
233
|
-
projectCheckpointImageBytes,
|
|
234
|
-
allCheckpointImagesBytes,
|
|
235
|
-
agentboxHomeBytes,
|
|
236
|
-
boxResourceStats
|
|
237
|
-
};
|
|
238
|
-
//# sourceMappingURL=chunk-BBZMA2K6.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../packages/sandbox-docker/src/stats.ts"],"sourcesContent":["import { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { execa } from 'execa';\nimport type { BoxResourceLimits, BoxResourceStats } from '@agentbox/core';\nimport { CHECKPOINT_IMAGE_PREFIX, checkpointImageTag } from './checkpoint.js';\nimport {\n inspectContainer,\n inspectContainerStatus,\n inspectVolumeMountpoint,\n} from './docker.js';\nimport { detectEngine } from './host-export.js';\nimport type { BoxRecord } from './state.js';\n\n/**\n * Parse one of Docker's human-formatted size tokens (`512MiB`, `1.2kB`,\n * `3.4GB`, `0B`, `--`). Returns null when unparseable. Docker mixes binary\n * (`KiB/MiB/GiB`) and decimal (`kB/MB/GB`) suffixes depending on the column, so\n * we handle both.\n */\nexport function parseDockerSize(raw: string): number | null {\n const s = raw.trim();\n if (!s || s === '--' || s === 'N/A') return null;\n const m = /^([\\d.]+)\\s*([A-Za-z]*)$/.exec(s);\n if (!m) return null;\n const n = Number(m[1]);\n if (!Number.isFinite(n)) return null;\n const unit = (m[2] ?? '').toLowerCase();\n const mult: Record<string, number> = {\n '': 1,\n b: 1,\n kb: 1e3,\n mb: 1e6,\n gb: 1e9,\n tb: 1e12,\n kib: 1024,\n mib: 1024 ** 2,\n gib: 1024 ** 3,\n tib: 1024 ** 4,\n };\n const factor = mult[unit];\n return factor === undefined ? null : n * factor;\n}\n\nfunction parsePercent(raw: string | undefined): number | null {\n if (!raw) return null;\n const n = Number(raw.replace('%', '').trim());\n return Number.isFinite(n) ? n : null;\n}\n\n/** Split Docker's \"<a> / <b>\" pair columns (MemUsage, NetIO, BlockIO). */\nfunction splitPair(raw: string | undefined): [string, string] | null {\n if (!raw) return null;\n const parts = raw.split('/');\n if (parts.length !== 2) return null;\n return [parts[0]!.trim(), parts[1]!.trim()];\n}\n\nasync function duBytes(path: string): Promise<number | null> {\n const result = await execa('du', ['-sk', path], { reject: false });\n if (result.exitCode !== 0) return null;\n const kb = Number.parseInt((result.stdout ?? '').split(/\\s+/)[0] ?? '', 10);\n return Number.isNaN(kb) ? null : kb * 1024;\n}\n\n/**\n * Best-effort on-host byte size of a Docker named volume. Fastest path first:\n * 1. OrbStack exposes volumes live at ~/OrbStack/docker/volumes/<name>/.\n * 2. `docker system df -v` (cheap-walked once; may report \"N/A\").\n * 3. The reported mountpoint, only when host-readable (Linux native Docker).\n * Returns null when no path is reachable from the host (the macOS VM case\n * where `system df` also has no number).\n */\nexport async function volumeSizeBytes(name: string): Promise<number | null> {\n if (!name) return null;\n const engine = await detectEngine();\n if (engine === 'orbstack') {\n const live = join(homedir(), 'OrbStack', 'docker', 'volumes', name);\n const sz = await duBytes(live);\n if (sz !== null) return sz;\n }\n const df = await execa(\n 'docker',\n ['system', 'df', '-v', '--format', '{{json .Volumes}}'],\n { reject: false },\n );\n if (df.exitCode === 0) {\n try {\n const vols = JSON.parse(df.stdout || '[]') as Array<{ Name?: string; Size?: string }>;\n const hit = vols.find((v) => v.Name === name);\n const parsed = hit?.Size ? parseDockerSize(hit.Size) : null;\n if (parsed !== null) return parsed;\n } catch {\n // fall through to mountpoint\n }\n }\n const mp = await inspectVolumeMountpoint(name);\n if (mp && !mp.startsWith('/var/lib/docker')) {\n return duBytes(mp);\n }\n return null;\n}\n\n/**\n * On-host byte size of a Docker image (sum of its own layer sizes — what\n * `docker images` reports). Null on docker errors.\n */\nasync function imageBytes(tag: string): Promise<number | null> {\n const r = await execa('docker', ['image', 'inspect', tag, '--format', '{{.Size}}'], {\n reject: false,\n });\n if (r.exitCode !== 0) return null;\n const n = Number.parseInt((r.stdout ?? '').trim(), 10);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Size of a project's most-recent checkpoint image (the head of the lineage,\n * resolved via the `checkpointImageTag` helper from a checkpoint *name*). The\n * caller passes the name because we don't enumerate manifests from this\n * module — that lives in checkpoint.ts. Null when the image isn't present.\n */\nexport async function projectCheckpointImageBytes(\n projectRoot: string,\n name: string,\n): Promise<number | null> {\n return imageBytes(checkpointImageTag(projectRoot, name));\n}\n\n/**\n * Total on-host bytes of every checkpoint image (the durable, cross-box\n * warm-state assets). Walks every image tag under `CHECKPOINT_IMAGE_PREFIX`.\n * Null when none exist.\n */\nexport async function allCheckpointImagesBytes(): Promise<number | null> {\n const r = await execa(\n 'docker',\n [\n 'image',\n 'ls',\n '--format',\n '{{.Repository}}:{{.Tag}}\\t{{.Size}}',\n `${CHECKPOINT_IMAGE_PREFIX}*`,\n ],\n { reject: false },\n );\n if (r.exitCode !== 0) return null;\n const lines = (r.stdout ?? '')\n .split('\\n')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n if (lines.length === 0) return null;\n let total = 0;\n let any = false;\n for (const line of lines) {\n const [, size] = line.split('\\t');\n const n = size ? parseDockerSize(size) : null;\n if (n !== null) {\n total += n;\n any = true;\n }\n }\n return any ? total : null;\n}\n\n/** On-host byte size of the whole ~/.agentbox state/runtime directory. */\nexport async function agentboxHomeBytes(): Promise<number | null> {\n return duBytes(join(homedir(), '.agentbox'));\n}\n\nfunction limitsFromRecord(record: BoxRecord): BoxResourceLimits {\n const r = record.resourceLimits;\n return {\n memoryBytes: r?.memoryBytes && r.memoryBytes > 0 ? r.memoryBytes : null,\n cpus: r?.cpus && r.cpus > 0 ? r.cpus : null,\n pidsLimit: r?.pidsLimit && r.pidsLimit > 0 ? r.pidsLimit : null,\n disk: r?.disk || null,\n };\n}\n\n/**\n * Cross-check persisted limits against the live container's HostConfig so an\n * externally `docker update`d box still reports the truth. The persisted\n * record stays the fallback when the container is gone.\n */\nfunction reconcileLimits(persisted: BoxResourceLimits, dockerJson: unknown): BoxResourceLimits {\n const hc = (dockerJson as { HostConfig?: Record<string, unknown> } | null)?.HostConfig;\n if (!hc) return persisted;\n const mem = typeof hc.Memory === 'number' && hc.Memory > 0 ? hc.Memory : null;\n const nano = typeof hc.NanoCpus === 'number' && hc.NanoCpus > 0 ? hc.NanoCpus : null;\n const pids = typeof hc.PidsLimit === 'number' && hc.PidsLimit > 0 ? hc.PidsLimit : null;\n return {\n memoryBytes: mem ?? persisted.memoryBytes,\n cpus: nano ? nano / 1e9 : persisted.cpus,\n pidsLimit: pids ?? persisted.pidsLimit,\n disk: persisted.disk,\n };\n}\n\ninterface DockerStatsLine {\n CPUPerc?: string;\n MemUsage?: string;\n MemPerc?: string;\n PIDs?: string;\n NetIO?: string;\n BlockIO?: string;\n}\n\n/**\n * Container writable-layer size from `docker ps --size`. With the overlay\n * gone, `/workspace` lives here (not in a named volume), so this is the\n * box's primary writable-surface number.\n */\nasync function containerWritableBytes(container: string): Promise<number | null> {\n const r = await execa(\n 'docker',\n ['ps', '-a', '--filter', `name=^${container}$`, '--format', '{{.Size}}', '--size'],\n { reject: false },\n );\n if (r.exitCode !== 0) return null;\n // `--size` produces `<rw> (virtual <total>)`; we want the first number.\n const first = (r.stdout ?? '').split('\\n')[0]?.trim();\n if (!first) return null;\n const m = /^([^()]+?)(?:\\s*\\(.*\\))?$/.exec(first);\n const sz = m ? m[1]!.trim() : first;\n return parseDockerSize(sz);\n}\n\n/**\n * Provider-agnostic resource snapshot for a box. CPU/mem/pids/IO come from\n * `docker stats --no-stream` (point-in-time sample; only when the container is\n * running). Disk is the container's writable layer (where `/workspace` lives\n * now that the overlay is gone) plus the in-box dockerd's data-root volume;\n * the per-box host snapshot dir and the checkpoint image lineage are\n * reported on their own fields.\n */\nexport async function boxResourceStats(record: BoxRecord): Promise<BoxResourceStats> {\n const warnings: string[] = [];\n const dockerJson = await inspectContainer(record.container);\n const limits = reconcileLimits(limitsFromRecord(record), dockerJson);\n\n const [diskContainer, diskDocker, snapshotDiskBytes, checkpointImageBytesValue] =\n await Promise.all([\n containerWritableBytes(record.container),\n record.dockerVolume ? volumeSizeBytes(record.dockerVolume) : Promise.resolve(null),\n record.snapshotDir ? duBytes(record.snapshotDir) : Promise.resolve(null),\n record.checkpointImage ? imageBytes(record.checkpointImage) : Promise.resolve(null),\n ]);\n const diskUsedBytes =\n diskContainer === null && diskDocker === null\n ? null\n : (diskContainer ?? 0) + (diskDocker ?? 0);\n if (diskUsedBytes === null) {\n warnings.push('disk usage unavailable on this engine');\n }\n\n const base: BoxResourceStats = {\n source: 'docker',\n live: false,\n cpuPercent: null,\n memUsedBytes: null,\n memLimitBytes: limits.memoryBytes,\n memPercent: null,\n pids: null,\n diskUsedBytes,\n snapshotDiskBytes,\n checkpointVolumeBytes: checkpointImageBytesValue,\n netRxBytes: null,\n netTxBytes: null,\n blockReadBytes: null,\n blockWriteBytes: null,\n limits,\n warnings,\n };\n\n if ((await inspectContainerStatus(record.container)) !== 'running') {\n return base;\n }\n\n const proc = await execa(\n 'docker',\n ['stats', '--no-stream', '--format', '{{json .}}', record.container],\n { reject: false },\n );\n if (proc.exitCode !== 0 || !proc.stdout.trim()) {\n return base;\n }\n let line: DockerStatsLine;\n try {\n line = JSON.parse(proc.stdout.trim().split('\\n')[0]!) as DockerStatsLine;\n } catch {\n return base;\n }\n\n const memPair = splitPair(line.MemUsage);\n const memUsedBytes = memPair ? parseDockerSize(memPair[0]) : null;\n const memEngineTotal = memPair ? parseDockerSize(memPair[1]) : null;\n const netPair = splitPair(line.NetIO);\n const blkPair = splitPair(line.BlockIO);\n\n return {\n ...base,\n live: true,\n cpuPercent: parsePercent(line.CPUPerc),\n memUsedBytes,\n // The applied limit when set; otherwise docker stats' own denominator\n // (the engine/host total).\n memLimitBytes: limits.memoryBytes ?? memEngineTotal,\n memPercent: parsePercent(line.MemPerc),\n pids: line.PIDs ? Number.parseInt(line.PIDs, 10) || null : null,\n netRxBytes: netPair ? parseDockerSize(netPair[0]) : null,\n netTxBytes: netPair ? parseDockerSize(netPair[1]) : null,\n blockReadBytes: blkPair ? parseDockerSize(blkPair[0]) : null,\n blockWriteBytes: blkPair ? parseDockerSize(blkPair[1]) : null,\n };\n}\n"],"mappings":"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","names":[]}
|