@madarco/agentbox 0.5.0 → 0.6.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/{chunk-7J5AJLWG.js → chunk-BBZMA2K6.js} +3 -3
- package/dist/{chunk-RFC5F5HR.js → chunk-HHMWQNLF.js} +8 -8
- package/dist/chunk-HHMWQNLF.js.map +1 -0
- package/dist/{chunk-PXUBE5KS.js → chunk-HTTKML3C.js} +351 -42
- package/dist/chunk-HTTKML3C.js.map +1 -0
- package/dist/{chunk-6VTAPD4H.js → chunk-KJNZP6I3.js} +100 -21
- package/dist/chunk-KJNZP6I3.js.map +1 -0
- package/dist/{chunk-FJNIFTWK.js → chunk-M7I247BK.js} +6 -4
- package/dist/chunk-M7I247BK.js.map +1 -0
- package/dist/{create-AHZ3GVEZ-TGEDL7UX.js → create-6PWXI6HO-OWAMHBAK.js} +4 -4
- package/dist/index.js +310 -102
- package/dist/index.js.map +1 -1
- package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js → lifecycle-EMXR46DI-DUVBXNTV.js} +4 -4
- package/dist/{stats-Z4BVJODD-HEC4TMUZ.js → stats-SZXOJE3D-N7OODCHW.js} +3 -3
- package/package.json +4 -4
- package/runtime/docker/Dockerfile.box +23 -11
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +19 -11
- package/runtime/docker/packages/ctl/dist/bin.cjs +56 -15
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
- package/runtime/relay/bin.cjs +121 -2
- package/share/agentbox-setup/SKILL.md +19 -11
- package/dist/chunk-6VTAPD4H.js.map +0 -1
- package/dist/chunk-FJNIFTWK.js.map +0 -1
- package/dist/chunk-PXUBE5KS.js.map +0 -1
- package/dist/chunk-RFC5F5HR.js.map +0 -1
- /package/dist/{chunk-7J5AJLWG.js.map → chunk-BBZMA2K6.js.map} +0 -0
- /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
- /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
- /package/dist/{stats-Z4BVJODD-HEC4TMUZ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
package/runtime/relay/bin.cjs
CHANGED
|
@@ -10381,8 +10381,69 @@ var RELAY_EVENT_RING_SIZE = 1e3;
|
|
|
10381
10381
|
var import_node_child_process = require("child_process");
|
|
10382
10382
|
var import_node_http = require("http");
|
|
10383
10383
|
|
|
10384
|
-
// src/
|
|
10384
|
+
// src/notices.ts
|
|
10385
10385
|
var import_node_crypto = require("crypto");
|
|
10386
|
+
var DEFAULT_NOTICE_TTL_MS = 66e4;
|
|
10387
|
+
var BoxNotices = class {
|
|
10388
|
+
constructor(subscribers) {
|
|
10389
|
+
this.subscribers = subscribers;
|
|
10390
|
+
}
|
|
10391
|
+
subscribers;
|
|
10392
|
+
/** keyed by notice id. */
|
|
10393
|
+
entries = /* @__PURE__ */ new Map();
|
|
10394
|
+
/**
|
|
10395
|
+
* Register a notice for `boxId` and broadcast `notice-set`. At most one
|
|
10396
|
+
* notice per (box, kind) is kept — a fresh `set` for the same kind
|
|
10397
|
+
* replaces the previous one (and cancels its TTL timer so a stale timer
|
|
10398
|
+
* can't later fire a `notice-clear` racing the replacement). Returns the
|
|
10399
|
+
* generated notice id.
|
|
10400
|
+
*/
|
|
10401
|
+
set(boxId, kind, message, ttlMs) {
|
|
10402
|
+
for (const [id, entry] of this.entries) {
|
|
10403
|
+
if (entry.boxId === boxId && entry.ev.kind === kind) {
|
|
10404
|
+
clearTimeout(entry.timer);
|
|
10405
|
+
this.entries.delete(id);
|
|
10406
|
+
}
|
|
10407
|
+
}
|
|
10408
|
+
const ev = { id: (0, import_node_crypto.randomUUID)(), kind, message };
|
|
10409
|
+
const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;
|
|
10410
|
+
const timer = setTimeout(() => {
|
|
10411
|
+
if (this.entries.delete(ev.id)) {
|
|
10412
|
+
this.subscribers.broadcast(boxId, "notice-clear", { id: ev.id });
|
|
10413
|
+
}
|
|
10414
|
+
}, ttl);
|
|
10415
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
10416
|
+
this.entries.set(ev.id, { ev, boxId, timer });
|
|
10417
|
+
this.subscribers.broadcast(boxId, "notice-set", ev);
|
|
10418
|
+
return ev.id;
|
|
10419
|
+
}
|
|
10420
|
+
/**
|
|
10421
|
+
* Clear a notice by id. Idempotent: returns false when no such notice
|
|
10422
|
+
* exists (already cleared / expired). Broadcasts `notice-clear` on a hit.
|
|
10423
|
+
*/
|
|
10424
|
+
clear(id) {
|
|
10425
|
+
const entry = this.entries.get(id);
|
|
10426
|
+
if (!entry) return false;
|
|
10427
|
+
clearTimeout(entry.timer);
|
|
10428
|
+
this.entries.delete(id);
|
|
10429
|
+
this.subscribers.broadcast(entry.boxId, "notice-clear", { id });
|
|
10430
|
+
return true;
|
|
10431
|
+
}
|
|
10432
|
+
/** Snapshot of active notices for a box; replayed to a new SSE subscriber. */
|
|
10433
|
+
forBox(boxId) {
|
|
10434
|
+
const out = [];
|
|
10435
|
+
for (const entry of this.entries.values()) {
|
|
10436
|
+
if (entry.boxId === boxId) out.push(entry.ev);
|
|
10437
|
+
}
|
|
10438
|
+
return out;
|
|
10439
|
+
}
|
|
10440
|
+
size() {
|
|
10441
|
+
return this.entries.size;
|
|
10442
|
+
}
|
|
10443
|
+
};
|
|
10444
|
+
|
|
10445
|
+
// src/prompts.ts
|
|
10446
|
+
var import_node_crypto2 = require("crypto");
|
|
10386
10447
|
var PendingPrompts = class {
|
|
10387
10448
|
entries = /* @__PURE__ */ new Map();
|
|
10388
10449
|
add(boxId, ev) {
|
|
@@ -10471,7 +10532,7 @@ async function askPrompt(prompts, subscribers, boxId, params) {
|
|
|
10471
10532
|
if (process.env.AGENTBOX_PROMPT === "off") {
|
|
10472
10533
|
return { answer: "y" };
|
|
10473
10534
|
}
|
|
10474
|
-
const ev = { id: (0,
|
|
10535
|
+
const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
|
|
10475
10536
|
const promise = prompts.add(boxId, ev);
|
|
10476
10537
|
subscribers.broadcast(boxId, "prompt-ask", ev);
|
|
10477
10538
|
return promise;
|
|
@@ -10605,6 +10666,7 @@ var GIT_RPC_TIMEOUT_MS = 12e4;
|
|
|
10605
10666
|
var CHECKPOINT_RPC_TIMEOUT_MS = 6e5;
|
|
10606
10667
|
var DOWNLOAD_RPC_TIMEOUT_MS = 6e5;
|
|
10607
10668
|
var CP_RPC_TIMEOUT_MS = 3e5;
|
|
10669
|
+
var BROWSER_OPEN_RPC_TIMEOUT_MS = 15e3;
|
|
10608
10670
|
var SSE_HEARTBEAT_MS = 15e3;
|
|
10609
10671
|
function send(res, status, body, contentType = "application/json") {
|
|
10610
10672
|
const text = body == null ? "" : typeof body === "string" ? body : JSON.stringify(body);
|
|
@@ -10663,6 +10725,7 @@ function createRelayServer(opts) {
|
|
|
10663
10725
|
const statusStore = new BoxStatusStore();
|
|
10664
10726
|
const prompts = new PendingPrompts();
|
|
10665
10727
|
const subscribers = new PromptSubscribers();
|
|
10728
|
+
const notices = new BoxNotices(subscribers);
|
|
10666
10729
|
const host = opts.host ?? "0.0.0.0";
|
|
10667
10730
|
const server = (0, import_node_http.createServer)((req, res) => {
|
|
10668
10731
|
handle(req, res).catch((err) => {
|
|
@@ -10803,6 +10866,23 @@ function createRelayServer(opts) {
|
|
|
10803
10866
|
send(res, status, result);
|
|
10804
10867
|
return;
|
|
10805
10868
|
}
|
|
10869
|
+
if (body.method === "browser.open") {
|
|
10870
|
+
const params = body.params;
|
|
10871
|
+
const url2 = typeof params?.url === "string" ? params.url.trim() : "";
|
|
10872
|
+
if (!isOpenableUrl(url2)) {
|
|
10873
|
+
send(res, 400, {
|
|
10874
|
+
exitCode: 64,
|
|
10875
|
+
stdout: "",
|
|
10876
|
+
stderr: "browser.open: only http/https URLs are allowed\n"
|
|
10877
|
+
});
|
|
10878
|
+
return;
|
|
10879
|
+
}
|
|
10880
|
+
events.append({ boxId: reg.boxId, type: "browser-open", payload: { url: url2 } });
|
|
10881
|
+
const result = await runHostCommand(["open", url2], BROWSER_OPEN_RPC_TIMEOUT_MS);
|
|
10882
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
10883
|
+
send(res, status, result);
|
|
10884
|
+
return;
|
|
10885
|
+
}
|
|
10806
10886
|
events.append({
|
|
10807
10887
|
boxId: reg.boxId,
|
|
10808
10888
|
type: "rpc-unknown",
|
|
@@ -10896,6 +10976,12 @@ function createRelayServer(opts) {
|
|
|
10896
10976
|
res.write(`event: prompt-ask
|
|
10897
10977
|
data: ${JSON.stringify(ev)}
|
|
10898
10978
|
|
|
10979
|
+
`);
|
|
10980
|
+
}
|
|
10981
|
+
for (const ev of notices.forBox(boxId)) {
|
|
10982
|
+
res.write(`event: notice-set
|
|
10983
|
+
data: ${JSON.stringify(ev)}
|
|
10984
|
+
|
|
10899
10985
|
`);
|
|
10900
10986
|
}
|
|
10901
10987
|
const heartbeat = setInterval(() => {
|
|
@@ -10932,6 +11018,29 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
10932
11018
|
send(res, 204, null);
|
|
10933
11019
|
return;
|
|
10934
11020
|
}
|
|
11021
|
+
if (route === "POST /admin/notices/set") {
|
|
11022
|
+
const body = await readJsonBody(req);
|
|
11023
|
+
if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.kind !== "string" || body.kind.length === 0 || typeof body.message !== "string" || body.message.length === 0) {
|
|
11024
|
+
send(res, 400, { error: "expected {boxId, kind, message}" });
|
|
11025
|
+
return;
|
|
11026
|
+
}
|
|
11027
|
+
const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
|
|
11028
|
+
const id = notices.set(body.boxId, body.kind, body.message, ttlMs);
|
|
11029
|
+
log(`notice-set box=${body.boxId} kind=${body.kind} id=${id}`);
|
|
11030
|
+
send(res, 200, { id });
|
|
11031
|
+
return;
|
|
11032
|
+
}
|
|
11033
|
+
if (route === "POST /admin/notices/clear") {
|
|
11034
|
+
const body = await readJsonBody(req);
|
|
11035
|
+
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
11036
|
+
send(res, 400, { error: "expected {boxId, id}" });
|
|
11037
|
+
return;
|
|
11038
|
+
}
|
|
11039
|
+
notices.clear(body.id);
|
|
11040
|
+
log(`notice-clear id=${body.id}`);
|
|
11041
|
+
send(res, 204, null);
|
|
11042
|
+
return;
|
|
11043
|
+
}
|
|
10935
11044
|
send(res, 404, { error: "not found", route });
|
|
10936
11045
|
}
|
|
10937
11046
|
function authBox(req, res, reg) {
|
|
@@ -10954,6 +11063,7 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
10954
11063
|
statusStore,
|
|
10955
11064
|
prompts,
|
|
10956
11065
|
subscribers,
|
|
11066
|
+
notices,
|
|
10957
11067
|
url: `http://${host}:${String(opts.port)}`,
|
|
10958
11068
|
close: () => new Promise((resolve2, reject) => {
|
|
10959
11069
|
server.close((err) => {
|
|
@@ -11048,6 +11158,15 @@ async function handleCheckpointRpc(reg, params) {
|
|
|
11048
11158
|
if (params?.replace === true) argv.push("--replace");
|
|
11049
11159
|
return runHostCommand(argv, CHECKPOINT_RPC_TIMEOUT_MS);
|
|
11050
11160
|
}
|
|
11161
|
+
function isOpenableUrl(value) {
|
|
11162
|
+
let url;
|
|
11163
|
+
try {
|
|
11164
|
+
url = new URL(value);
|
|
11165
|
+
} catch {
|
|
11166
|
+
return false;
|
|
11167
|
+
}
|
|
11168
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
11169
|
+
}
|
|
11051
11170
|
function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
|
|
11052
11171
|
return new Promise((resolve2) => {
|
|
11053
11172
|
const [cmd, ...rest] = argv;
|
|
@@ -158,21 +158,29 @@ services:
|
|
|
158
158
|
- A service with `restart: never` and an autostart dependency will block the dependent forever after one failed run — usually a mistake.
|
|
159
159
|
- `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
160
|
|
|
161
|
-
## 8.
|
|
161
|
+
## 8. Checkpoint the warm state (do this at the very end)
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
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.
|
|
164
|
+
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.
|
|
165
|
+
This doesn't need to be confirmed by the user.
|
|
166
|
+
It will pause the container for several seconds so warn the user about it and write Done when it's done.
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
## 9. Hand-off
|
|
169
169
|
|
|
170
|
-
|
|
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.
|
|
170
|
+
Tell the user (verbatim):
|
|
174
171
|
|
|
175
|
-
|
|
172
|
+
```
|
|
173
|
+
█████╗ ██████╗ ███████╗███╗ ██╗████████╗██████╗ ██████╗ ██╗ ██╗
|
|
174
|
+
██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██╔═══██╗╚██╗██╔╝
|
|
175
|
+
███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██████╔╝██║ ██║ ╚███╔╝
|
|
176
|
+
██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██╔══██╗██║ ██║ ██╔██╗
|
|
177
|
+
██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ██████╔╝╚██████╔╝██╔╝ ██╗
|
|
178
|
+
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
your box is ready, you can start more sessions with `agentbox claude`
|
|
182
|
+
|
|
183
|
+
## 10. Known issues
|
|
176
184
|
|
|
177
185
|
- For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
|
|
178
186
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../packages/sandbox-docker/src/create.ts","../../../packages/sandbox-docker/src/box-env.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { mkdir, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { execa } from 'execa';\nimport { ConfigError, loadConfig } from '@agentbox/ctl';\nimport {\n buildClaudeMounts,\n ensureClaudeVolume,\n resolveClaudeVolume,\n seedSetupSkillIntoVolume,\n} from './claude.js';\nimport {\n type BoxLimitSpec,\n containerExists,\n dockerInfo,\n dockerStorageDriver,\n ensureVolume,\n publishedHostPort,\n runBox,\n} from './docker.js';\nimport { dockerVolumeName, launchDockerdDaemon } from './dockerd.js';\nimport { generateVncPassword, launchVncDaemon, VNC_CONTAINER_PORT } from './vnc.js';\nimport { WEB_CONTAINER_PORT } from './web.js';\nimport { detectGitRepos, pickFreshBranch } from './git-worktree.js';\nimport {\n bindWorktrees,\n chownGitBindParents,\n collectRepoCarryOver,\n gitWorktreePathFor,\n seedWorkspace,\n seedWorkspaceFromDir,\n type RepoCarryOver,\n} from './in-box-git.js';\nimport {\n CONTAINER_EXPORT_MERGED,\n DEFAULT_ENV_PATTERNS,\n boxRunDirFor,\n copyHostEnvFilesToBox,\n copyHostFilesToBox,\n} from './host-export.js';\nimport { DEFAULT_BOX_IMAGE, ensureImage } from './image.js';\nimport {\n allocateProjectIndex,\n readState,\n recordBox,\n type BoxRecord,\n type GitWorktreeRecord,\n} from './state.js';\nimport { createSnapshot, snapshotPathFor } from './snapshot.js';\nimport { resolveCheckpoint } from './checkpoint.js';\nimport { launchCtlDaemon } from './ctl.js';\nimport { writeBoxEnvFile } from './box-env.js';\nimport {\n ensureRelay,\n generateRelayToken,\n registerBoxWithRelay,\n rehydrateRelayRegistry,\n} from './relay.js';\nimport {\n buildIdeMounts,\n cursorServerVolumeName,\n ensureIdeVolumes,\n repairIdeOwnership,\n vscodeServerVolumeName,\n} from './vscode.js';\n\nexport interface CreateBoxOptions {\n workspacePath: string;\n name?: string;\n /**\n * Take a `cp -c` APFS clone of the host workspace into\n * `~/.agentbox/snapshots/<id>/` before seeding `/workspace`. Stabilizes the\n * source of the tar pipe in the non-git case (and the untracked-file\n * pipe in the git case) against host edits during create. Effectively a\n * no-op when a git worktree is detected — the worktree's tracked content\n * comes from `.git`, not from a workspace copy.\n */\n useSnapshot: boolean;\n /**\n * Start the box from a project checkpoint (the `--snapshot <ref>` path).\n * Resolved against `projectRoot` (or `workspacePath` when unset). The\n * checkpoint is a local Docker *image* tag now; the box is created with\n * `docker run <ckpt-image>` and inherits a populated `/workspace`. No\n * `seedWorkspace` runs in this path.\n */\n checkpointRef?: string;\n image?: string;\n onLog?: (line: string) => void;\n /**\n * Claude Code config volume. When omitted, defaults to `{ isolate: false }` —\n * every box mounts the shared `agentbox-claude-config` volume at\n * /home/vscode/.claude so auth / skills / plugins persist across boxes.\n */\n claudeConfig?: { isolate: boolean };\n /** Extra env vars forwarded to the container (merged on top of claude env forwarding). */\n claudeEnv?: Record<string, string>;\n /**\n * When true, run `npm install -g @playwright/cli@latest` inside the box after\n * `/workspace` is seeded. agent-browser is always installed in the image;\n * this flag adds the Playwright CLI on top for boxes that need it.\n */\n withPlaywright?: boolean;\n /**\n * When true, copy the host's env/config files (DEFAULT_ENV_PATTERNS basename\n * globs — `.env*`, `secrets.toml`, `agentbox.yaml`, ...) into the box's\n * /workspace after seeding, bypassing gitignore. The reverse of `pull env`.\n * One-shot at create time; the files persist in the container's writable\n * layer across pause/stop/start.\n */\n withEnv?: boolean;\n /**\n * Explicit relative-path file list to copy from `workspacePath` into the\n * box's /workspace after seeding (no glob expansion, no scan — the list is\n * pre-vetted, e.g. picked by the wizard's multiselect). Independent of\n * `withEnv`: if both are set, both run (idempotent on overlapping files).\n * One-shot at create time; persists across pause/stop/start.\n */\n envFilesToImport?: string[];\n /**\n * VNC stack (Xvnc on :1 + websockify serving noVNC on container :6080).\n * Defaults to enabled. The CLI exposes `--no-vnc` for opt-out. Disabling\n * skips port mapping + password generation + the in-container supervisor\n * launch; the apt-installed binaries stay in the image but are unused.\n */\n vnc?: { enabled: boolean };\n /**\n * Docker-in-Docker. Always-on (the in-box dockerd is part of the box\n * surface). When `sharedCache` is true the per-box `agentbox-docker-<id>`\n * volume is replaced with the shared `agentbox-docker-cache` volume — image\n * layers persist across boxes (and `destroy`/`prune` won't remove it).\n */\n docker?: { sharedCache: boolean };\n /**\n * Absolute host path of the cwd's project at create time. When provided,\n * `createBox` stamps `projectRoot` + an allocated `projectIndex` on the\n * BoxRecord so the CLI can auto-pick / resolve by index. The CLI computes\n * this via `findProjectRoot(workspacePath)` from `@agentbox/config`; this\n * package stays free of the config dep. Omit for unowned boxes created\n * directly via the programmatic API.\n */\n projectRoot?: string;\n /**\n * Container resource ceilings (engine-agnostic: bytes / fractional cpus /\n * pid count / raw disk size string). Absent fields = unlimited. `disk` is\n * best-effort: dropped (with a warning via `onLog`) when the engine's\n * storage driver can't enforce `--storage-opt size=` (overlay2 / macOS).\n */\n limits?: BoxLimitSpec;\n}\n\nexport interface CreatedBox {\n record: BoxRecord;\n imageBuilt: boolean;\n}\n\n/**\n * Compact the engine-applied limits into the BoxRecord shape: only fields that\n * actually constrain the box (>0 / non-empty). Returns undefined when nothing\n * was applied so legacy/unlimited boxes stay free of the field.\n */\nfunction persistableLimits(\n lim: BoxLimitSpec | undefined,\n): BoxRecord['resourceLimits'] | undefined {\n if (!lim) return undefined;\n const out: NonNullable<BoxRecord['resourceLimits']> = {};\n if (lim.memoryBytes && lim.memoryBytes > 0) out.memoryBytes = Math.floor(lim.memoryBytes);\n if (lim.cpus && lim.cpus > 0) out.cpus = lim.cpus;\n if (lim.pidsLimit && lim.pidsLimit > 0) out.pidsLimit = Math.floor(lim.pidsLimit);\n if (lim.disk) out.disk = lim.disk;\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\nfunction generateBoxId(): string {\n return randomBytes(4).toString('hex');\n}\n\nexport function sanitizeBasename(workspacePath: string): string {\n const raw = basename(resolve(workspacePath));\n return raw\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^[-._]+|[-._]+$/g, '')\n .slice(0, 30)\n .replace(/[-._]+$/, '');\n}\n\nexport function defaultBoxName(workspacePath: string, id: string): string {\n const base = sanitizeBasename(workspacePath);\n return base.length > 0 ? `${base}-${id}` : id;\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\n// ~/.claude is intentionally NOT in this list: it lives in the named volume\n// `agentbox-claude-config` (see resolveClaudeVolume / ensureClaudeVolume) so\n// auth persists inside the container without leaking host state. Only\n// non-claude identity files are bind-mounted from the host.\nasync function buildIdentityMounts(): Promise<string[]> {\n const home = homedir();\n const candidates: Array<{ src: string; dst: string; readOnly: boolean }> = [\n { src: join(home, '.codex'), dst: '/home/vscode/.codex', readOnly: false },\n { src: join(home, '.gitconfig'), dst: '/home/vscode/.gitconfig', readOnly: true },\n ];\n const out: string[] = [];\n for (const c of candidates) {\n if (await pathExists(c.src)) {\n out.push(`${c.src}:${c.dst}${c.readOnly ? ':ro' : ''}`);\n }\n }\n return out;\n}\n\nexport async function createBox(opts: CreateBoxOptions): Promise<CreatedBox> {\n const log = opts.onLog ?? (() => {});\n const workspace = resolve(opts.workspacePath);\n if (!(await pathExists(workspace))) {\n throw new Error(`workspace does not exist: ${workspace}`);\n }\n\n // Pre-flight agentbox.yaml validation on the host so the user sees the real\n // ConfigError instead of an opaque \"socket did not appear\" timeout from the\n // detached daemon exec later. The daemon re-validates inside the box anyway\n // — defence in depth, and necessary because the file lives in the\n // container's writable layer and can be edited after create.\n const cfgPath = join(workspace, 'agentbox.yaml');\n if (await pathExists(cfgPath)) {\n try {\n const cfg = await loadConfig(cfgPath);\n log(`agentbox.yaml validated (${String(cfg.services.length)} service(s))`);\n } catch (err) {\n if (err instanceof ConfigError) {\n throw new Error(`agentbox.yaml validation failed:\\n ${err.message}`);\n }\n throw err;\n }\n }\n\n await dockerInfo();\n log('docker daemon reachable');\n\n // Checkpoint resolution happens *before* image ensure because a checkpoint\n // image replaces the base image as the docker-run base. resolveCheckpoint\n // returns null on miss; we error with the ref so the user can fix it.\n let checkpointImage: string | undefined;\n let checkpointSource: BoxRecord['checkpointSource'];\n let restoredWorktrees: GitWorktreeRecord[] | undefined;\n if (opts.checkpointRef) {\n const projectRootForCkpt = opts.projectRoot ?? workspace;\n const head = await resolveCheckpoint(projectRootForCkpt, opts.checkpointRef);\n if (!head) {\n throw new Error(`checkpoint not found: ${opts.checkpointRef}`);\n }\n checkpointImage = head.manifest.image;\n // Chain: head first then its parents (base-most last). For a flattened\n // checkpoint this collapses to a single-entry chain.\n const chain = [head.name, ...head.manifest.parents];\n checkpointSource = { ref: opts.checkpointRef, type: head.manifest.type, chain };\n // The source's per-worktree paths persisted on the manifest so we can\n // re-establish the /workspace bind mount(s) after `docker run` (docker\n // commit doesn't capture bind-mount content, so the image's /workspace\n // is empty until we re-bind).\n restoredWorktrees = head.manifest.worktrees;\n log(\n `starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`,\n );\n }\n\n const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;\n // ensureImage only acts on the base image; checkpoint images are local-only\n // and must already exist (they were created by `agentbox checkpoint`).\n const ensureRef = checkpointImage ? (opts.image ?? DEFAULT_BOX_IMAGE) : imageRef;\n const { built } = await ensureImage(ensureRef, {\n onProgress: (line) => log(`[image] ${line}`),\n });\n log(built ? `built image ${ensureRef}` : `using cached image ${imageRef}`);\n\n // Bring up the host relay before the box so the box can post events\n // immediately on boot. Best-effort — a relay outage shouldn't block create.\n // Always re-push known box tokens after ensure: the relay's registry is\n // in-memory, so a daemon restart or `docker restart agentbox-relay` between\n // CLI invocations leaves it empty. Repushing is idempotent and cheap.\n let relayUp = false;\n try {\n await ensureRelay({ onLog: log });\n const existing = await readState();\n await rehydrateRelayRegistry(existing.boxes);\n relayUp = true;\n } catch (err) {\n log(`relay unavailable: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n const id = generateBoxId();\n const name = opts.name ?? defaultBoxName(workspace, id);\n const containerName = `agentbox-${name}`;\n const createdAt = new Date().toISOString();\n if (await containerExists(containerName)) {\n throw new Error(`container ${containerName} already exists; remove it first`);\n }\n\n // Per-project monotonic index. Allocated *here* so it can flow into the\n // box / snapshot dir segments (`<id>-<n>-<mnemonic>`) and the\n // `AGENTBOX_PROJECT_INDEX` container env var. Pre-feature legacy boxes\n // never pass `projectRoot`; those records keep `projectIndex` undefined and\n // the dir segments fall back to `<id>-<mnemonic>`.\n let projectIndex: number | undefined;\n if (opts.projectRoot) {\n projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);\n }\n\n // Repo detection + host-side carry-over capture. Branches are picked here\n // (against the host main repos' refs) so they're recorded on the BoxRecord\n // regardless of whether the in-container `git worktree add` succeeds later.\n // When restoring from a checkpoint, the source's per-worktree records are\n // restored from the manifest *here* (not after `docker run`) so the\n // `.git/` bind-mounts in `extraVolumes` know which host main repos to\n // wire up — without those binds the in-container `/workspace/.git` would\n // resolve to a path that doesn't exist in the new container.\n const repoCarryOvers: RepoCarryOver[] = [];\n const gitWorktreeRecords: GitWorktreeRecord[] = [];\n if (checkpointImage && restoredWorktrees && restoredWorktrees.length > 0) {\n gitWorktreeRecords.push(...restoredWorktrees);\n }\n if (!checkpointImage) {\n const repos = await detectGitRepos(workspace);\n if (repos.length > 0) {\n log(\n `detected ${String(repos.length)} git repo(s): ` +\n repos.map((r) => `${r.kind}${r.relPathFromWorkspace ? '@' + r.relPathFromWorkspace : ''}`).join(', '),\n );\n }\n for (const r of repos) {\n const branchBase =\n r.kind === 'root'\n ? `agentbox/${name}`\n : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, '_')}`;\n const branch = await pickFreshBranch(r.hostMainRepo, branchBase);\n const containerPath =\n r.kind === 'root' ? '/workspace' : `/workspace/${r.relPathFromWorkspace}`;\n const gitWorktreePath = gitWorktreePathFor(branch);\n const carry = await collectRepoCarryOver(r, branch, containerPath, gitWorktreePath);\n repoCarryOvers.push(carry);\n gitWorktreeRecords.push({\n kind: r.kind,\n hostMainRepo: r.hostMainRepo,\n containerPath,\n gitWorktreePath,\n branch,\n relPathFromWorkspace: r.relPathFromWorkspace,\n });\n }\n }\n\n // --host-snapshot: APFS clone the workspace into a per-box scratch dir.\n // Only the no-git, no-checkpoint path actually consumes the clone (as the\n // source of the tar pipe in seedWorkspaceFromDir). For the git path the\n // worktree content comes from `.git`'s object DB (bind-mounted) and the\n // untracked-file tar pipe reads from the live host main repo — neither\n // touches `snapshotDir`, so we skip it. For checkpoint restore there's no\n // seedWorkspace at all. Kept on the BoxRecord so destroyBox can clean it up.\n let snapshotDir: string | null = null;\n const snapshotIsUseful = !checkpointImage && repoCarryOvers.length === 0;\n if (opts.useSnapshot && snapshotIsUseful) {\n snapshotDir = snapshotPathFor({ id, name, projectIndex });\n log(`cloning workspace to ${snapshotDir} (APFS clone where available)`);\n const snap = await createSnapshot({ source: workspace, destination: snapshotDir });\n log(`pruned ${snap.prunedPaths.length} platform-dependent dirs from snapshot`);\n } else if (opts.useSnapshot && !checkpointImage) {\n log('skipping --host-snapshot: git worktree path reads content from .git, not from a workspace clone');\n }\n\n await ensureIdeVolumes(id);\n const dockerCacheShared = opts.docker?.sharedCache === true;\n const dockerVolume = dockerVolumeName(id, dockerCacheShared);\n await ensureVolume(dockerVolume);\n log(`prepared volumes ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`);\n const ide = buildIdeMounts(id);\n\n // Claude Code config volume. Shared by default so users sign in once across\n // every box; --isolate-claude-config opts into a per-box volume. Either way,\n // the host's ~/.claude is the authoritative source: we rsync host -> volume\n // on every create so updates on the host (new login, new skills, new MCP)\n // flow into the next box. Sync is additive — box-only state (session logs,\n // etc.) is preserved.\n const claudeSpec = resolveClaudeVolume({\n isolate: opts.claudeConfig?.isolate ?? false,\n boxId: id,\n });\n const claudeEnsured = await ensureClaudeVolume(claudeSpec, {\n syncFromHost: true,\n image: ensureRef,\n hostWorkspace: workspace,\n });\n if (claudeEnsured.synced) {\n log(`synced ${claudeSpec.volume} from ~/.claude`);\n if ((claudeEnsured.filteredHookCount ?? 0) > 0) {\n log(\n `filtered ${String(claudeEnsured.filteredHookCount)} host-path hook(s) (paths under ~/)`,\n );\n }\n if (claudeEnsured.clearedInstallMethod) {\n log(\"cleared host's installMethod from synced .claude.json (box uses the native installer)\");\n }\n if (claudeEnsured.aliasedProjectKey) {\n log(`aliased project state for ${workspace} -> /workspace in synced .claude.json`);\n }\n } else if (claudeEnsured.created) {\n log(`created empty volume ${claudeSpec.volume} (no host ~/.claude to sync)`);\n } else {\n log(`reusing volume ${claudeSpec.volume} (no host ~/.claude to sync)`);\n }\n // Box-only: seed /agentbox-setup into the volume from the image. Never\n // touches the host's ~/.claude. Skipped if a copy already exists.\n const seeded = await seedSetupSkillIntoVolume(claudeSpec.volume, ensureRef);\n if (seeded.seeded) log(`seeded /agentbox-setup skill into ${claudeSpec.volume}`);\n const claudeMounts = buildClaudeMounts(claudeSpec, process.env);\n\n const boxDir = boxRunDirFor({ id, name, projectIndex });\n const socketDir = join(boxDir, 'run');\n const socketPath = join(socketDir, 'ctl.sock');\n // Per-box host dir that `agentbox open` refreshes the merged /workspace\n // into. Bound in at create time so `docker exec rsync` can write straight\n // to the host filesystem — no container restart needed.\n const mergedExportDir = join(boxDir, 'workspace');\n await mkdir(socketDir, { recursive: true });\n await mkdir(mergedExportDir, { recursive: true });\n\n const extraVolumes = await buildIdentityMounts();\n extraVolumes.push(...claudeMounts.extraVolumes);\n extraVolumes.push(...ide.extraVolumes);\n extraVolumes.push(`${socketDir}:/run/agentbox`);\n extraVolumes.push(`${mergedExportDir}:${CONTAINER_EXPORT_MERGED}`);\n // In-box dockerd's data root. Per-box (`agentbox-docker-<id>`, wiped on\n // destroy) by default; shared (`agentbox-docker-cache`, preserved) when\n // `box.dockerCacheShared` is set.\n extraVolumes.push(`${dockerVolume}:/var/lib/docker`);\n // Bind-mount each main repo's `.git/` at its identical absolute host path,\n // RW. The in-container `git worktree add` writes to <main>/.git/worktrees/\n // and the agent's commits write to refs/objects; both have to hit the same\n // path on host and inside the container so `git push` from the host main\n // repo sees the new commits without further sync.\n for (const w of gitWorktreeRecords) {\n extraVolumes.push(`${w.hostMainRepo}/.git:${w.hostMainRepo}/.git`);\n }\n for (const v of extraVolumes) log(`mounting agent dir: ${v}`);\n\n // Per-box bearer token for the host relay. Register *before* runBox so the\n // box's supervisor can post on boot. Skip if the relay isn't reachable —\n // the box still works, it just won't deliver events to the host.\n const relayToken = generateRelayToken();\n if (relayUp) {\n try {\n await registerBoxWithRelay({\n boxId: id,\n token: relayToken,\n name,\n containerName,\n createdAt,\n projectIndex,\n worktrees: gitWorktreeRecords,\n });\n log(`registered box token with relay`);\n } catch (err) {\n log(`relay register failed: ${err instanceof Error ? err.message : String(err)}`);\n relayUp = false;\n }\n }\n const relayEnv: Record<string, string> = relayUp\n ? {\n // host.docker.internal resolves to the host (where the relay node\n // process is running). The matching `--add-host` is set in runBox.\n AGENTBOX_RELAY_URL: `http://host.docker.internal:8787`,\n AGENTBOX_RELAY_TOKEN: relayToken,\n }\n : {};\n\n // VNC stack defaults on; the CLI surfaces `--no-vnc` for opt-out. Generate\n // the password and the port mapping up front so they're baked into the\n // container's env + `-p` flags before `docker run` — both must be set at\n // create time (env survives stop/start; port mappings are immutable).\n const vncEnabled = opts.vnc?.enabled !== false;\n const vncPassword = vncEnabled ? generateVncPassword() : undefined;\n const vncEnv: Record<string, string> = vncEnabled && vncPassword\n ? { AGENTBOX_VNC_PASSWORD: vncPassword }\n : {};\n const vncPortMappings = vncEnabled\n ? [{ hostPort: 0, containerPort: VNC_CONTAINER_PORT, hostIp: '127.0.0.1' }]\n : [];\n\n // Reserve the web port unconditionally: `docker run -p` is immutable, but the\n // `expose:`-flagged service is usually only known after the in-box wizard\n // writes agentbox.yaml. The supervisor forwards :80 to it later; here we just\n // guarantee a published host port exists for whenever that happens.\n const webPortMappings = [\n { hostPort: 0, containerPort: WEB_CONTAINER_PORT, hostIp: '127.0.0.1' },\n ];\n\n // Identity vars that make the box self-aware. `projectIndex` was allocated\n // earlier (right after `id`/`name`) so dir-segment helpers could see it; we\n // just read the binding here.\n const agentboxEnv: Record<string, string> = {\n AGENTBOX: '1',\n AGENTBOX_BOX_NAME: name,\n AGENTBOX_HOST_WORKSPACE: workspace,\n ...(opts.projectRoot ? { AGENTBOX_PROJECT_ROOT: opts.projectRoot } : {}),\n ...(projectIndex !== undefined\n ? { AGENTBOX_PROJECT_INDEX: String(projectIndex) }\n : {}),\n };\n const boxEnvForFile: Record<string, string> = {\n AGENTBOX_BOX_ID: id,\n ...agentboxEnv,\n };\n\n // `--storage-opt size=` is only enforced by devicemapper/btrfs/zfs.\n const appliedLimits: BoxLimitSpec | undefined = opts.limits;\n let effectiveLimits = appliedLimits;\n if (appliedLimits?.disk) {\n const driver = await dockerStorageDriver();\n if (!/^(devicemapper|btrfs|zfs|windowsfilter)$/.test(driver)) {\n log(\n `warning: --disk/box.disk is a no-op on this engine (storage-driver=${driver || 'unknown'}); ignoring`,\n );\n effectiveLimits = { ...appliedLimits, disk: null };\n }\n }\n\n await runBox({\n name: containerName,\n image: imageRef,\n extraVolumes,\n limits: effectiveLimits,\n portMappings: [...vncPortMappings, ...webPortMappings],\n env: {\n AGENTBOX_BOX_ID: id,\n ...agentboxEnv,\n ...claudeMounts.env,\n ...relayEnv,\n ...vncEnv,\n ...(opts.claudeEnv ?? {}),\n },\n });\n log(`container ${containerName} started`);\n\n // Flip the in-container parent dir of each bind-mounted `.git` to\n // vscode-owned. Docker auto-creates the intermediates (e.g. the project root\n // path that contains `.git`) as root:root 755 in the writable layer; without\n // this chown the agent can't write siblings of `.git` (`.turbo/`, `.next/`,\n // build caches) at the project root. Non-recursive — the bind-mounted `.git`\n // itself stays untouched (recursive chown would propagate to the host).\n if (gitWorktreeRecords.length > 0) {\n await chownGitBindParents({\n container: containerName,\n hostMainRepos: gitWorktreeRecords.map((w) => w.hostMainRepo),\n onLog: log,\n });\n }\n\n // /etc/agentbox/box.env: sourced by /etc/profile.d/agentbox.sh in login\n // shells (the docker-run env doesn't reach `agentbox shell <box>` cleanly\n // without it). Best-effort — env vars on the container are the primary\n // path; this file is for shells launched via tools that strip env.\n const boxEnv = await writeBoxEnvFile(containerName, boxEnvForFile);\n if (boxEnv.ok) log('wrote /etc/agentbox/box.env');\n else log(`writing /etc/agentbox/box.env failed: ${boxEnv.reason}`);\n\n // Seed /workspace.\n // - Checkpoint restore: the image already has the source box's per-box\n // worktree dir populated; we only need to re-establish the bind mount\n // onto /workspace (docker commit doesn't capture bind-mount content).\n // - Git path: create in-container worktrees + bind + replay stash + untracked.\n // - No-git path: tar-pipe host workspace (or its APFS clone) into\n // /workspace (no bind — files live directly in the image's writable\n // layer at /workspace).\n if (!checkpointImage) {\n if (repoCarryOvers.length > 0) {\n try {\n await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });\n log('seeded /workspace from in-container git worktree(s)');\n } catch (err) {\n log(\n `seedWorkspace failed; leaving ${containerName} running so you can inspect it`,\n );\n throw err;\n }\n } else {\n const source = snapshotDir ?? workspace;\n await seedWorkspaceFromDir({ container: containerName, hostSource: source, onLog: log });\n }\n } else if (restoredWorktrees && restoredWorktrees.length > 0) {\n // gitWorktreeRecords was populated above (pre-`docker run`) so the .git\n // bind-mounts in extraVolumes are wired. The /workspace bind itself\n // can't be set up until the container is running, so we apply it here.\n await bindWorktrees(\n containerName,\n restoredWorktrees.map((w) => ({\n kind: w.kind,\n containerPath: w.containerPath,\n gitWorktreePath: w.gitWorktreePath,\n })),\n log,\n );\n log('re-bound /workspace from checkpoint image');\n } else {\n log('using /workspace from checkpoint image (no worktrees recorded; no rebind)');\n }\n\n await repairIdeOwnership(containerName);\n log('.vscode-server + .cursor-server ownership verified');\n\n const ctl = await launchCtlDaemon(containerName, socketPath);\n if (ctl.up) log('agentbox-ctl daemon up');\n else log(`agentbox-ctl daemon did not become reachable: ${ctl.reason}`);\n\n // dockerd: always-on, mirrors launchVncDaemon. Best-effort — a slow start\n // shouldn't fail box creation; `agentbox start` will relaunch on restart\n // (the daemon dies with the container). Storage driver is fuse-overlayfs,\n // pinned in /etc/docker/daemon.json baked into the image.\n const dockerd = await launchDockerdDaemon(containerName);\n if (dockerd.up) {\n log(`dockerd up (storage-driver=fuse-overlayfs, data root=${dockerVolume})`);\n } else {\n log(`dockerd did not become ready: ${dockerd.reason}`);\n }\n\n if (opts.withPlaywright) {\n log('installing @playwright/cli@latest (--with-playwright)');\n const result = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'root',\n containerName,\n 'bash',\n '-lc',\n 'npm install -g @playwright/cli@latest 2>&1',\n ],\n { reject: false },\n );\n for (const line of (result.stdout ?? '').split('\\n')) {\n if (line.trim().length > 0) log(`[playwright] ${line}`);\n }\n if (result.exitCode !== 0) {\n throw new Error(\n `failed to install @playwright/cli (exit ${String(result.exitCode)}): ${(result.stderr ?? '').toString().slice(0, 400)}`,\n );\n }\n log('@playwright/cli installed');\n }\n\n if (opts.withEnv) {\n log('copying host env/config files into /workspace (--with-env)');\n const { copied } = await copyHostEnvFilesToBox({\n container: containerName,\n workspaceDir: workspace,\n patterns: DEFAULT_ENV_PATTERNS,\n onLog: log,\n });\n log(copied > 0 ? `copied ${String(copied)} env/config file(s)` : 'no env/config files found');\n }\n\n if (opts.envFilesToImport && opts.envFilesToImport.length > 0) {\n log(`copying ${String(opts.envFilesToImport.length)} selected env/config file(s) into /workspace`);\n const { copied } = await copyHostFilesToBox({\n container: containerName,\n workspaceDir: workspace,\n files: opts.envFilesToImport,\n onLog: log,\n });\n if (copied !== opts.envFilesToImport.length) {\n log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);\n }\n }\n\n // VNC daemon (Xvnc + websockify). Best-effort, like launchCtlDaemon. The\n // host port mapping was wired into runBox above (hostPort=0 → random); we\n // resolve the assigned port here for storage. If the daemon fails to come\n // up we still record vncEnabled so `agentbox start` will retry the launch.\n let vncHostPort: number | null = null;\n if (vncEnabled) {\n const vnc = await launchVncDaemon(containerName);\n if (vnc.up) log('vnc stack up (Xvnc + websockify + noVNC)');\n else log(`vnc stack did not become reachable: ${vnc.reason}`);\n vncHostPort = await publishedHostPort(containerName, VNC_CONTAINER_PORT);\n if (vncHostPort) log(`vnc web on host 127.0.0.1:${String(vncHostPort)}`);\n }\n\n const webHostPort = await publishedHostPort(containerName, WEB_CONTAINER_PORT);\n if (webHostPort) {\n log(\n `web port reserved on host 127.0.0.1:${String(webHostPort)} ` +\n `(forwards to the web service once agentbox.yaml sets a service expose:)`,\n );\n }\n\n const record: BoxRecord = {\n id,\n name,\n container: containerName,\n image: imageRef,\n workspacePath: workspace,\n snapshotDir,\n socketPath,\n claudeConfigVolume: claudeSpec.volume,\n vscodeServerVolume: vscodeServerVolumeName(id),\n cursorServerVolume: cursorServerVolumeName(id),\n relayToken: relayUp ? relayToken : undefined,\n gitWorktrees: gitWorktreeRecords.length > 0 ? gitWorktreeRecords : undefined,\n withPlaywright: opts.withPlaywright ? true : undefined,\n withEnv: opts.withEnv ? true : undefined,\n vncEnabled: vncEnabled ? true : undefined,\n vncContainerPort: vncEnabled ? VNC_CONTAINER_PORT : undefined,\n vncHostPort: vncHostPort ?? undefined,\n vncPassword: vncPassword,\n webContainerPort: WEB_CONTAINER_PORT,\n webHostPort: webHostPort ?? undefined,\n dockerVolume,\n dockerCacheShared: dockerCacheShared || undefined,\n projectRoot: opts.projectRoot,\n projectIndex,\n checkpointImage,\n checkpointSource,\n resourceLimits: persistableLimits(effectiveLimits),\n createdAt,\n };\n await recordBox(record);\n\n return { record, imageBuilt: built };\n}\n","import { execa } from 'execa';\n\n/**\n * Writes /etc/agentbox/box.env inside the container as a POSIX-sourceable\n * key='value' file. Paired with /etc/profile.d/agentbox.sh (baked in the\n * image), which `set -a; . /etc/agentbox/box.env; set +a`s it on login.\n *\n * Best-effort: failure is logged by the caller; an unwritable file just\n * means interactive shells lose the AGENTBOX_* vars (the env vars baked\n * into docker run still survive).\n */\nexport async function writeBoxEnvFile(\n container: string,\n env: Record<string, string>,\n): Promise<{ ok: true } | { ok: false; reason: string }> {\n const body = formatBoxEnvBody(env);\n const result = await execa(\n 'docker',\n ['exec', '--user', 'root', '-i', container, 'sh', '-c', 'umask 022 && cat > /etc/agentbox/box.env'],\n { input: body, reject: false },\n );\n if (result.exitCode !== 0) {\n return {\n ok: false,\n reason: `docker exec failed (exit ${String(result.exitCode)}): ${(result.stderr ?? '').toString().slice(0, 400)}`,\n };\n }\n return { ok: true };\n}\n\n// Single-quote each value and escape embedded single quotes as '\\''. Avoids\n// double-quoted form because `. ` would expand $foo / `cmd` at source time.\nexport function formatBoxEnvBody(env: Record<string, string>): string {\n const lines: string[] = [];\n for (const [k, v] of Object.entries(env)) {\n lines.push(`${k}=${shellSingleQuote(v)}`);\n }\n return lines.join('\\n') + '\\n';\n}\n\nfunction shellSingleQuote(s: string): string {\n return `'${s.replace(/'/g, `'\\\\''`)}'`;\n}\n"],"mappings":"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","names":["execa","execa"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../packages/sandbox-docker/src/lifecycle.ts","../../../packages/sandbox-docker/src/endpoints.ts"],"sourcesContent":["import { execa } from 'execa';\nimport { readdir, rm, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { BoxState } from '@agentbox/core';\nimport type { BoxStatus, ClaudeActivityState } from '@agentbox/ctl';\nimport { claudeSessionInfo, SHARED_CLAUDE_VOLUME, type ClaudeSessionInfo } from './claude.js';\nimport { bindWorktrees, removeInBoxWorktree } from './in-box-git.js';\nimport {\n cursorServerVolumeName,\n SHARED_CURSOR_EXTENSIONS_VOLUME,\n SHARED_VSCODE_EXTENSIONS_VOLUME,\n vscodeServerVolumeName,\n} from './vscode.js';\nimport {\n BOXES_ROOT,\n boxRunDirFor,\n detectEngine,\n getHostPaths,\n openInFinder,\n readBoxStatus,\n type HostPaths,\n type OpenOptions,\n type OpenResult,\n} from './host-export.js';\nimport {\n inspectContainer,\n inspectContainerStatus,\n listAgentboxContainers,\n listAgentboxVolumes,\n pauseContainer,\n publishedHostPort,\n removeContainer,\n removeImage,\n removeNetwork,\n removeVolume,\n startContainer,\n stopContainer,\n unpauseContainer,\n} from './docker.js';\nimport { CHECKPOINT_IMAGE_PREFIX, listAllCheckpointImages } from './checkpoint.js';\nimport { launchCtlDaemon } from './ctl.js';\nimport { launchDockerdDaemon, SHARED_DOCKER_CACHE_VOLUME } from './dockerd.js';\nimport { launchVncDaemon, VNC_CONTAINER_PORT } from './vnc.js';\nimport { WEB_CONTAINER_PORT } from './web.js';\nimport { getBoxEndpoints, type BoxEndpoints } from './endpoints.js';\nimport {\n ensureRelay,\n forgetBoxFromRelay,\n registerBoxWithRelay,\n RELAY_CONTAINER_NAME,\n RELAY_IMAGE_REF,\n RELAY_NETWORK_NAME,\n} from './relay.js';\nimport { SNAPSHOTS_ROOT } from './snapshot.js';\nimport {\n findBox,\n readState,\n recordBox,\n removeBoxRecord,\n type BoxRecord,\n type FindBoxResult,\n} from './state.js';\n\nexport interface ListedBox extends BoxRecord {\n state: BoxState;\n endpoints: BoxEndpoints;\n /** From the persisted status file; undefined for pre-feature/never-pushed boxes. */\n claudeActivity?: ClaudeActivityState;\n /** Sanitized in-box terminal title Claude set; undefined when none. */\n claudeSessionTitle?: string;\n}\n\nexport async function listBoxes(): Promise<ListedBox[]> {\n const { boxes } = await readState();\n const engine = await detectEngine();\n return Promise.all(\n boxes.map(async (b): Promise<ListedBox> => {\n const state = await inspectContainerStatus(b.container);\n const persisted = await readBoxStatus(b);\n const endpoints = await getBoxEndpoints(b, engine, persisted);\n return {\n ...b,\n state,\n endpoints,\n claudeActivity: persisted?.claude.state,\n claudeSessionTitle: persisted?.claude.sessionTitle,\n };\n }),\n );\n}\n\nexport class BoxNotFoundError extends Error {\n constructor(public readonly query: string) {\n super(`no agentbox matches \"${query}\"`);\n this.name = 'BoxNotFoundError';\n }\n}\n\nexport class AmbiguousBoxError extends Error {\n constructor(\n public readonly query: string,\n public readonly matches: BoxRecord[],\n ) {\n const ids = matches.map((m) => m.id).join(', ');\n super(`\"${query}\" matches multiple boxes: ${ids}`);\n this.name = 'AmbiguousBoxError';\n }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function resolveBox(idOrName: string): Promise<BoxRecord> {\n const state = await readState();\n const result: FindBoxResult = findBox(idOrName, state);\n switch (result.kind) {\n case 'ok':\n return result.box;\n case 'none':\n throw new BoxNotFoundError(idOrName);\n case 'ambiguous':\n throw new AmbiguousBoxError(idOrName, result.matches);\n }\n}\n\nexport async function pauseBox(idOrName: string): Promise<BoxRecord> {\n const box = await resolveBox(idOrName);\n await pauseContainer(box.container);\n return box;\n}\n\nexport async function unpauseBox(idOrName: string): Promise<BoxRecord> {\n const box = await resolveBox(idOrName);\n await unpauseContainer(box.container);\n return box;\n}\n\nexport async function stopBox(idOrName: string): Promise<BoxRecord> {\n const box = await resolveBox(idOrName);\n await stopContainer(box.container);\n return box;\n}\n\nexport interface StartedBox {\n record: BoxRecord;\n}\n\n/**\n * Re-start a stopped box.\n *\n * /workspace is just the container's writable filesystem now, so there's no\n * overlay to remount — `docker start` brings everything back. The in-box\n * supervisor, dockerd, and Xvnc all die with the container, so we relaunch\n * them via the same exec-d helpers `create` used. Ephemeral host ports for\n * VNC + web get re-allocated by Docker on `start`, so we re-resolve and\n * persist those too.\n */\nexport async function startBox(idOrName: string): Promise<StartedBox> {\n const box = await resolveBox(idOrName);\n // .git bind-mounts are baked into the container at create time; if a host\n // main repo's .git/ has been deleted out from under us, restart fails with\n // an opaque mount error. Surface it loudly.\n for (const w of box.gitWorktrees ?? []) {\n if (!(await pathExists(join(w.hostMainRepo, '.git')))) {\n throw new Error(\n `main repo for box worktree missing: ${join(w.hostMainRepo, '.git')} (recreate the box)`,\n );\n }\n }\n await startContainer(box.container);\n\n // /workspace bind mounts don't survive `docker stop` (the mount namespace\n // is recreated on start). Re-bind each registered worktree before any\n // daemon comes up — the supervisor and dockerd may resolve paths under\n // /workspace and would see the image's empty dir without this.\n if ((box.gitWorktrees ?? []).length > 0) {\n await bindWorktrees(\n box.container,\n (box.gitWorktrees ?? []).map((w) => ({\n kind: w.kind,\n containerPath: w.containerPath,\n gitWorktreePath: w.gitWorktreePath,\n })),\n );\n }\n\n if (box.socketPath) {\n // The daemon died with the container; relaunch it. Best-effort, same as\n // create.ts — a missing config or other startup issue shouldn't block\n // resumption of the box itself.\n await launchCtlDaemon(box.container, box.socketPath);\n }\n if (box.dockerVolume) {\n await launchDockerdDaemon(box.container);\n }\n if (box.vncEnabled) {\n // Xvnc + websockify both die with the container. The password is already\n // in the container env (set at `docker run` time and preserved across\n // start/stop), so we don't need to forward it here.\n await launchVncDaemon(box.container);\n // Docker re-allocates an ephemeral host port for `-p 0:6080` on every\n // `start`. Re-resolve and persist; the orb.local URL is name-based and\n // unaffected. Best-effort — a failed resolve just leaves the record as-is.\n const freshHostPort = await publishedHostPort(box.container, VNC_CONTAINER_PORT);\n if (freshHostPort && freshHostPort !== box.vncHostPort) {\n box.vncHostPort = freshHostPort;\n await recordBox(box);\n }\n }\n // Same ephemeral-reallocation story for the reserved web port. Gated on\n // webContainerPort so pre-feature boxes (no `-p 0:80` mapping) are skipped.\n if (box.webContainerPort !== undefined) {\n const freshWebPort = await publishedHostPort(\n box.container,\n box.webContainerPort ?? WEB_CONTAINER_PORT,\n );\n if (freshWebPort && freshWebPort !== box.webHostPort) {\n box.webHostPort = freshWebPort;\n await recordBox(box);\n }\n }\n // Relay's in-memory registry may have been lost if the relay restarted\n // between create and now (or this is the first start after a host reboot).\n // Re-ensure + re-register so outbound push from the box keeps working.\n if (box.relayToken) {\n try {\n await ensureRelay();\n await registerBoxWithRelay({\n boxId: box.id,\n token: box.relayToken,\n name: box.name,\n containerName: box.container,\n createdAt: box.createdAt,\n projectIndex: box.projectIndex,\n worktrees: box.gitWorktrees,\n });\n } catch {\n // best-effort\n }\n }\n return { record: box };\n}\n\nexport interface OpenedBox extends OpenResult {\n record: BoxRecord;\n}\n\nexport async function openBoxInFinder(idOrName: string, opts: OpenOptions): Promise<OpenedBox> {\n const box = await resolveBox(idOrName);\n const result = await openInFinder(box, opts);\n return { ...result, record: box };\n}\n\nexport async function getBoxHostPaths(\n idOrName: string,\n): Promise<{ record: BoxRecord; paths: HostPaths }> {\n const box = await resolveBox(idOrName);\n const paths = await getHostPaths(box);\n return { record: box, paths };\n}\n\nexport interface InspectedBox {\n record: BoxRecord;\n state: BoxState;\n snapshotSizeBytes: number | null;\n dockerInspect: unknown;\n /** Null when the container isn't running; otherwise best-effort probe of the tmux 'claude' session. */\n claudeSession: ClaudeSessionInfo | null;\n /** Persisted status snapshot (services/tasks/ports/claude); null when none. */\n persistedStatus: BoxStatus | null;\n /** Host paths for `agentbox open`. */\n hostPaths: HostPaths;\n /** Box network surface: domain + VNC + service ports. */\n endpoints: BoxEndpoints;\n}\n\nasync function dirSizeBytes(path: string): Promise<number | null> {\n try {\n const result = await execa('du', ['-sk', path], { reject: false });\n if (result.exitCode !== 0) return null;\n const sizeKb = Number.parseInt((result.stdout ?? '').split(/\\s+/)[0] ?? '', 10);\n if (Number.isNaN(sizeKb)) return null;\n return sizeKb * 1024;\n } catch {\n return null;\n }\n}\n\nexport async function inspectBox(idOrName: string): Promise<InspectedBox> {\n const record = await resolveBox(idOrName);\n const state = await inspectContainerStatus(record.container);\n const snapshotSizeBytes = record.snapshotDir ? await dirSizeBytes(record.snapshotDir) : null;\n const dockerJson = await inspectContainer(record.container);\n\n let claudeSession: ClaudeSessionInfo | null = null;\n if (state === 'running') {\n try {\n claudeSession = await claudeSessionInfo(record.container);\n } catch {\n claudeSession = null;\n }\n }\n\n const hostPaths = await getHostPaths(record);\n const engine = await detectEngine();\n const persistedStatus = await readBoxStatus(record);\n const endpoints = await getBoxEndpoints(record, engine, persistedStatus);\n\n return {\n record,\n state,\n snapshotSizeBytes,\n dockerInspect: dockerJson,\n claudeSession,\n persistedStatus,\n hostPaths,\n endpoints,\n };\n}\n\nexport interface DestroyOptions {\n keepSnapshot?: boolean;\n}\n\nexport interface DestroyResult {\n record: BoxRecord;\n removedContainer: boolean;\n removedVolumes: string[];\n removedSnapshot: string | null;\n}\n\nexport async function destroyBox(\n idOrName: string,\n opts: DestroyOptions = {},\n): Promise<DestroyResult> {\n const box = await resolveBox(idOrName);\n\n // Each step is best-effort. We collect what actually went away so the CLI\n // can show a truthful summary even if e.g. the container was gone already.\n if (box.relayToken) {\n try {\n await forgetBoxFromRelay(box.id);\n } catch {\n // best-effort — relay may be down or already wiped the entry\n }\n }\n // Deregister each in-container worktree from the host main repo. Skip\n // when this box was checkpoint-restored: its `gitWorktrees` were inherited\n // from the source box via the checkpoint manifest, and the same\n // `gitWorktreePath` may still be in use by the source (or by sibling\n // restores). Removing the registration here would break those. The\n // registration is cosmetically `prunable` on the host anyway (the path is\n // container-only) and can be reaped with `git worktree prune` when the\n // user is sure no box references it.\n const ownsWorktrees = !box.checkpointImage;\n if (ownsWorktrees) {\n for (const w of box.gitWorktrees ?? []) {\n try {\n await removeInBoxWorktree({\n hostMainRepo: w.hostMainRepo,\n gitWorktreePath: w.gitWorktreePath,\n });\n } catch {\n // best-effort\n }\n }\n }\n const beforeContainer = await inspectContainerStatus(box.container);\n await removeContainer(box.container);\n const afterContainer = await inspectContainerStatus(box.container);\n const removedContainer = beforeContainer !== 'missing' && afterContainer === 'missing';\n\n const removedVolumes: string[] = [];\n // Per-box claude config volumes are box-private — safe to remove. The shared\n // SHARED_CLAUDE_VOLUME holds user identity (auth, skills, plugins) across\n // every box, so never auto-remove it; users delete it manually if they want.\n if (box.claudeConfigVolume && box.claudeConfigVolume !== SHARED_CLAUDE_VOLUME) {\n await removeVolume(box.claudeConfigVolume);\n removedVolumes.push(box.claudeConfigVolume);\n }\n // Per-box `.vscode-server` and `.cursor-server` volumes. The shared\n // SHARED_*_EXTENSIONS_VOLUMEs are never auto-removed (parallel reasoning to\n // the shared claude volume).\n const perBoxIdeVolumes = [\n box.vscodeServerVolume ?? vscodeServerVolumeName(box.id),\n box.cursorServerVolume ?? cursorServerVolumeName(box.id),\n ];\n for (const v of perBoxIdeVolumes) {\n await removeVolume(v);\n removedVolumes.push(v);\n }\n // Per-box dockerd data root. Skip when this box used the shared cache —\n // wiping it would also remove image layers other boxes (or future ones)\n // depend on. The shared volume is allowlisted in `pruneBoxes --all` too.\n if (box.dockerVolume && !box.dockerCacheShared) {\n await removeVolume(box.dockerVolume);\n removedVolumes.push(box.dockerVolume);\n }\n\n let removedSnapshot: string | null = null;\n if (box.snapshotDir && !opts.keepSnapshot) {\n try {\n await rm(box.snapshotDir, { recursive: true, force: true });\n removedSnapshot = box.snapshotDir;\n } catch {\n removedSnapshot = null;\n }\n }\n\n // The per-box runtime dir holds the ctl socket plus the workspace export\n // dir used by `agentbox open`. Wipe the whole thing so destroy leaves no\n // residue under ~/.agentbox/boxes/.\n try {\n await rm(boxRunDirFor(box), { recursive: true, force: true });\n } catch {\n // best-effort\n }\n\n await removeBoxRecord(box.id);\n\n return { record: box, removedContainer, removedVolumes, removedSnapshot };\n}\n\nexport interface PruneOptions {\n dryRun?: boolean;\n all?: boolean;\n}\n\nexport interface PruneResult {\n removedRecords: string[];\n removedContainers: string[];\n removedVolumes: string[];\n removedSnapshotDirs: string[];\n removedBoxDirs: string[];\n removedCheckpointImages: string[];\n dryRun: boolean;\n}\n\nasync function listSnapshotDirs(): Promise<string[]> {\n try {\n const entries = await readdir(SNAPSHOTS_ROOT, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => join(SNAPSHOTS_ROOT, e.name));\n } catch {\n return [];\n }\n}\n\nasync function listBoxDirs(): Promise<string[]> {\n try {\n const entries = await readdir(BOXES_ROOT, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => join(BOXES_ROOT, e.name));\n } catch {\n return [];\n }\n}\n\n/**\n * Local Docker image *tags* that look like checkpoint images\n * (`agentbox-ckpt-<projectHash>:<name>`). Used by `prune --all` to find\n * candidates for reaping. An image is reapable only when **both** of these\n * are true: no surviving box's `checkpointImage` points at it, **and** no\n * on-disk manifest under `~/.agentbox/checkpoints/<projectHash>/<name>/`\n * names it as its `image` (see `listAllCheckpointImages`) — otherwise a\n * `destroy` + `prune --all` would silently break checkpoints the user still\n * intends to start new boxes from. Best-effort: returns empty on docker\n * errors.\n */\nasync function listCheckpointImageTags(): Promise<string[]> {\n const r = await execa(\n 'docker',\n ['image', 'ls', '--format', '{{.Repository}}:{{.Tag}}', `${CHECKPOINT_IMAGE_PREFIX}*`],\n { reject: false },\n );\n if (r.exitCode !== 0) return [];\n return (r.stdout ?? '')\n .split('\\n')\n .map((s) => s.trim())\n .filter((s) => s.startsWith(CHECKPOINT_IMAGE_PREFIX));\n}\n\nexport async function pruneBoxes(opts: PruneOptions = {}): Promise<PruneResult> {\n const dryRun = opts.dryRun ?? false;\n const all = opts.all ?? false;\n\n const { boxes } = await readState();\n\n // Step 1: missing-state records.\n const stateChecks = await Promise.all(\n boxes.map(async (b) => ({ box: b, status: await inspectContainerStatus(b.container) })),\n );\n const missingRecords = stateChecks.filter((c) => c.status === 'missing').map((c) => c.box);\n\n // Step 2 (only with --all): orphan docker containers / volumes / snapshot\n // dirs / per-box dirs / unreferenced checkpoint images.\n let orphanContainers: string[] = [];\n let orphanVolumes: string[] = [];\n let orphanSnapshots: string[] = [];\n let orphanBoxDirs: string[] = [];\n let orphanCheckpointImages: string[] = [];\n\n if (all) {\n const liveContainers = await listAgentboxContainers();\n const liveVolumes = await listAgentboxVolumes();\n const liveSnapshotDirs = await listSnapshotDirs();\n const liveBoxDirs = await listBoxDirs();\n const liveCheckpointImages = await listCheckpointImageTags();\n // Manifests on disk are the durable source of truth for \"this checkpoint\n // exists\" — `destroyBox` leaves them alone on purpose, so an image whose\n // source box was destroyed is still pinned as long as its manifest is\n // there.\n const manifestPinnedImages = await listAllCheckpointImages();\n // The state we'd have AFTER step 1 runs: missing-state records gone.\n const survivingBoxes = boxes.filter((b) => !missingRecords.some((m) => m.id === b.id));\n const expectedContainers = new Set<string>([\n ...survivingBoxes.map((b) => b.container),\n // The relay no longer runs as a container; leftovers are collected\n // below.\n ]);\n const expectedVolumes = new Set<string>([\n ...survivingBoxes\n .map((b) => b.claudeConfigVolume)\n .filter((v): v is string => typeof v === 'string'),\n ...survivingBoxes\n .map((b) => b.vscodeServerVolume)\n .filter((v): v is string => typeof v === 'string'),\n ...survivingBoxes\n .map((b) => b.cursorServerVolume)\n .filter((v): v is string => typeof v === 'string'),\n ...survivingBoxes\n .map((b) => b.dockerVolume)\n .filter((v): v is string => typeof v === 'string'),\n // The shared claude-config volume holds user identity across every box;\n // never reap it via prune even if no surviving box currently references it.\n SHARED_CLAUDE_VOLUME,\n // Shared across boxes: downloaded IDE extensions. Same reasoning.\n SHARED_VSCODE_EXTENSIONS_VOLUME,\n SHARED_CURSOR_EXTENSIONS_VOLUME,\n // Shared in-box docker image cache — opt-in via `box.dockerCacheShared`,\n // never auto-removed (image layers may be reused by future boxes).\n SHARED_DOCKER_CACHE_VOLUME,\n ]);\n const expectedSnapshots = new Set(\n survivingBoxes\n .filter((b): b is BoxRecord & { snapshotDir: string } =>\n typeof b.snapshotDir === 'string',\n )\n .map((b) => b.snapshotDir),\n );\n const expectedBoxDirs = new Set(survivingBoxes.map((b) => boxRunDirFor(b)));\n // Checkpoint images: keep any tag that either a surviving box's\n // `checkpointImage` points at, or that any on-disk manifest still claims\n // as its `image`. The manifest case is the one that matters most after\n // destroy: the source box is gone but the user still wants to seed new\n // boxes from the checkpoint. The surviving-box case stays as a fallback\n // for the edge where someone `rm -rf`'d a manifest dir while a box\n // restored from it is still running.\n const expectedCheckpointImages = new Set<string>([\n ...survivingBoxes\n .map((b) => b.checkpointImage)\n .filter((v): v is string => typeof v === 'string'),\n ...manifestPinnedImages,\n ]);\n orphanContainers = liveContainers.filter((c) => !expectedContainers.has(c));\n orphanVolumes = liveVolumes.filter((v) => !expectedVolumes.has(v));\n orphanSnapshots = liveSnapshotDirs.filter((d) => !expectedSnapshots.has(d));\n orphanBoxDirs = liveBoxDirs.filter((d) => !expectedBoxDirs.has(d));\n orphanCheckpointImages = liveCheckpointImages.filter(\n (t) => !expectedCheckpointImages.has(t),\n );\n }\n\n if (dryRun) {\n return {\n removedRecords: missingRecords.map((b) => b.id),\n removedContainers: orphanContainers,\n removedVolumes: orphanVolumes,\n removedSnapshotDirs: orphanSnapshots,\n removedBoxDirs: orphanBoxDirs,\n removedCheckpointImages: orphanCheckpointImages,\n dryRun: true,\n };\n }\n\n for (const b of missingRecords) await removeBoxRecord(b.id);\n for (const c of orphanContainers) await removeContainer(c);\n for (const v of orphanVolumes) await removeVolume(v);\n for (const d of orphanSnapshots) {\n try {\n await rm(d, { recursive: true, force: true });\n } catch {\n // best-effort\n }\n }\n for (const d of orphanBoxDirs) {\n try {\n await rm(d, { recursive: true, force: true });\n } catch {\n // best-effort\n }\n }\n for (const img of orphanCheckpointImages) {\n await removeImage(img, { force: true });\n }\n\n // Migration sweep: the relay used to be a docker container on a dedicated\n // network with its own image. None of those exist after this version of\n // agentbox; drop any leftovers from previous installs. Idempotent and\n // best-effort — these calls succeed silently if the objects are already\n // gone.\n if (all) {\n try {\n await removeContainer(RELAY_CONTAINER_NAME);\n } catch {\n // best-effort\n }\n try {\n await execa('docker', ['image', 'rm', RELAY_IMAGE_REF], { reject: false });\n } catch {\n // best-effort\n }\n try {\n await removeNetwork(RELAY_NETWORK_NAME);\n } catch {\n // best-effort\n }\n }\n\n return {\n removedRecords: missingRecords.map((b) => b.id),\n removedContainers: orphanContainers,\n removedVolumes: orphanVolumes,\n removedSnapshotDirs: orphanSnapshots,\n removedBoxDirs: orphanBoxDirs,\n removedCheckpointImages: orphanCheckpointImages,\n dryRun: false,\n };\n}\n\n// Help vitest / unit tests get to the snapshot-root constant without pulling\n// the whole snapshot module surface.\nexport { SNAPSHOTS_ROOT };\n\n// Re-export the file existence helper for inspect output; useful guard for\n// callers that want to know if a snapshot dir was ever created.\nexport async function snapshotPresent(path: string | null): Promise<boolean> {\n if (!path) return false;\n try {\n const s = await stat(path);\n return s.isDirectory();\n } catch {\n return false;\n }\n}\n","import { join } from 'node:path';\nimport { loadConfig } from '@agentbox/ctl';\nimport type { BoxStatus } from '@agentbox/ctl';\nimport type { BoxRecord } from './state.js';\nimport type { DockerEngine } from './host-export.js';\nimport { buildVncUrls, VNC_CONTAINER_PORT } from './vnc.js';\nimport { WEB_CONTAINER_PORT } from './web.js';\n\nexport interface BoxEndpoint {\n kind: 'vnc' | 'service' | 'web';\n /** Service name (kind === 'service'/'web') or 'vnc' (kind === 'vnc'). */\n name: string;\n /** In-container port (6080 for VNC, the `ready_when.port` value for services). */\n containerPort: number;\n /**\n * Host-side URL the user can open. Undefined when the port isn't reachable\n * from the host (service ports on Docker Desktop, since we don't auto-publish\n * them today).\n */\n url?: string;\n /** Whether the URL is reachable from the host on the current engine. */\n reachable: boolean;\n}\n\nexport interface BoxEndpoints {\n /** Bare hostname/IP for the box — `<container>.orb.local` on OrbStack, `127.0.0.1` otherwise. */\n domain: string;\n /** True when domain is the OrbStack auto-DNS (any in-container port works). */\n domainIsOrb: boolean;\n /** Ordered list of endpoints: VNC first (if enabled), then services in agentbox.yaml order. */\n endpoints: BoxEndpoint[];\n}\n\n/**\n * Build the box's user-facing network surface. Pure host-side: no docker exec,\n * no network — safe to call from `agentbox list` in a tight loop.\n *\n * Service ports come from the persisted status snapshot when available\n * (`~/.agentbox/boxes/<id>/status.json`, pushed by the in-box supervisor via\n * the relay). That snapshot resolves `ready_when.port` *inside the box*, so it\n * works even when `agentbox.yaml` lives only in the box and was never pulled to\n * the host. Falls back to parsing the host's `agentbox.yaml` for pre-relay\n * boxes (or ones that never pushed a snapshot).\n *\n * Missing config + no snapshot is non-fatal: the VNC entry (if any) is still\n * returned. Engine drives reachability — OrbStack auto-routes\n * `<container>.orb.local:<port>` for any in-box port; other engines see only\n * what we explicitly publish via `docker run -p`, which today is just VNC.\n */\nexport async function getBoxEndpoints(\n record: BoxRecord,\n engine: DockerEngine,\n persisted?: BoxStatus | null,\n): Promise<BoxEndpoints> {\n const domainIsOrb = engine === 'orbstack';\n const domain = domainIsOrb ? `${record.container}.orb.local` : '127.0.0.1';\n\n const endpoints: BoxEndpoint[] = [];\n\n if (record.vncEnabled && record.vncPassword) {\n const vncUrls = buildVncUrls(record, engine);\n const url = vncUrls.orbUrl ?? vncUrls.loopbackUrl;\n endpoints.push({\n kind: 'vnc',\n name: 'vnc',\n containerPort: VNC_CONTAINER_PORT,\n url,\n reachable: Boolean(url),\n });\n }\n\n // The single `expose:`-flagged service, from the snapshot first (works when\n // agentbox.yaml lives only in the box), else the host yaml.\n let webServiceName: string | null = null;\n const persistedWeb = persisted?.services.find((s) => s.expose);\n if (persistedWeb) {\n webServiceName = persistedWeb.name;\n }\n\n const pushService = (name: string, port: number): void => {\n // The web service is surfaced as the dedicated `web` endpoint below;\n // don't also list it as a generic service.\n if (name === webServiceName) return;\n endpoints.push({\n kind: 'service',\n name,\n containerPort: port,\n // Only OrbStack auto-routes arbitrary in-box ports; on other engines we\n // don't publish service ports, so the URL isn't host-reachable.\n ...(domainIsOrb\n ? { url: `http://${domain}:${String(port)}`, reachable: true }\n : { reachable: false }),\n });\n };\n\n const persistedServices = persisted?.services.filter(\n (s): s is typeof s & { port: number } => typeof s.port === 'number',\n );\n if (persistedServices && persistedServices.length > 0) {\n for (const svc of persistedServices) pushService(svc.name, svc.port);\n } else {\n try {\n const cfg = await loadConfig(join(record.workspacePath, 'agentbox.yaml'));\n if (!webServiceName) {\n webServiceName = cfg.services.find((s) => s.expose)?.name ?? null;\n }\n for (const svc of cfg.services) {\n if (svc.readyWhen?.kind !== 'port') continue;\n pushService(svc.name, svc.readyWhen.port);\n }\n } catch {\n // No persisted snapshot and no host agentbox.yaml — skip service\n // endpoints. The VNC entry, if any, is unaffected.\n }\n }\n\n // Web endpoint: only for boxes that reserved container :80 at create. The\n // URL is the published loopback host port — uniform across engines, NOT\n // gated on OrbStack (requirement: don't rely on orb auto-DNS). No url until\n // both a service declares `expose:` and the host port is resolved; until\n // then it renders as \"reserved\".\n if (record.webContainerPort !== undefined) {\n const hasTarget = webServiceName !== null && record.webHostPort !== undefined;\n endpoints.push({\n kind: 'web',\n name: webServiceName ?? 'web',\n containerPort: record.webContainerPort ?? WEB_CONTAINER_PORT,\n ...(hasTarget\n ? { url: `http://127.0.0.1:${String(record.webHostPort)}`, reachable: true }\n : { reachable: false }),\n });\n }\n\n return { domain, domainIsOrb, endpoints };\n}\n"],"mappings":"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","names":["join","join"]}
|