@madarco/agentbox 0.5.0 → 0.7.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 (62) hide show
  1. package/dist/_cloud-attach-DMVH6GWO.js +12 -0
  2. package/dist/chunk-7KOEFGN2.js +1162 -0
  3. package/dist/chunk-7KOEFGN2.js.map +1 -0
  4. package/dist/chunk-I24B6AXR.js +600 -0
  5. package/dist/chunk-I24B6AXR.js.map +1 -0
  6. package/dist/chunk-NAVL4R34.js +7546 -0
  7. package/dist/chunk-NAVL4R34.js.map +1 -0
  8. package/dist/chunk-NW5NYTQM.js +1366 -0
  9. package/dist/chunk-NW5NYTQM.js.map +1 -0
  10. package/dist/chunk-UK72UQ5U.js +237 -0
  11. package/dist/chunk-UK72UQ5U.js.map +1 -0
  12. package/dist/chunk-V5KZGB5V.js +722 -0
  13. package/dist/chunk-V5KZGB5V.js.map +1 -0
  14. package/dist/cloud-poller-ZIWSADJB-JXFRJUEM.js +10 -0
  15. package/dist/dist-ETCFRVPA.js +423 -0
  16. package/dist/dist-QZGJIBT5.js +1339 -0
  17. package/dist/dist-QZGJIBT5.js.map +1 -0
  18. package/dist/dist-R67WMLCF.js +183 -0
  19. package/dist/dist-R67WMLCF.js.map +1 -0
  20. package/dist/index.js +4088 -1451
  21. package/dist/index.js.map +1 -1
  22. package/package.json +9 -4
  23. package/runtime/docker/Dockerfile.box +115 -19
  24. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +34 -19
  25. package/runtime/docker/packages/ctl/dist/bin.cjs +10246 -758
  26. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
  27. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +37 -0
  28. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
  29. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
  30. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
  31. package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
  32. package/runtime/hetzner/agentbox-codex-hooks.json +37 -0
  33. package/runtime/hetzner/agentbox-dockerd-start +132 -0
  34. package/runtime/hetzner/agentbox-open +28 -0
  35. package/runtime/hetzner/agentbox-setup-skill.md +196 -0
  36. package/runtime/hetzner/agentbox-vnc-start +77 -0
  37. package/runtime/hetzner/claude-managed-settings.json +54 -0
  38. package/runtime/hetzner/ctl.cjs +22350 -0
  39. package/runtime/hetzner/custom-system-CLAUDE.md +27 -0
  40. package/runtime/hetzner/scripts/install-box.sh +365 -0
  41. package/runtime/relay/bin.cjs +9182 -754
  42. package/share/agentbox-setup/SKILL.md +34 -19
  43. package/dist/chunk-6VTAPD4H.js +0 -507
  44. package/dist/chunk-6VTAPD4H.js.map +0 -1
  45. package/dist/chunk-7J5AJLWG.js +0 -238
  46. package/dist/chunk-7J5AJLWG.js.map +0 -1
  47. package/dist/chunk-FJNIFTWK.js +0 -523
  48. package/dist/chunk-FJNIFTWK.js.map +0 -1
  49. package/dist/chunk-HPZMD5DE.js +0 -106
  50. package/dist/chunk-HPZMD5DE.js.map +0 -1
  51. package/dist/chunk-PXUBE5KS.js +0 -2346
  52. package/dist/chunk-PXUBE5KS.js.map +0 -1
  53. package/dist/chunk-RFC5F5HR.js +0 -1709
  54. package/dist/chunk-RFC5F5HR.js.map +0 -1
  55. package/dist/create-AHZ3GVEZ-TGEDL7UX.js +0 -15
  56. package/dist/lifecycle-LFOL6YFM-TCHDX3J5.js +0 -38
  57. package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
  58. package/dist/stats-Z4BVJODD-HEC4TMUZ.js +0 -19
  59. package/dist/stats-Z4BVJODD-HEC4TMUZ.js.map +0 -1
  60. /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → _cloud-attach-DMVH6GWO.js.map} +0 -0
  61. /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → cloud-poller-ZIWSADJB-JXFRJUEM.js.map} +0 -0
  62. /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-ETCFRVPA.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/`.
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
 
@@ -158,24 +164,33 @@ services:
158
164
  - A service with `restart: never` and an autostart dependency will block the dependent forever after one failed run — usually a mistake.
159
165
  - `command:` is either a shell string (run via `bash -c`) or an argv array. Use the argv form if you need to avoid shell quoting.
160
166
 
161
- ## 8. Hand-off
167
+ ## 8. Checkpoint the warm state (do this at the very end)
168
+
169
+ Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --name setup --replace --set-default` so future boxes start ready.
170
+ Run this command exactly once. The `--name setup --replace` makes it idempotent — if it ever needs to run again it overwrites the existing `setup` checkpoint instead of stacking duplicates.
171
+ This doesn't need to be confirmed by the user.
172
+ It will pause the container for several seconds so warn the user about it and write Done when it's done.
173
+
174
+ ## 9. Hand-off
162
175
 
