@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.
Files changed (75) hide show
  1. package/dist/_cloud-attach-T727ZPRV.js +13 -0
  2. package/dist/chunk-67N47KUS.js +1640 -0
  3. package/dist/chunk-67N47KUS.js.map +1 -0
  4. package/dist/chunk-6OZDFNBF.js +8114 -0
  5. package/dist/chunk-6OZDFNBF.js.map +1 -0
  6. package/dist/chunk-BGK32PZE.js +455 -0
  7. package/dist/chunk-BGK32PZE.js.map +1 -0
  8. package/dist/chunk-FODMEHD3.js +1200 -0
  9. package/dist/chunk-FODMEHD3.js.map +1 -0
  10. package/dist/chunk-G3H2L3O2.js +288 -0
  11. package/dist/chunk-G3H2L3O2.js.map +1 -0
  12. package/dist/chunk-I24B6AXR.js +600 -0
  13. package/dist/chunk-I24B6AXR.js.map +1 -0
  14. package/dist/chunk-LEV3KICD.js +738 -0
  15. package/dist/chunk-LEV3KICD.js.map +1 -0
  16. package/dist/cloud-poller-SUNA6ZQC-2RG5WPRN.js +10 -0
  17. package/dist/dist-L4LCG5SJ.js +293 -0
  18. package/dist/dist-L4LCG5SJ.js.map +1 -0
  19. package/dist/dist-LOZBWMBF.js +447 -0
  20. package/dist/dist-ZODPD2I6.js +1407 -0
  21. package/dist/dist-ZODPD2I6.js.map +1 -0
  22. package/dist/index.js +7281 -2134
  23. package/dist/index.js.map +1 -1
  24. package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js +18 -0
  25. package/package.json +8 -3
  26. package/runtime/daytona/custom-system-CLAUDE.md +39 -0
  27. package/runtime/docker/Dockerfile.box +120 -14
  28. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +15 -8
  29. package/runtime/docker/packages/ctl/dist/bin.cjs +11310 -816
  30. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +68 -0
  31. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +9 -9
  32. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
  33. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
  34. package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
  35. package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
  36. package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
  37. package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
  38. package/runtime/hetzner/agentbox-codex-hooks.json +68 -0
  39. package/runtime/hetzner/agentbox-dockerd-start +132 -0
  40. package/runtime/hetzner/agentbox-open +28 -0
  41. package/runtime/hetzner/agentbox-setup-skill.md +196 -0
  42. package/runtime/hetzner/agentbox-vnc-start +77 -0
  43. package/runtime/hetzner/claude-managed-settings.json +115 -0
  44. package/runtime/hetzner/ctl.cjs +23397 -0
  45. package/runtime/hetzner/custom-system-CLAUDE.md +39 -0
  46. package/runtime/hetzner/gh-shim +263 -0
  47. package/runtime/hetzner/git-shim +131 -0
  48. package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
  49. package/runtime/hetzner/scripts/install-box.sh +374 -0
  50. package/runtime/relay/bin.cjs +10017 -817
  51. package/share/agentbox-setup/SKILL.md +15 -8
  52. package/share/host-skills/agentbox/SKILL.md +29 -0
  53. package/share/host-skills/agentbox-info/SKILL.md +211 -0
  54. package/share/host-skills/codex/agentbox.md +35 -0
  55. package/share/host-skills/opencode/agentbox.md +26 -0
  56. package/dist/chunk-BBZMA2K6.js +0 -238
  57. package/dist/chunk-BBZMA2K6.js.map +0 -1
  58. package/dist/chunk-HHMWQNLF.js +0 -1709
  59. package/dist/chunk-HHMWQNLF.js.map +0 -1
  60. package/dist/chunk-HPZMD5DE.js +0 -106
  61. package/dist/chunk-HPZMD5DE.js.map +0 -1
  62. package/dist/chunk-HTTKML3C.js +0 -2655
  63. package/dist/chunk-HTTKML3C.js.map +0 -1
  64. package/dist/chunk-KJNZP6I3.js +0 -586
  65. package/dist/chunk-KJNZP6I3.js.map +0 -1
  66. package/dist/chunk-M7I247BK.js +0 -525
  67. package/dist/chunk-M7I247BK.js.map +0 -1
  68. package/dist/create-6PWXI6HO-OWAMHBAK.js +0 -15
  69. package/dist/lifecycle-EMXR46DI-DUVBXNTV.js +0 -38
  70. package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
  71. package/dist/stats-SZXOJE3D-N7OODCHW.js +0 -19
  72. /package/dist/{create-6PWXI6HO-OWAMHBAK.js.map → _cloud-attach-T727ZPRV.js.map} +0 -0
  73. /package/dist/{lifecycle-EMXR46DI-DUVBXNTV.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
  74. /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-LOZBWMBF.js.map} +0 -0
  75. /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 passwordless sudo to run commands as root.
10
+ Your user i `vscode` and you can use `sudo` to run commands as root.
11
11
 
12
- `/workspace` is the box's plain writable filesystem — 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). Anything you install or build into `/workspace` (incl. `node_modules`, `.next`, `target`, `.venv`) lives in the **container's writable layer** and is captured wholesale by `agentbox checkpoint` (`docker commit`) — so a setup task that runs the install once becomes a warm-start asset for every future box in the project. Everything is wiped on `agentbox destroy`.
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
- Three bind mounts wire the box back to the host:
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`** — a Docker named volume (`agentbox-claude-config`, shared across boxes by default) seeded from the host's `~/.claude` on each create so auth, skills, and plugins persist without leaking the host's home dir.
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
- - 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`.
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
- - Host-only CLI wrappers (portless, etc.) must be bypassed, eg some projects wrap the dev server with a host-side proxy (here: `portless projectname next dev --turbopack`). Override the service command: to call the underlying tool directly (`next dev --turbopack`)
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.
@@ -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":[]}