163
- 1. Write the file to `/workspace/agentbox.yaml`.
164
- 2. **Apply it live**: from inside the box run `agentbox-ctl reload`. The already-running supervisor re-reads the config and immediately runs the declared tasks and autostarts the services — no box restart needed. It prints the `added` / `removed` / `changed` diff. If it errors because the daemon isn't running, the config is still valid: the next `agentbox start` (or `agentbox create` in this workspace) picks it up automatically.
165
- 3. Confirm with `agentbox-ctl status`: tasks should be `running` or `done`, autostart services `starting` or `ready`. If something failed, tail it with `agentbox-ctl logs <service>` and fix the config, then `agentbox-ctl reload` again.
166
- 4. Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --set-default` so future boxes start ready.
176
+ Tell the user (verbatim):
167
177
 
168
- 5. Tell the user:
178
+ ```
179
+ █████╗ ██████╗ ███████╗███╗ ██╗████████╗██████╗ ██████╗ ██╗ ██╗
180
+ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██╔═══██╗╚██╗██╔╝
181
+ ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██████╔╝██║ ██║ ╚███╔╝
182
+ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██╔══██╗██║ ██║ ██╔██╗
183
+ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ██████╔╝╚██████╔╝██╔╝ ██╗
184
+ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
185
+ ```
169
186
 
170
- > I wrote `/workspace/agentbox.yaml` and ran `agentbox-ctl reload` so the supervisor is already running the declared tasks/services. To land the file on the host:
171
- > - I've created a checkpoint of the warm box state so future boxes start ready in seconds, no reinstall.
172
- > - commit it inside the box (`git add agentbox.yaml && git commit -m 'add agentbox config'`) — the box's `.git/` is bind-mounted, so the commit shows up on the host immediately; or
173
- > - on the host, tell the user to run `agentbox download config` to update their original host workspace.
187
+ your box is ready, you can start more sessions with `agentbox claude`
188
+ you can access the web app at https://<boxname>.localhost
174
189
 
175
- ## 9. Known issues
190
+ ## 10. Known issues
176
191
 
177
192
  - For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
178
193
 
179
- - 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.
180
195
 
181
- - 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`.
@@ -1,507 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- ConfigError,
4
- VNC_CONTAINER_PORT,
5
- WEB_CONTAINER_PORT,
6
- bindWorktrees,
7
- buildClaudeMounts,
8
- buildIdeMounts,
9
- chownGitBindParents,
10
- collectRepoCarryOver,
11
- createSnapshot,
12
- cursorServerVolumeName,
13
- detectGitRepos,
14
- dockerVolumeName,
15
- ensureClaudeVolume,
16
- ensureIdeVolumes,
17
- ensureRelay,
18
- generateRelayToken,
19
- generateVncPassword,
20
- gitWorktreePathFor,
21
- launchCtlDaemon,
22
- launchDockerdDaemon,
23
- launchVncDaemon,
24
- loadConfig,
25
- pickFreshBranch,
26
- registerBoxWithRelay,
27
- rehydrateRelayRegistry,
28
- repairIdeOwnership,
29
- resolveClaudeVolume,
30
- seedSetupSkillIntoVolume,
31
- seedWorkspace,
32
- seedWorkspaceFromDir,
33
- snapshotPathFor,
34
- vscodeServerVolumeName
35
- } from "./chunk-PXUBE5KS.js";
36
- import {
37
- allocateProjectIndex,
38
- readState,
39
- recordBox
40
- } from "./chunk-HPZMD5DE.js";
41
- import {
42
- CONTAINER_EXPORT_MERGED,
43
- DEFAULT_BOX_IMAGE,
44
- DEFAULT_ENV_PATTERNS,
45
- boxRunDirFor,
46
- containerExists,
47
- copyHostEnvFilesToBox,
48
- copyHostFilesToBox,
49
- dockerInfo,
50
- dockerStorageDriver,
51
- ensureImage,
52
- ensureVolume,
53
- publishedHostPort,
54
- resolveCheckpoint,
55
- runBox
56
- } from "./chunk-RFC5F5HR.js";
57
-
58
- // ../../packages/sandbox-docker/dist/chunk-NCSJPHDB.js
59
- import { randomBytes } from "crypto";
60
- import { mkdir, stat } from "fs/promises";
61
- import { homedir } from "os";
62
- import { basename, join, resolve } from "path";
63
- import { execa as execa2 } from "execa";
64
- import { execa } from "execa";
65
- async function writeBoxEnvFile(container, env) {
66
- const body = formatBoxEnvBody(env);
67
- const result = await execa(
68
- "docker",
69
- ["exec", "--user", "root", "-i", container, "sh", "-c", "umask 022 && cat > /etc/agentbox/box.env"],
70
- { input: body, reject: false }
71
- );
72
- if (result.exitCode !== 0) {
73
- return {
74
- ok: false,
75
- reason: `docker exec failed (exit ${String(result.exitCode)}): ${(result.stderr ?? "").toString().slice(0, 400)}`
76
- };
77
- }
78
- return { ok: true };
79
- }
80
- function formatBoxEnvBody(env) {
81
- const lines = [];
82
- for (const [k, v] of Object.entries(env)) {
83
- lines.push(`${k}=${shellSingleQuote(v)}`);
84
- }
85
- return lines.join("\n") + "\n";
86
- }
87
- function shellSingleQuote(s) {
88
- return `'${s.replace(/'/g, `'\\''`)}'`;
89
- }
90
- function persistableLimits(lim) {
91
- if (!lim) return void 0;
92
- const out = {};
93
- if (lim.memoryBytes && lim.memoryBytes > 0) out.memoryBytes = Math.floor(lim.memoryBytes);
94
- if (lim.cpus && lim.cpus > 0) out.cpus = lim.cpus;
95
- if (lim.pidsLimit && lim.pidsLimit > 0) out.pidsLimit = Math.floor(lim.pidsLimit);
96
- if (lim.disk) out.disk = lim.disk;
97
- return Object.keys(out).length > 0 ? out : void 0;
98
- }
99
- function generateBoxId() {
100
- return randomBytes(4).toString("hex");
101
- }
102
- function sanitizeBasename(workspacePath) {
103
- const raw = basename(resolve(workspacePath));
104
- return raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-._]+|[-._]+$/g, "").slice(0, 30).replace(/[-._]+$/, "");
105
- }
106
- function defaultBoxName(workspacePath, id) {
107
- const base = sanitizeBasename(workspacePath);
108
- return base.length > 0 ? `${base}-${id}` : id;
109
- }
110
- async function pathExists(p) {
111
- try {
112
- await stat(p);
113
- return true;
114
- } catch {
115
- return false;
116
- }
117
- }
118
- async function buildIdentityMounts() {
119
- const home = homedir();
120
- const candidates = [
121
- { src: join(home, ".codex"), dst: "/home/vscode/.codex", readOnly: false },
122
- { src: join(home, ".gitconfig"), dst: "/home/vscode/.gitconfig", readOnly: true }
123
- ];
124
- const out = [];
125
- for (const c of candidates) {
126
- if (await pathExists(c.src)) {
127
- out.push(`${c.src}:${c.dst}${c.readOnly ? ":ro" : ""}`);
128
- }
129
- }
130
- return out;
131
- }
132
- async function createBox(opts) {
133
- const log = opts.onLog ?? (() => {
134
- });
135
- const workspace = resolve(opts.workspacePath);
136
- if (!await pathExists(workspace)) {
137
- throw new Error(`workspace does not exist: ${workspace}`);
138
- }
139
- const cfgPath = join(workspace, "agentbox.yaml");
140
- if (await pathExists(cfgPath)) {
141
- try {
142
- const cfg = await loadConfig(cfgPath);
143
- log(`agentbox.yaml validated (${String(cfg.services.length)} service(s))`);
144
- } catch (err) {
145
- if (err instanceof ConfigError) {
146
- throw new Error(`agentbox.yaml validation failed:
147
- ${err.message}`);
148
- }
149
- throw err;
150
- }
151
- }
152
- await dockerInfo();
153
- log("docker daemon reachable");
154
- let checkpointImage;
155
- let checkpointSource;
156
- let restoredWorktrees;
157
- if (opts.checkpointRef) {
158
- const projectRootForCkpt = opts.projectRoot ?? workspace;
159
- const head = await resolveCheckpoint(projectRootForCkpt, opts.checkpointRef);
160
- if (!head) {
161
- throw new Error(`checkpoint not found: ${opts.checkpointRef}`);
162
- }
163
- checkpointImage = head.manifest.image;
164
- const chain = [head.name, ...head.manifest.parents];
165
- checkpointSource = { ref: opts.checkpointRef, type: head.manifest.type, chain };
166
- restoredWorktrees = head.manifest.worktrees;
167
- log(
168
- `starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`
169
- );
170
- }
171
- const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;
172
- const ensureRef = checkpointImage ? opts.image ?? DEFAULT_BOX_IMAGE : imageRef;
173
- const { built } = await ensureImage(ensureRef, {
174
- onProgress: (line) => log(`[image] ${line}`)
175
- });
176
- log(built ? `built image ${ensureRef}` : `using cached image ${imageRef}`);
177
- let relayUp = false;
178
- try {
179
- await ensureRelay({ onLog: log });
180
- const existing = await readState();
181
- await rehydrateRelayRegistry(existing.boxes);
182
- relayUp = true;
183
- } catch (err) {
184
- log(`relay unavailable: ${err instanceof Error ? err.message : String(err)}`);
185
- }
186
- const id = generateBoxId();
187
- const name = opts.name ?? defaultBoxName(workspace, id);
188
- const containerName = `agentbox-${name}`;
189
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
190
- if (await containerExists(containerName)) {
191
- throw new Error(`container ${containerName} already exists; remove it first`);
192
- }
193
- let projectIndex;
194
- if (opts.projectRoot) {
195
- projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);
196
- }
197
- const repoCarryOvers = [];
198
- const gitWorktreeRecords = [];
199
- if (checkpointImage && restoredWorktrees && restoredWorktrees.length > 0) {
200
- gitWorktreeRecords.push(...restoredWorktrees);
201
- }
202
- if (!checkpointImage) {
203
- const repos = await detectGitRepos(workspace);
204
- if (repos.length > 0) {
205
- log(
206
- `detected ${String(repos.length)} git repo(s): ` + repos.map((r) => `${r.kind}${r.relPathFromWorkspace ? "@" + r.relPathFromWorkspace : ""}`).join(", ")
207
- );
208
- }
209
- for (const r of repos) {
210
- const branchBase = r.kind === "root" ? `agentbox/${name}` : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, "_")}`;
211
- const branch = await pickFreshBranch(r.hostMainRepo, branchBase);
212
- const containerPath = r.kind === "root" ? "/workspace" : `/workspace/${r.relPathFromWorkspace}`;
213
- const gitWorktreePath = gitWorktreePathFor(branch);
214
- const carry = await collectRepoCarryOver(r, branch, containerPath, gitWorktreePath);
215
- repoCarryOvers.push(carry);
216
- gitWorktreeRecords.push({
217
- kind: r.kind,
218
- hostMainRepo: r.hostMainRepo,
219
- containerPath,
220
- gitWorktreePath,
221
- branch,
222
- relPathFromWorkspace: r.relPathFromWorkspace
223
- });
224
- }
225
- }
226
- let snapshotDir = null;
227
- const snapshotIsUseful = !checkpointImage && repoCarryOvers.length === 0;
228
- if (opts.useSnapshot && snapshotIsUseful) {
229
- snapshotDir = snapshotPathFor({ id, name, projectIndex });
230
- log(`cloning workspace to ${snapshotDir} (APFS clone where available)`);
231
- const snap = await createSnapshot({ source: workspace, destination: snapshotDir });
232
- log(`pruned ${snap.prunedPaths.length} platform-dependent dirs from snapshot`);
233
- } else if (opts.useSnapshot && !checkpointImage) {
234
- log("skipping --host-snapshot: git worktree path reads content from .git, not from a workspace clone");
235
- }
236
- await ensureIdeVolumes(id);
237
- const dockerCacheShared = opts.docker?.sharedCache === true;
238
- const dockerVolume = dockerVolumeName(id, dockerCacheShared);
239
- await ensureVolume(dockerVolume);
240
- log(`prepared volumes ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`);
241
- const ide = buildIdeMounts(id);
242
- const claudeSpec = resolveClaudeVolume({
243
- isolate: opts.claudeConfig?.isolate ?? false,
244
- boxId: id
245
- });
246
- const claudeEnsured = await ensureClaudeVolume(claudeSpec, {
247
- syncFromHost: true,
248
- image: ensureRef,
249
- hostWorkspace: workspace
250
- });
251
- if (claudeEnsured.synced) {
252
- log(`synced ${claudeSpec.volume} from ~/.claude`);
253
- if ((claudeEnsured.filteredHookCount ?? 0) > 0) {
254
- log(
255
- `filtered ${String(claudeEnsured.filteredHookCount)} host-path hook(s) (paths under ~/)`
256
- );
257
- }
258
- if (claudeEnsured.clearedInstallMethod) {
259
- log("cleared host's installMethod from synced .claude.json (box uses the native installer)");
260
- }
261
- if (claudeEnsured.aliasedProjectKey) {
262
- log(`aliased project state for ${workspace} -> /workspace in synced .claude.json`);
263
- }
264
- } else if (claudeEnsured.created) {
265
- log(`created empty volume ${claudeSpec.volume} (no host ~/.claude to sync)`);
266
- } else {
267
- log(`reusing volume ${claudeSpec.volume} (no host ~/.claude to sync)`);
268
- }
269
- const seeded = await seedSetupSkillIntoVolume(claudeSpec.volume, ensureRef);
270
- if (seeded.seeded) log(`seeded /agentbox-setup skill into ${claudeSpec.volume}`);
271
- const claudeMounts = buildClaudeMounts(claudeSpec, process.env);
272
- const boxDir = boxRunDirFor({ id, name, projectIndex });
273
- const socketDir = join(boxDir, "run");
274
- const socketPath = join(socketDir, "ctl.sock");
275
- const mergedExportDir = join(boxDir, "workspace");
276
- await mkdir(socketDir, { recursive: true });
277
- await mkdir(mergedExportDir, { recursive: true });
278
- const extraVolumes = await buildIdentityMounts();
279
- extraVolumes.push(...claudeMounts.extraVolumes);
280
- extraVolumes.push(...ide.extraVolumes);
281
- extraVolumes.push(`${socketDir}:/run/agentbox`);
282
- extraVolumes.push(`${mergedExportDir}:${CONTAINER_EXPORT_MERGED}`);
283
- extraVolumes.push(`${dockerVolume}:/var/lib/docker`);
284
- for (const w of gitWorktreeRecords) {
285
- extraVolumes.push(`${w.hostMainRepo}/.git:${w.hostMainRepo}/.git`);
286
- }
287
- for (const v of extraVolumes) log(`mounting agent dir: ${v}`);
288
- const relayToken = generateRelayToken();
289
- if (relayUp) {
290
- try {
291
- await registerBoxWithRelay({
292
- boxId: id,
293
- token: relayToken,
294
- name,
295
- containerName,
296
- createdAt,
297
- projectIndex,
298
- worktrees: gitWorktreeRecords
299
- });
300
- log(`registered box token with relay`);
301
- } catch (err) {
302
- log(`relay register failed: ${err instanceof Error ? err.message : String(err)}`);
303
- relayUp = false;
304
- }
305
- }
306
- const relayEnv = relayUp ? {
307
- // host.docker.internal resolves to the host (where the relay node
308
- // process is running). The matching `--add-host` is set in runBox.
309
- AGENTBOX_RELAY_URL: `http://host.docker.internal:8787`,
310
- AGENTBOX_RELAY_TOKEN: relayToken
311
- } : {};
312
- const vncEnabled = opts.vnc?.enabled !== false;
313
- const vncPassword = vncEnabled ? generateVncPassword() : void 0;
314
- const vncEnv = vncEnabled && vncPassword ? { AGENTBOX_VNC_PASSWORD: vncPassword } : {};
315
- const vncPortMappings = vncEnabled ? [{ hostPort: 0, containerPort: VNC_CONTAINER_PORT, hostIp: "127.0.0.1" }] : [];
316
- const webPortMappings = [
317
- { hostPort: 0, containerPort: WEB_CONTAINER_PORT, hostIp: "127.0.0.1" }
318
- ];
319
- const agentboxEnv = {
320
- AGENTBOX: "1",
321
- AGENTBOX_BOX_NAME: name,
322
- AGENTBOX_HOST_WORKSPACE: workspace,
323
- ...opts.projectRoot ? { AGENTBOX_PROJECT_ROOT: opts.projectRoot } : {},
324
- ...projectIndex !== void 0 ? { AGENTBOX_PROJECT_INDEX: String(projectIndex) } : {}
325
- };
326
- const boxEnvForFile = {
327
- AGENTBOX_BOX_ID: id,
328
- ...agentboxEnv
329
- };
330
- const appliedLimits = opts.limits;
331
- let effectiveLimits = appliedLimits;
332
- if (appliedLimits?.disk) {
333
- const driver = await dockerStorageDriver();
334
- if (!/^(devicemapper|btrfs|zfs|windowsfilter)$/.test(driver)) {
335
- log(
336
- `warning: --disk/box.disk is a no-op on this engine (storage-driver=${driver || "unknown"}); ignoring`
337
- );
338
- effectiveLimits = { ...appliedLimits, disk: null };
339
- }
340
- }
341
- await runBox({
342
- name: containerName,
343
- image: imageRef,
344
- extraVolumes,
345
- limits: effectiveLimits,
346
- portMappings: [...vncPortMappings, ...webPortMappings],
347
- env: {
348
- AGENTBOX_BOX_ID: id,
349
- ...agentboxEnv,
350
- ...claudeMounts.env,
351
- ...relayEnv,
352
- ...vncEnv,
353
- ...opts.claudeEnv ?? {}
354
- }
355
- });
356
- log(`container ${containerName} started`);
357
- if (gitWorktreeRecords.length > 0) {
358
- await chownGitBindParents({
359
- container: containerName,
360
- hostMainRepos: gitWorktreeRecords.map((w) => w.hostMainRepo),
361
- onLog: log
362
- });
363
- }
364
- const boxEnv = await writeBoxEnvFile(containerName, boxEnvForFile);
365
- if (boxEnv.ok) log("wrote /etc/agentbox/box.env");
366
- else log(`writing /etc/agentbox/box.env failed: ${boxEnv.reason}`);
367
- if (!checkpointImage) {
368
- if (repoCarryOvers.length > 0) {
369
- try {
370
- await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });
371
- log("seeded /workspace from in-container git worktree(s)");
372
- } catch (err) {
373
- log(
374
- `seedWorkspace failed; leaving ${containerName} running so you can inspect it`
375
- );
376
- throw err;
377
- }
378
- } else {
379
- const source = snapshotDir ?? workspace;
380
- await seedWorkspaceFromDir({ container: containerName, hostSource: source, onLog: log });
381
- }
382
- } else if (restoredWorktrees && restoredWorktrees.length > 0) {
383
- await bindWorktrees(
384
- containerName,
385
- restoredWorktrees.map((w) => ({
386
- kind: w.kind,
387
- containerPath: w.containerPath,
388
- gitWorktreePath: w.gitWorktreePath
389
- })),
390
- log
391
- );
392
- log("re-bound /workspace from checkpoint image");
393
- } else {
394
- log("using /workspace from checkpoint image (no worktrees recorded; no rebind)");
395
- }
396
- await repairIdeOwnership(containerName);
397
- log(".vscode-server + .cursor-server ownership verified");
398
- const ctl = await launchCtlDaemon(containerName, socketPath);
399
- if (ctl.up) log("agentbox-ctl daemon up");
400
- else log(`agentbox-ctl daemon did not become reachable: ${ctl.reason}`);
401
- const dockerd = await launchDockerdDaemon(containerName);
402
- if (dockerd.up) {
403
- log(`dockerd up (storage-driver=fuse-overlayfs, data root=${dockerVolume})`);
404
- } else {
405
- log(`dockerd did not become ready: ${dockerd.reason}`);
406
- }
407
- if (opts.withPlaywright) {
408
- log("installing @playwright/cli@latest (--with-playwright)");
409
- const result = await execa2(
410
- "docker",
411
- [
412
- "exec",
413
- "--user",
414
- "root",
415
- containerName,
416
- "bash",
417
- "-lc",
418
- "npm install -g @playwright/cli@latest 2>&1"
419
- ],
420
- { reject: false }
421
- );
422
- for (const line of (result.stdout ?? "").split("\n")) {
423
- if (line.trim().length > 0) log(`[playwright] ${line}`);
424
- }
425
- if (result.exitCode !== 0) {
426
- throw new Error(
427
- `failed to install @playwright/cli (exit ${String(result.exitCode)}): ${(result.stderr ?? "").toString().slice(0, 400)}`
428
- );
429
- }
430
- log("@playwright/cli installed");
431
- }
432
- if (opts.withEnv) {
433
- log("copying host env/config files into /workspace (--with-env)");
434
- const { copied } = await copyHostEnvFilesToBox({
435
- container: containerName,
436
- workspaceDir: workspace,
437
- patterns: DEFAULT_ENV_PATTERNS,
438
- onLog: log
439
- });
440
- log(copied > 0 ? `copied ${String(copied)} env/config file(s)` : "no env/config files found");
441
- }
442
- if (opts.envFilesToImport && opts.envFilesToImport.length > 0) {
443
- log(`copying ${String(opts.envFilesToImport.length)} selected env/config file(s) into /workspace`);
444
- const { copied } = await copyHostFilesToBox({
445
- container: containerName,
446
- workspaceDir: workspace,
447
- files: opts.envFilesToImport,
448
- onLog: log
449
- });
450
- if (copied !== opts.envFilesToImport.length) {
451
- log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);
452
- }
453
- }
454
- let vncHostPort = null;
455
- if (vncEnabled) {
456
- const vnc = await launchVncDaemon(containerName);
457
- if (vnc.up) log("vnc stack up (Xvnc + websockify + noVNC)");
458
- else log(`vnc stack did not become reachable: ${vnc.reason}`);
459
- vncHostPort = await publishedHostPort(containerName, VNC_CONTAINER_PORT);
460
- if (vncHostPort) log(`vnc web on host 127.0.0.1:${String(vncHostPort)}`);
461
- }
462
- const webHostPort = await publishedHostPort(containerName, WEB_CONTAINER_PORT);
463
- if (webHostPort) {
464
- log(
465
- `web port reserved on host 127.0.0.1:${String(webHostPort)} (forwards to the web service once agentbox.yaml sets a service expose:)`
466
- );
467
- }
468
- const record = {
469
- id,
470
- name,
471
- container: containerName,
472
- image: imageRef,
473
- workspacePath: workspace,
474
- snapshotDir,
475
- socketPath,
476
- claudeConfigVolume: claudeSpec.volume,
477
- vscodeServerVolume: vscodeServerVolumeName(id),
478
- cursorServerVolume: cursorServerVolumeName(id),
479
- relayToken: relayUp ? relayToken : void 0,
480
- gitWorktrees: gitWorktreeRecords.length > 0 ? gitWorktreeRecords : void 0,
481
- withPlaywright: opts.withPlaywright ? true : void 0,
482
- withEnv: opts.withEnv ? true : void 0,
483
- vncEnabled: vncEnabled ? true : void 0,
484
- vncContainerPort: vncEnabled ? VNC_CONTAINER_PORT : void 0,
485
- vncHostPort: vncHostPort ?? void 0,
486
- vncPassword,
487
- webContainerPort: WEB_CONTAINER_PORT,
488
- webHostPort: webHostPort ?? void 0,
489
- dockerVolume,
490
- dockerCacheShared: dockerCacheShared || void 0,
491
- projectRoot: opts.projectRoot,
492
- projectIndex,
493
- checkpointImage,
494
- checkpointSource,
495
- resourceLimits: persistableLimits(effectiveLimits),
496
- createdAt
497
- };
498
- await recordBox(record);
499
- return { record, imageBuilt: built };
500
- }
501
-
502
- export {
503
- sanitizeBasename,
504
- defaultBoxName,
505
- createBox
506
- };
507
- //# sourceMappingURL=chunk-6VTAPD4H.js.map