@madarco/agentbox 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_cloud-attach-T727ZPRV.js +13 -0
- package/dist/chunk-67N47KUS.js +1640 -0
- package/dist/chunk-67N47KUS.js.map +1 -0
- package/dist/chunk-6OZDFNBF.js +8114 -0
- package/dist/chunk-6OZDFNBF.js.map +1 -0
- package/dist/chunk-BGK32PZE.js +455 -0
- package/dist/chunk-BGK32PZE.js.map +1 -0
- package/dist/chunk-FODMEHD3.js +1200 -0
- package/dist/chunk-FODMEHD3.js.map +1 -0
- package/dist/chunk-G3H2L3O2.js +288 -0
- package/dist/chunk-G3H2L3O2.js.map +1 -0
- package/dist/chunk-I24B6AXR.js +600 -0
- package/dist/chunk-I24B6AXR.js.map +1 -0
- package/dist/chunk-LEV3KICD.js +738 -0
- package/dist/chunk-LEV3KICD.js.map +1 -0
- package/dist/cloud-poller-SUNA6ZQC-2RG5WPRN.js +10 -0
- package/dist/dist-L4LCG5SJ.js +293 -0
- package/dist/dist-L4LCG5SJ.js.map +1 -0
- package/dist/dist-LOZBWMBF.js +447 -0
- package/dist/dist-ZODPD2I6.js +1407 -0
- package/dist/dist-ZODPD2I6.js.map +1 -0
- package/dist/index.js +7281 -2134
- package/dist/index.js.map +1 -1
- package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js +18 -0
- package/package.json +8 -3
- package/runtime/daytona/custom-system-CLAUDE.md +39 -0
- package/runtime/docker/Dockerfile.box +120 -14
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +15 -8
- package/runtime/docker/packages/ctl/dist/bin.cjs +11310 -816
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +68 -0
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +9 -9
- package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
- package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
- package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
- package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
- package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
- package/runtime/hetzner/agentbox-codex-hooks.json +68 -0
- package/runtime/hetzner/agentbox-dockerd-start +132 -0
- package/runtime/hetzner/agentbox-open +28 -0
- package/runtime/hetzner/agentbox-setup-skill.md +196 -0
- package/runtime/hetzner/agentbox-vnc-start +77 -0
- package/runtime/hetzner/claude-managed-settings.json +115 -0
- package/runtime/hetzner/ctl.cjs +23397 -0
- package/runtime/hetzner/custom-system-CLAUDE.md +39 -0
- package/runtime/hetzner/gh-shim +263 -0
- package/runtime/hetzner/git-shim +131 -0
- package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
- package/runtime/hetzner/scripts/install-box.sh +374 -0
- package/runtime/relay/bin.cjs +10017 -817
- package/share/agentbox-setup/SKILL.md +15 -8
- package/share/host-skills/agentbox/SKILL.md +29 -0
- package/share/host-skills/agentbox-info/SKILL.md +211 -0
- package/share/host-skills/codex/agentbox.md +35 -0
- package/share/host-skills/opencode/agentbox.md +26 -0
- package/dist/chunk-BBZMA2K6.js +0 -238
- package/dist/chunk-BBZMA2K6.js.map +0 -1
- package/dist/chunk-HHMWQNLF.js +0 -1709
- package/dist/chunk-HHMWQNLF.js.map +0 -1
- package/dist/chunk-HPZMD5DE.js +0 -106
- package/dist/chunk-HPZMD5DE.js.map +0 -1
- package/dist/chunk-HTTKML3C.js +0 -2655
- package/dist/chunk-HTTKML3C.js.map +0 -1
- package/dist/chunk-KJNZP6I3.js +0 -586
- package/dist/chunk-KJNZP6I3.js.map +0 -1
- package/dist/chunk-M7I247BK.js +0 -525
- package/dist/chunk-M7I247BK.js.map +0 -1
- package/dist/create-6PWXI6HO-OWAMHBAK.js +0 -15
- package/dist/lifecycle-EMXR46DI-DUVBXNTV.js +0 -38
- package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
- package/dist/stats-SZXOJE3D-N7OODCHW.js +0 -19
- /package/dist/{create-6PWXI6HO-OWAMHBAK.js.map → _cloud-attach-T727ZPRV.js.map} +0 -0
- /package/dist/{lifecycle-EMXR46DI-DUVBXNTV.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
- /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-LOZBWMBF.js.map} +0 -0
- /package/dist/{stats-SZXOJE3D-N7OODCHW.js.map → prepared-state-CL4CWXQA-ME4HSKDE.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../packages/sandbox-docker/src/claude.ts","../../../packages/sandbox-docker/src/claude-hooks-filter.ts","../../../packages/sandbox-docker/src/claude-pull.ts","../../../packages/sandbox-docker/src/dockerd.ts","../../../packages/sandbox-docker/src/vnc.ts","../../../packages/sandbox-docker/src/web.ts","../../../packages/sandbox-docker/src/git-worktree.ts","../../../packages/sandbox-docker/src/in-box-git.ts","../../../packages/sandbox-docker/src/snapshot.ts","../../../packages/sandbox-docker/src/ctl.ts","../../../packages/sandbox-docker/src/home-ownership.ts","../../../packages/sandbox-docker/src/relay.ts","../../../packages/sandbox-docker/src/vscode.ts","../../../packages/relay/src/types.ts","../../../packages/relay/src/registry.ts","../../../packages/relay/src/prompts.ts","../../../packages/relay/src/notices.ts","../../../packages/relay/src/status-store.ts","../../../packages/relay/src/server.ts","../../../packages/relay/src/autopause.ts","../../../packages/ctl/src/types.ts","../../../packages/ctl/src/client.ts","../../../packages/ctl/src/render.ts","../../../packages/ctl/src/config.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';\nimport { homedir, tmpdir } from 'node:os';\nimport { join, relative } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { execa } from 'execa';\nimport {\n addProjectAlias,\n filterHostHooks,\n setInstallMethodNative,\n trustWorkspace,\n} from './claude-hooks-filter.js';\nimport {\n mergeInstalledPlugins,\n mergeKnownMarketplaces,\n pickNewItems,\n referencedPluginVersionKeys,\n SKILL_EXCLUDE_PREFIXES,\n} from './claude-pull.js';\nimport { ensureVolume, volumeExists } from './docker.js';\nimport { detectEngine, orbstackVolumePath } from './host-export.js';\n\nexport const SHARED_CLAUDE_VOLUME = 'agentbox-claude-config';\nexport const DEFAULT_CLAUDE_SESSION = 'claude';\nconst CONTAINER_CLAUDE_DIR = '/home/vscode/.claude';\nexport const CONTAINER_USER = 'vscode';\n/** Workspace is always mounted here inside the box, regardless of host path. */\nconst CONTAINER_WORKSPACE = '/workspace';\n/**\n * Image-baked copy of the agentbox-setup skill (Dockerfile.box COPYs\n * `apps/cli/share/agentbox-setup/SKILL.md` here). We seed it into the\n * claude-config volume so `/agentbox-setup` is available *inside boxes only* —\n * it is intentionally never written to the host's ~/.claude.\n */\nconst IN_BOX_SETUP_GUIDE_PATH = '/usr/local/share/agentbox/setup-guide.md';\n/** Destination skill file inside the claude-config volume (mounted at /dst). */\nconst SETUP_SKILL_DST = '/dst/skills/agentbox-setup/SKILL.md';\n\nexport interface ClaudeConfigSpec {\n /** Resolved Docker volume name mounted at /home/vscode/.claude. */\n volume: string;\n}\n\nexport function resolveClaudeVolume(opts: { isolate: boolean; boxId: string }): ClaudeConfigSpec {\n if (opts.isolate) {\n return { volume: `${SHARED_CLAUDE_VOLUME}-${opts.boxId}` };\n }\n return { volume: SHARED_CLAUDE_VOLUME };\n}\n\nexport interface EnsureClaudeVolumeOptions {\n /**\n * When true and the host's ~/.claude exists, rsync host -> volume on every call.\n * Sync is additive: files present on host overwrite same-named files in the\n * volume; box-only files (e.g. `projects/<hash>/*.jsonl` session history written\n * inside earlier boxes) are preserved.\n */\n syncFromHost: boolean;\n /** Image used by the throwaway sync helper container; we use the box image to avoid extra pulls. */\n image: string;\n /**\n * Host-absolute path of the workspace being bound to /workspace inside the\n * box. When provided, the synced `_claude.json` gets `projects[<hostWorkspace>]`\n * duplicated to `projects['/workspace']` so project-scoped MCP servers,\n * trust state, and history match what the host has for this project.\n */\n hostWorkspace?: string;\n}\n\nexport interface EnsureClaudeVolumeResult {\n /** True only the very first time the volume is created (on this host). */\n created: boolean;\n /** True when the rsync helper actually ran (syncFromHost was true AND host ~/.claude existed). */\n synced: boolean;\n /**\n * Number of hook entries dropped during sync because their `command` pointed\n * at a host path (under `$HOME/`) that wouldn't exist inside the container.\n * 0 when nothing was filtered or no sync ran.\n */\n filteredHookCount?: number;\n /**\n * True when the synced `_claude.json` had its install-method fields\n * (installMethod / autoUpdates / autoUpdatesProtectedForNative) coerced\n * to match the box's native install. False when they already matched.\n */\n installMethodFixed?: boolean;\n /**\n * True when `projects[<hostWorkspace>]` was duplicated to\n * `projects['/workspace']` in the synced `_claude.json` so the in-box claude\n * sees the host's project-scoped state (mcpServers, history, …).\n */\n aliasedProjectKey?: boolean;\n /**\n * True when `projects['/workspace'].hasTrustDialogAccepted` was set to `true`\n * in the synced `_claude.json` (it wasn't already). Pre-trusting the box's\n * workspace skips the trust dialog and avoids the Claude Code untrusted-\n * workspace `400 role 'system'` bug.\n */\n workspaceTrusted?: boolean;\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/**\n * True when the claude-config volume already holds a `_claude.json` at its\n * root. Claude Code treats `~/.claude.json` as its own mutable runtime/auth\n * state (`oauthAccount`, `userID`, onboarding flags, ...). Once the volume has\n * one — written by an earlier box session or the throwaway `claude auth login`\n * container — overwriting it with the host's copy clobbers that state, and the\n * box's first API request then fails with `400 \"system\" role is not supported`.\n * Callers use this to keep `_claude.json` write-once.\n */\nasync function volumeHasClaudeJson(volume: string, image: string): Promise<boolean> {\n const res = await execa(\n 'docker',\n ['run', '--rm', '-v', `${volume}:/dst`, image, 'sh', '-c', 'test -e /dst/_claude.json'],\n { reject: false },\n );\n return res.exitCode === 0;\n}\n\n/**\n * Walk `root` and return rsync-style relative paths of every symlink whose\n * target doesn't resolve. We pass these to rsync as `--exclude` patterns so\n * the broken-symlink set (e.g. claude's `debug/latest` once an older debug\n * file is reaped) doesn't abort the whole sync under `--copy-unsafe-links`.\n *\n * Crosses into subdirs; doesn't follow symlinks (the whole point is to test\n * them rather than traverse them).\n */\nasync function findBrokenSymlinks(root: string): Promise<string[]> {\n const broken: string[] = [];\n async function walk(dir: string): Promise<void> {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const ent of entries) {\n const full = join(dir, ent.name);\n if (ent.isSymbolicLink()) {\n try {\n await stat(full);\n } catch {\n broken.push(relative(root, full));\n }\n } else if (ent.isDirectory()) {\n await walk(full);\n }\n }\n }\n await walk(root);\n return broken;\n}\n\n/**\n * Ensure the named volume exists, then (when {@link EnsureClaudeVolumeOptions.syncFromHost}\n * is true and the host has a `~/.claude` directory) rsync host -> volume via a throwaway\n * helper container. The host is treated as the authoritative source for config:\n * settings, auth token, skills, plugins, and MCP entries on the host overwrite the\n * same-named files in the volume on every call. Files that only exist in the volume\n * (in-box session history under `projects/`, statsig cache, etc.) are preserved —\n * rsync runs without `--delete`.\n *\n * Caveat: if another box is currently running with the same shared volume mounted,\n * the rsync can change config files under it mid-session. We accept this as part of\n * \"host is authoritative\" — per-box state under `projects/` is untouched, so the\n * effect is limited to overlapping config files (rare to be edited live).\n *\n * Returns `created: true` only on the very first run for this volume; `synced: true`\n * whenever the rsync actually executed.\n */\nexport async function ensureClaudeVolume(\n spec: ClaudeConfigSpec,\n opts: EnsureClaudeVolumeOptions,\n): Promise<EnsureClaudeVolumeResult> {\n const existed = await volumeExists(spec.volume);\n await ensureVolume(spec.volume);\n const created = !existed;\n\n if (!opts.syncFromHost) return { created, synced: false };\n\n const hostClaude = join(homedir(), '.claude');\n if (!(await pathExists(hostClaude))) return { created, synced: false };\n\n // rsync (not cp -a) so repeat syncs skip unchanged files. rsync is installed in\n // the box image (Dockerfile.box). Trailing slash on /src-claude/ means\n // \"contents of src\", matching the original cp -a /src/. /dst/ semantics.\n // We run as root (--user 0) because the volume's existing content may be a\n // mix of UIDs (host's macOS UID for files copied from ~/.claude, plus\n // vscode's UID 1000 for anything claude wrote inside a box); only root can\n // rewrite arbitrary ownership. The post-chown brings everything back to\n // UID 1000 so the in-box vscode user can read/write.\n //\n // We also pull in ~/.claude.json (the *file* at home root that Claude Code\n // uses for global state: hasCompletedOnboarding, anonymousId, oauthAccount,\n // plugin caches). It's not inside ~/.claude, so we bind-mount it separately\n // (when present) and copy it into the volume as _claude.json. A symlink\n // baked into the image (/home/vscode/.claude.json -> .../_claude.json)\n // makes it reachable from the path claude expects.\n const hostClaudeJson = join(homedir(), '.claude.json');\n const hasJson = await pathExists(hostClaudeJson);\n // `_claude.json` is write-once in the volume. The first writer wins — the\n // throwaway `claude auth login` container (seeded just before it via this\n // same function), or an earlier box session. Re-copying the host's\n // ~/.claude.json on every create/start would clobber Claude's own\n // `oauthAccount` and break the box's first request (see volumeHasClaudeJson).\n const seedClaudeJson = !(await volumeHasClaudeJson(spec.volume, opts.image));\n const hostHome = homedir();\n // Claude Code's user-skills convention: ~/.claude/skills/<name> is a\n // RELATIVE symlink to ../../.agents/skills/<name>. From /src-claude/skills/\n // inside the helper that resolves to /.agents/skills/<name>. Bind-mount the\n // host's ~/.agents at /.agents so --copy-unsafe-links can dereference each\n // symlink into a real directory in /dst. Without this, rsync errors with\n // \"symlink has no referent\" and the whole sync aborts.\n const hostAgents = join(homedir(), '.agents');\n const hasAgents = await pathExists(hostAgents);\n const args: string[] = [\n 'run',\n '--rm',\n '--user',\n '0',\n // HOST_HOME used inside the shell script to rewrite host-absolute\n // installPath values in plugins/installed_plugins.json.\n '-e',\n `HOST_HOME=${hostHome}`,\n '-v',\n `${spec.volume}:/dst`,\n '-v',\n `${hostClaude}:/src-claude:ro`,\n ];\n if (hasJson && seedClaudeJson) args.push('-v', `${hostClaudeJson}:/src-claude-json:ro`);\n if (hasAgents) args.push('-v', `${hostAgents}:/.agents:ro`);\n\n // Pre-filter host-path hooks. Hook commands whose path is under the user's\n // host home (e.g. `/Users/marco/.config/iterm2/cc-status`) won't exist\n // inside the Linux container, and Claude logs a noisy\n // `SessionStart:startup hook error /bin/sh: …: not found` every time. We\n // build a small tempdir with filtered copies of `settings.json` /\n // `.claude.json`, mount it as `/src-filter`, and let the helper container\n // overlay it on top of what rsync brought in. The host files are never\n // touched.\n const filterDir = await mkdtemp(join(tmpdir(), 'agentbox-claude-filter-'));\n let filteredHookCount = 0;\n let installMethodFixed = false;\n let aliasedProjectKey = false;\n let workspaceTrusted = false;\n try {\n const settingsResult = await maybeFilterTo(\n join(hostClaude, 'settings.json'),\n join(filterDir, 'settings.json'),\n hostHome,\n );\n filteredHookCount += settingsResult.removedHooks;\n if (!seedClaudeJson) {\n // The volume already has a `_claude.json`; write-once leaves it intact\n // (see seedClaudeJson). No host overlay is generated for it — the\n // settings.json filtering above still applies.\n } else if (hasJson) {\n const jsonResult = await maybeFilterTo(\n hostClaudeJson,\n join(filterDir, '_claude.json'),\n hostHome,\n {\n setInstallMethodNative: true,\n aliasProject: opts.hostWorkspace\n ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE }\n : undefined,\n trustWorkspacePath: CONTAINER_WORKSPACE,\n },\n );\n filteredHookCount += jsonResult.removedHooks;\n installMethodFixed = jsonResult.installMethodFixed;\n aliasedProjectKey = jsonResult.aliasedProjectKey;\n workspaceTrusted = jsonResult.workspaceTrusted;\n } else {\n // Host has no ~/.claude.json. Write a minimal _claude.json directly to\n // the filter dir so the in-box claude still gets installMethod=native\n // (skips the integrity warning) and a pre-trusted /workspace (skips the\n // trust dialog — and avoids the Claude Code bug where an untrusted\n // workspace yields `400 role 'system' is not supported on this model`).\n await writeFile(\n join(filterDir, '_claude.json'),\n JSON.stringify(\n {\n installMethod: 'native',\n autoUpdates: false,\n autoUpdatesProtectedForNative: true,\n projects: { [CONTAINER_WORKSPACE]: { hasTrustDialogAccepted: true } },\n },\n null,\n 2,\n ),\n );\n installMethodFixed = true;\n workspaceTrusted = true;\n }\n if (filteredHookCount > 0 || installMethodFixed || aliasedProjectKey || workspaceTrusted) {\n args.push('-v', `${filterDir}:/src-filter:ro`);\n }\n // Pre-scan for broken symlinks. With --copy-unsafe-links rsync errors out\n // and exits 23 when any unsafe symlink's referent is missing — e.g.\n // `~/.claude/debug/latest` regularly points to a debug file that's been\n // reaped. We can't predict every such case, so we walk once and tell\n // rsync to skip exactly those entries.\n const brokenSymlinks = await findBrokenSymlinks(hostClaude);\n const rsyncExcludes = ['--exclude=node_modules'];\n for (const rel of brokenSymlinks) rsyncExcludes.push(`--exclude=/${rel}`);\n const rsyncFlags = `-a --copy-unsafe-links ${rsyncExcludes.join(' ')}`;\n args.push(\n opts.image,\n 'sh',\n '-c',\n // Each step in its own brace group so a missing optional file (no\n // .claude.json on host, no filtered overlays) doesn't short-circuit the\n // final chown.\n //\n // --copy-unsafe-links: dereference symlinks pointing OUTSIDE\n // /src-claude (e.g. ~/.claude/skills/* -> ../../.agents/skills/*),\n // so user skills materialize as real directories inside the volume\n // without needing to also bind-mount ~/.agents.\n // --exclude=node_modules: skip every node_modules directory anywhere\n // in the tree. Plugin caches (plugins/cache/<m>/<p>/<v>/node_modules)\n // ship host-platform-specific binaries (darwin-arm64 fsevents,\n // esbuild, rollup, sharp) that are useless on linux/amd64. The\n // plugin source still lands; node_modules is rebuilt lazily inside\n // the box on first claude session (see rebuildPluginNativeDeps).\n //\n // The top-level plugin registry JSONs (installed_plugins.json,\n // known_marketplaces.json) carry host-absolute `installPath` /\n // `installLocation` values; without rewriting, claude resolves them\n // to `/Users/<you>/...` (or, when claude detects the missing path,\n // falls back to a slug derived from `source.repo` like\n // `microsoft-playwright-cli` — neither exists in the box, and the\n // marketplace fails to load, which masquerades as \"plugin not\n // found in marketplace\"). One sweep over every JSON directly under\n // /dst/plugins/ catches both files (and any future registry).\n // One-shot migration for volumes that were populated before\n // --exclude=node_modules existed. Without it, the volume keeps\n // host-darwin node_modules forever (rsync without --delete won't\n // remove them). The `.agentbox-cleaned-nm-v1` sentinel makes the wipe\n // a no-op after the first run; rebuildPluginNativeDeps repopulates\n // linux/amd64 node_modules on the next `agentbox claude`.\n '{ [ ! -f /dst/.agentbox-cleaned-nm-v1 ] && ' +\n 'find /dst -name node_modules -type d -prune -exec rm -rf {} + && ' +\n 'touch /dst/.agentbox-cleaned-nm-v1; true; }' +\n ` && rsync ${rsyncFlags} /src-claude/ /dst/` +\n ' && { [ -f /src-claude-json ] && cp -a /src-claude-json /dst/_claude.json; true; }' +\n ' && { [ -f /src-filter/settings.json ] && cp -a /src-filter/settings.json /dst/settings.json; true; }' +\n ' && { [ -f /src-filter/_claude.json ] && cp -a /src-filter/_claude.json /dst/_claude.json; true; }' +\n ' && { [ -d /dst/plugins ] && [ -n \"$HOST_HOME\" ] && ' +\n 'find /dst/plugins -maxdepth 1 -type f -name \"*.json\" ' +\n '-exec sed -i \"s|$HOST_HOME/.claude/plugins/|/home/vscode/.claude/plugins/|g\" {} +; true; }' +\n ' && chown -R 1000:1000 /dst',\n );\n await execa('docker', args);\n } finally {\n await rm(filterDir, { recursive: true, force: true });\n }\n\n return {\n created,\n synced: true,\n filteredHookCount,\n installMethodFixed,\n aliasedProjectKey,\n workspaceTrusted,\n };\n}\n\n/**\n * Seed the `agentbox-setup` skill into the claude-config volume from the\n * image-baked copy ({@link IN_BOX_SETUP_GUIDE_PATH}). This is the box-only\n * install path: the skill is intentionally never written to the host's\n * ~/.claude (so `agentbox claude` doesn't pollute the user's machine).\n *\n * Independent of `ensureClaudeVolume`'s host rsync — it runs even when the\n * host has no ~/.claude or `syncFromHost` was false. The skill is\n * agentbox-owned and image-versioned (not user-customizable, excluded from\n * the host<->box sync), so we re-copy it unconditionally: a stale copy in a\n * long-lived shared volume must not pin an old skill after an image upgrade.\n *\n * Best-effort: a failure here must not fail box creation.\n */\nexport async function seedSetupSkillIntoVolume(\n volume: string,\n image: string,\n): Promise<{ seeded: boolean }> {\n try {\n const { stdout } = await execa('docker', [\n 'run',\n '--rm',\n '--user',\n '0',\n '-v',\n `${volume}:/dst`,\n image,\n 'sh',\n '-c',\n // Always overwrite from the image so an image upgrade propagates. Prints\n // SEEDED on success; the whole thing is `|| true` so a missing image\n // asset is a clean no-op, never a non-zero exit.\n `{ [ -f ${IN_BOX_SETUP_GUIDE_PATH} ] && ` +\n `rm -rf /dst/skills/agentbox-setup && ` +\n `mkdir -p /dst/skills/agentbox-setup && ` +\n `cp -a ${IN_BOX_SETUP_GUIDE_PATH} ${SETUP_SKILL_DST} && ` +\n `chown -R 1000:1000 /dst/skills/agentbox-setup && echo SEEDED; } || true`,\n ]);\n return { seeded: stdout.includes('SEEDED') };\n } catch {\n return { seeded: false };\n }\n}\n\n/**\n * Read a JSON file, run it through {@link filterHostHooks}, (when opted in)\n * {@link setInstallMethodNative}, {@link addProjectAlias}, and\n * {@link trustWorkspace}, and write the result to `dest` ONLY when at least\n * one change was made. Tolerant of missing or garbage JSON — silently returns\n * zero changes in those cases (sync proceeds with the raw rsync'd file).\n */\nasync function maybeFilterTo(\n src: string,\n dest: string,\n hostHome: string,\n opts: {\n setInstallMethodNative?: boolean;\n aliasProject?: { from: string; to: string };\n trustWorkspacePath?: string;\n } = {},\n): Promise<{\n removedHooks: number;\n installMethodFixed: boolean;\n aliasedProjectKey: boolean;\n workspaceTrusted: boolean;\n}> {\n const zero = {\n removedHooks: 0,\n installMethodFixed: false,\n aliasedProjectKey: false,\n workspaceTrusted: false,\n };\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(src, 'utf8'));\n } catch {\n return zero;\n }\n const filtered = filterHostHooks(parsed, hostHome);\n let working: unknown = filtered.data;\n let installFixed = false;\n if (opts.setInstallMethodNative) {\n const r = setInstallMethodNative(working);\n working = r.data;\n installFixed = r.applied;\n }\n let aliased = false;\n if (opts.aliasProject) {\n const r = addProjectAlias(working, opts.aliasProject.from, opts.aliasProject.to);\n working = r.data;\n aliased = r.aliased;\n }\n let trusted = false;\n if (opts.trustWorkspacePath) {\n const r = trustWorkspace(working, opts.trustWorkspacePath);\n working = r.data;\n trusted = r.trusted;\n }\n if (filtered.removedCommands.length === 0 && !installFixed && !aliased && !trusted) {\n return zero;\n }\n await writeFile(dest, JSON.stringify(working, null, 2));\n return {\n removedHooks: filtered.removedCommands.length,\n installMethodFixed: installFixed,\n aliasedProjectKey: aliased,\n workspaceTrusted: trusted,\n };\n}\n\nexport interface ClaudeMountResult {\n /** Docker -v spec strings to append to runBox(extraVolumes). */\n extraVolumes: string[];\n /** Env vars to forward into the container; only includes keys that were set + non-empty on the host. */\n env: Record<string, string>;\n volumeName: string;\n}\n\n// Forwarded from the host's `process.env` into the box at `docker run -e` time\n// (and re-forwarded by `startClaudeSession` at `docker exec -e` time, so a\n// later `agentbox claude start <existing-box>` picks up the host's current\n// session env even when the container was created from a different shell).\n//\n// CLAUDE_EFFORT / ANTHROPIC_MODEL: Claude Code stores the user's model\n// selection (Opus/Sonnet/Haiku via /model or --effort) only in the parent\n// claude's process env — not in `~/.claude.json` or `~/.claude/settings.json`.\n// When the user invokes `agentbox claude` from inside their host claude\n// session, that env IS present in the calling shell; forwarding it is the\n// only way the in-box claude inherits the same model default.\nconst FORWARDED_ENV_KEYS = [\n 'ANTHROPIC_API_KEY',\n 'CLAUDE_CODE_OAUTH_TOKEN',\n 'CLAUDE_EFFORT',\n 'ANTHROPIC_MODEL',\n] as const;\n\nexport function buildClaudeMounts(\n spec: ClaudeConfigSpec,\n hostEnv: NodeJS.ProcessEnv,\n): ClaudeMountResult {\n const env: Record<string, string> = {};\n for (const k of FORWARDED_ENV_KEYS) {\n const v = hostEnv[k];\n if (typeof v === 'string' && v.length > 0) env[k] = v;\n }\n return {\n extraVolumes: [`${spec.volume}:${CONTAINER_CLAUDE_DIR}`],\n env,\n volumeName: spec.volume,\n };\n}\n\nexport interface RebuildPluginNativeDepsResult {\n /** Plugin cache directories whose node_modules was (re)installed during this call. */\n rebuilt: string[];\n /** Plugin cache directories where install failed; non-fatal, claude often still loads. */\n failed: Array<{ dir: string; stderr: string }>;\n /**\n * Stale plugin-version cache dirs (`<m>/<p>/<v>`, not referenced by\n * `installed_plugins.json`) whose `node_modules` was pruned during this call.\n */\n pruned: string[];\n /** Total bytes freed by {@link RebuildPluginNativeDepsResult.pruned}. */\n prunedBytes: number;\n /**\n * True when the in-box exec was skipped entirely because a host-side scan\n * proved every package.json-bearing plugin already carries its install\n * marker. Only possible when the volume is host-visible (OrbStack).\n */\n skipped: boolean;\n}\n\n/** Per-plugin sentinel written inside the cache dir after a successful install. */\nconst PLUGIN_INSTALLED_MARKER = '.agentbox-installed';\n\n/**\n * Per-plugin sentinel written (mtime = failure time) when an install fails. A\n * plugin with a *recent* fail marker is skipped instead of retried on every\n * launch; once the marker ages past {@link PLUGIN_INSTALL_BACKOFF_MS} it's\n * retried. Cleared on a later success.\n */\nconst PLUGIN_FAILED_MARKER = '.agentbox-install-failed';\n\n/** How long a failed plugin install is skipped before it's retried. */\nconst PLUGIN_INSTALL_BACKOFF_MS = 6 * 60 * 60 * 1000;\n\n/** Backoff window in whole minutes, for the in-box `find -mmin` recency test. */\nconst PLUGIN_INSTALL_BACKOFF_MIN = Math.round(PLUGIN_INSTALL_BACKOFF_MS / 60000);\n\n/**\n * Persistent npm cache, kept inside the claude-config volume so a given\n * package@version is fetched from the registry once *globally* and reused by\n * every later box and plugin version. Shared across boxes with the default\n * shared volume; per-box only under `--isolate-claude-config`. Not named\n * `node_modules`, so the one-time node_modules cleanup migration leaves it\n * alone; the host->volume rsync is additive (won't delete it) and `pull claude`\n * only pulls skills/plugins/agents/commands (won't drag it to the host).\n */\nconst NPM_CACHE_DIR = '/home/vscode/.claude/.agentbox-npm-cache';\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/** True when `p` exists and its mtime is within the install-backoff window. */\nasync function isRecentFailMarker(p: string): Promise<boolean> {\n try {\n const st = await stat(p);\n return Date.now() - st.mtimeMs < PLUGIN_INSTALL_BACKOFF_MS;\n } catch {\n return false;\n }\n}\n\nasync function isDir(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Read `installed_plugins.json` next to a plugin cache dir and reduce it to the\n * set of `<m>/<p>/<v>` version keys it actively references. Missing/unparseable\n * file -> empty set (\"can't determine\"), which callers treat as \"apply no\n * reference-based filtering\".\n */\nasync function readReferencedPluginKeys(installedPluginsJsonPath: string): Promise<Set<string>> {\n try {\n const raw = await readFile(installedPluginsJsonPath, 'utf8');\n return referencedPluginVersionKeys(JSON.parse(raw) as unknown);\n } catch {\n return new Set<string>();\n }\n}\n\n/**\n * Pure host-side scan of a plugin `cache/<m>/<p>/<v>/` tree. Returns true iff\n * at least one version dir has a `package.json`, no install marker, and no\n * *recent* failure marker — i.e. the in-box rebuild would actually do npm\n * work. A missing/empty cache root means nothing to do (false). Mirrors the\n * in-box script's accept/skip rules (`packages/sandbox-docker/src/claude.ts`\n * rebuild script) so the host pre-check and the container never disagree.\n *\n * When the sibling `installed_plugins.json` yields a non-empty referenced set,\n * unreferenced version dirs are ignored here exactly as the in-box loop skips\n * installing them (prevention) — a stale dir is never \"rebuild work\".\n */\nexport async function scanPluginCacheForRebuild(cacheRoot: string): Promise<boolean> {\n const referenced = await readReferencedPluginKeys(\n join(cacheRoot, '..', 'installed_plugins.json'),\n );\n let marketplaces;\n try {\n marketplaces = await readdir(cacheRoot, { withFileTypes: true });\n } catch {\n return false;\n }\n for (const m of marketplaces) {\n if (!m.isDirectory()) continue;\n const mPath = join(cacheRoot, m.name);\n let plugins;\n try {\n plugins = await readdir(mPath, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const p of plugins) {\n if (!p.isDirectory()) continue;\n const pPath = join(mPath, p.name);\n let versions;\n try {\n versions = await readdir(pPath, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const v of versions) {\n if (!v.isDirectory()) continue;\n if (referenced.size > 0 && !referenced.has(`${m.name}/${p.name}/${v.name}`)) continue;\n const vPath = join(pPath, v.name);\n if (!(await isFile(join(vPath, 'package.json')))) continue;\n if (await isFile(join(vPath, PLUGIN_INSTALLED_MARKER))) continue;\n if (await isRecentFailMarker(join(vPath, PLUGIN_FAILED_MARKER))) continue;\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Host-visible `plugins/cache` dir for a claude-config volume, or null when the\n * engine doesn't expose volume contents to the host (Docker Desktop / other).\n * Returns the cache path even if it doesn't exist yet — {@link\n * scanPluginCacheForRebuild} treats a missing cache as \"nothing to do\" — but\n * only once the volume itself is materialized on the host.\n */\nasync function resolveClaudeCacheLiveOnHost(volume: string): Promise<string | null> {\n if ((await detectEngine()) !== 'orbstack') return null;\n if (!(await isDir(orbstackVolumePath(volume)))) return null;\n return orbstackVolumePath(volume, 'plugins', 'cache');\n}\n\n/**\n * Walk `/home/vscode/.claude/plugins/cache/<m>/<p>/<v>/` inside the box and run\n * `npm install` (or `npm ci` when a lockfile is present) for any plugin that\n * ships a `package.json` but hasn't been installed yet. Marker-gated (not\n * node_modules — plugins with empty dep lists install cleanly without ever\n * creating a node_modules dir, so a dir check would loop forever).\n *\n * This exists because the host→volume rsync excludes `node_modules` (host\n * darwin-arm64 native binaries like fsevents.node / @esbuild/darwin-arm64\n * are useless on the linux/amd64 box). The first claude session in a fresh\n * box pays the install cost; subsequent attaches don't.\n *\n * Three things keep this fast: installs run **in parallel** (bounded), npm\n * shares a **persistent cache in the claude-config volume** ({@link\n * NPM_CACHE_DIR}) with `--prefer-offline` so a package@version is fetched once\n * globally, and a failed plugin records {@link PLUGIN_FAILED_MARKER} so it's\n * skipped (not retried) until {@link PLUGIN_INSTALL_BACKOFF_MS} elapses.\n *\n * Failures on individual plugins are reported but don't throw — most\n * plugins still load with a partial dependency graph, and we prefer\n * launching claude over blocking on a third-party plugin's install hiccup.\n *\n * When this pass runs at all (i.e. a plugin difference was detected, so a\n * rebuild is warranted) it also **prunes** stale plugin-version dirs: when\n * Claude updates a plugin it leaves the old `cache/<m>/<p>/<v>/` dir on disk,\n * and its ~hundreds-of-MB `node_modules` would otherwise live forever in the\n * shared claude-config volume. Any version dir not referenced by\n * `installed_plugins.json` has its `node_modules` (and our markers) removed,\n * and the install loop never (re)installs into an unreferenced dir.\n */\nasync function readBoxReferencedPluginKeys(container: string): Promise<Set<string>> {\n const res = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n CONTAINER_USER,\n container,\n 'cat',\n `${CONTAINER_CLAUDE_DIR}/plugins/installed_plugins.json`,\n ],\n { reject: false },\n );\n if (res.exitCode !== 0 || !res.stdout) return new Set<string>();\n try {\n return referencedPluginVersionKeys(JSON.parse(res.stdout) as unknown);\n } catch {\n return new Set<string>();\n }\n}\n\nexport async function rebuildPluginNativeDeps(\n container: string,\n opts: {\n onProgress?: (line: string) => void;\n /**\n * The claude-config volume backing this box. When given and host-visible\n * (OrbStack), a pure-fs pre-scan skips the `docker exec` entirely if every\n * package.json plugin already has its install marker — the common case for\n * every box after the first global install.\n */\n volume?: string;\n } = {},\n): Promise<RebuildPluginNativeDepsResult> {\n if (opts.volume) {\n const cacheRoot = await resolveClaudeCacheLiveOnHost(opts.volume);\n if (cacheRoot && !(await scanPluginCacheForRebuild(cacheRoot))) {\n return { rebuilt: [], failed: [], pruned: [], prunedBytes: 0, skipped: true };\n }\n }\n // Reference set from the box's installed_plugins.json: version dirs Claude no\n // longer points at are stale. An empty set (file missing / unparseable)\n // disables both prevention and the prune pass — the script then behaves\n // exactly as it did before this feature.\n const referenced = await readBoxReferencedPluginKeys(container);\n const refSetup =\n referenced.size > 0\n ? `cat <<'AGENTBOX_REF_EOF' > \"$WORK/referenced\"\\n${[...referenced].sort().join('\\n')}\\nAGENTBOX_REF_EOF\\n`\n : '';\n // The host parser below expects the REBUILD_START / REBUILD_OK /\n // REBUILD_FAIL..REBUILD_FAIL_END protocol (plus PRUNE_OK lines); parallel\n // jobs write per-dir result+stderr files and we replay them after `wait`.\n const script = `set -u\nPLUGINS_DIR=/home/vscode/.claude/plugins/cache\nMARKER=${PLUGIN_INSTALLED_MARKER}\nFAILMARKER=${PLUGIN_FAILED_MARKER}\nNPM_CACHE=${NPM_CACHE_DIR}\nBACKOFF_MIN=${PLUGIN_INSTALL_BACKOFF_MIN}\nMAX=4\n[ -d \"$PLUGINS_DIR\" ] || exit 0\nmkdir -p \"$NPM_CACHE\"\nWORK=\\$(mktemp -d)\n${refSetup}relkey() { printf '%s' \"\\${1#$PLUGINS_DIR/}\" | tr '/' '_'; }\n# True when refs are unknown (no file) or $1 (<m>/<p>/<v>) is referenced.\nis_referenced() {\n [ -s \"$WORK/referenced\" ] || return 0\n grep -Fxq \"$1\" \"$WORK/referenced\"\n}\n# Run one plugin's install. $1 is frozen by value at call time, so it's safe\n# to read from the backgrounded subshell; the rest are set-once constants.\ndo_one() {\n d=\\$1\n key=\\$(relkey \"$d\")\n if (cd \"$d\" && \\\\\n if [ -f package-lock.json ]; then \\\\\n npm ci --no-audit --no-fund --silent --prefer-offline --cache \"$NPM_CACHE\"; \\\\\n else \\\\\n npm install --no-audit --no-fund --silent --no-package-lock --prefer-offline --cache \"$NPM_CACHE\"; \\\\\n fi) >\"$WORK/$key.out\" 2>\"$WORK/$key.err\"; then\n touch \"$d/$MARKER\"\n rm -f \"$d/$FAILMARKER\"\n printf 'OK\\\\n' > \"$WORK/$key.res\"\n else\n : > \"$d/$FAILMARKER\"\n printf 'FAIL\\\\n' > \"$WORK/$key.res\"\n fi\n}\n# Prune pass: every unreferenced (stale) version dir loses its node_modules and\n# our markers. Only runs when installed_plugins.json gave us a reference set.\nif [ -s \"$WORK/referenced\" ]; then\n for dir in \"$PLUGINS_DIR\"/*/*/*/; do\n [ -d \"$dir\" ] || continue\n rel=\\${dir%/}; rel=\\${rel#$PLUGINS_DIR/}\n grep -Fxq \"$rel\" \"$WORK/referenced\" && continue\n if [ -d \"$dir/node_modules\" ]; then\n bytes=\\$(du -sb \"$dir/node_modules\" 2>/dev/null | cut -f1)\n [ -n \"$bytes\" ] || bytes=0\n rm -rf \"$dir/node_modules\" \"$dir/$MARKER\" \"$dir/$FAILMARKER\"\n echo \"PRUNE_OK $rel $bytes\"\n else\n rm -f \"$dir/$MARKER\" \"$dir/$FAILMARKER\"\n fi\n done\nfi\nn=0\nfor dir in \"$PLUGINS_DIR\"/*/*/*/; do\n [ -d \"$dir\" ] || continue\n [ -f \"$dir/package.json\" ] || continue\n rel=\\${dir%/}; rel=\\${rel#$PLUGINS_DIR/}\n is_referenced \"$rel\" || continue\n [ -f \"$dir/$MARKER\" ] && continue\n [ -n \"\\$(find \"$dir\" -maxdepth 1 -name \"$FAILMARKER\" -mmin -\\$BACKOFF_MIN 2>/dev/null)\" ] && continue\n echo \"REBUILD_START \\${dir#$PLUGINS_DIR/}\"\n n=\\$((n+1))\n printf '%s\\\\n' \"$dir\" >> \"$WORK/dirs\"\ndone\nif [ \"$n\" -eq 0 ]; then rm -rf \"$WORK\"; exit 0; fi\nrunning=0\nwhile IFS= read -r dir; do\n do_one \"$dir\" &\n running=\\$((running+1))\n if [ \"$running\" -ge \"$MAX\" ]; then wait; running=0; fi\ndone < \"$WORK/dirs\"\nwait\nwhile IFS= read -r dir; do\n key=\\$(relkey \"$dir\")\n rel=\\${dir#$PLUGINS_DIR/}\n [ -f \"$WORK/$key.res\" ] || continue\n read -r st < \"$WORK/$key.res\"\n if [ \"$st\" = OK ]; then\n echo \"REBUILD_OK $rel\"\n else\n echo \"REBUILD_FAIL $rel\"\n sed 's/^/ /' \"$WORK/$key.err\"\n echo \"REBUILD_FAIL_END\"\n fi\ndone < \"$WORK/dirs\"\nrm -rf \"$WORK\"\n`;\n const result = await execa(\n 'docker',\n ['exec', '--user', CONTAINER_USER, container, 'sh', '-c', script],\n { reject: false },\n );\n const rebuilt: string[] = [];\n const failed: Array<{ dir: string; stderr: string }> = [];\n const pruned: string[] = [];\n let prunedBytes = 0;\n const lines = (result.stdout ?? '').split('\\n');\n let collectingFail: { dir: string; stderr: string[] } | null = null;\n for (const line of lines) {\n if (collectingFail) {\n if (line === 'REBUILD_FAIL_END') {\n failed.push({ dir: collectingFail.dir, stderr: collectingFail.stderr.join('\\n') });\n collectingFail = null;\n } else {\n collectingFail.stderr.push(line);\n }\n continue;\n }\n if (line.startsWith('REBUILD_START ')) {\n opts.onProgress?.(`rebuilding ${line.slice('REBUILD_START '.length)}`);\n } else if (line.startsWith('REBUILD_OK ')) {\n rebuilt.push(line.slice('REBUILD_OK '.length));\n } else if (line.startsWith('REBUILD_FAIL ')) {\n collectingFail = { dir: line.slice('REBUILD_FAIL '.length), stderr: [] };\n } else if (line.startsWith('PRUNE_OK ')) {\n // `PRUNE_OK <m>/<p>/<v> <bytes>` — bytes is the last space-delimited token.\n const rest = line.slice('PRUNE_OK '.length);\n const sp = rest.lastIndexOf(' ');\n if (sp > 0) {\n const dir = rest.slice(0, sp);\n const bytes = Number(rest.slice(sp + 1));\n pruned.push(dir);\n if (Number.isFinite(bytes)) prunedBytes += bytes;\n opts.onProgress?.(`pruning stale plugin cache ${dir}`);\n }\n }\n }\n return { rebuilt, failed, pruned, prunedBytes, skipped: false };\n}\n\nexport class ClaudeSessionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ClaudeSessionError';\n }\n}\n\nexport interface StartClaudeSessionOptions {\n container: string;\n claudeArgs: string[];\n sessionName?: string;\n /** Previously fed into the in-tmux status bar; now unused (the outer UI\n * shows the name). Kept for back-compat — callers may still pass it. */\n boxName?: string;\n}\n\n/**\n * Single-quote a token for /bin/sh. Conservative: anything outside the safe alphabet\n * gets wrapped. We don't try to detect \"obviously safe\" inputs; quoting is cheap.\n */\nfunction shQuote(arg: string): string {\n if (arg.length === 0) return `''`;\n if (/^[A-Za-z0-9_\\-./=:@%+,]+$/.test(arg)) return arg;\n return `'${arg.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * Start a detached tmux session running Claude Code inside the container. The session\n * survives client disconnects; reattach via {@link attachClaudeSession}.\n *\n * We forward the host's TERM (default xterm-256color) so the in-container tmux\n * picks the right terminal-overrides at session creation time — without this,\n * docker exec defaults TERM to `xterm` and tmux can't declare 24-bit color.\n *\n * We also re-forward {@link FORWARDED_ENV_KEYS} from the host's process env.\n * Values set at container-create time (via runBox -e) are still inherited\n * for free, but the user might be invoking `agentbox claude start <box>`\n * from a different shell session — e.g. inside their host claude (which sets\n * CLAUDE_EFFORT) for a box created earlier from a plain terminal. Re-passing\n * at exec time lets the in-box claude pick up the host's *current* selection.\n */\nexport async function startClaudeSession(opts: StartClaudeSessionOptions): Promise<void> {\n const sessionName = opts.sessionName ?? DEFAULT_CLAUDE_SESSION;\n const cmd = ['claude', ...opts.claudeArgs].map(shQuote).join(' ');\n const term = process.env['TERM'] ?? 'xterm-256color';\n const envFlags: string[] = ['-e', `TERM=${term}`];\n for (const k of FORWARDED_ENV_KEYS) {\n const v = process.env[k];\n if (typeof v === 'string' && v.length > 0) envFlags.push('-e', `${k}=${v}`);\n }\n const result = await execa(\n 'docker',\n [\n 'exec',\n ...envFlags,\n '--user',\n CONTAINER_USER,\n opts.container,\n 'tmux',\n 'new-session',\n '-d',\n '-s',\n sessionName,\n cmd,\n ...buildClaudeStatusBarArgs(sessionName),\n ],\n { reject: false },\n );\n if (result.exitCode === 0) return;\n const stderr = (result.stderr ?? '').toString();\n if (result.exitCode === 127 || /command not found|tmux: not found/i.test(stderr)) {\n throw new ClaudeSessionError(\n `tmux is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`,\n );\n }\n if (/claude.*not found|exec: \"claude\"/i.test(stderr)) {\n throw new ClaudeSessionError(\n `claude is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`,\n );\n }\n if (/duplicate session/i.test(stderr)) {\n throw new ClaudeSessionError(\n `a tmux session \"${sessionName}\" already exists in ${opts.container}; use \\`agentbox claude attach\\` to reattach.`,\n );\n }\n throw new ClaudeSessionError(\n `failed to start claude session in ${opts.container}: ${stderr.trim() || `exit ${String(result.exitCode)}`}`,\n );\n}\n\n/**\n * Replace the current process with `docker exec -it tmux attach`. Ctrl+a q returns\n * the user to their host shell with exit 0. We forward TERM so tmux declares\n * the outer terminal's true-color and hyperlink capabilities; without it\n * docker exec sets TERM=xterm and Claude renders without RGB.\n */\n/**\n * The `docker` argv that attaches an interactive terminal to a box's Claude\n * tmux session. Shared by {@link attachClaudeSession} (which `spawnSync`s it\n * directly) and the dashboard command (which hands it to `tmux respawn-pane`).\n */\nexport function buildClaudeAttachArgv(container: string, sessionName?: string): string[] {\n const name = sessionName ?? DEFAULT_CLAUDE_SESSION;\n const term = process.env['TERM'] ?? 'xterm-256color';\n return [\n 'exec',\n '-it',\n '-e',\n `TERM=${term}`,\n '--user',\n CONTAINER_USER,\n container,\n 'tmux',\n 'attach',\n '-t',\n name,\n ];\n}\n\n/**\n * Like {@link buildClaudeAttachArgv}, but for the dashboard's right pane. The\n * dashboard already draws its own bottom status bar, so a second client must\n * not show the inner tmux status bar. We attach via a *grouped* sibling session\n * (`<name>-dash`, `tmux new-session -t <name>`): grouped sessions share the\n * same windows/panes (identical live screen + scrollback) but keep independent\n * session options, so `status off` here does not affect a direct\n * `agentbox claude attach` to `<name>`. The bare `;` elements are tmux's\n * command separator — node-pty spawns docker without a shell, so they reach\n * tmux verbatim. `new-session -A -d` is a no-op if the grouped session already\n * exists; `attach` runs after `status off` so the footer is gone on first paint.\n */\nexport function buildClaudeDashboardAttachArgv(\n container: string,\n sessionName?: string,\n): string[] {\n const name = sessionName ?? DEFAULT_CLAUDE_SESSION;\n const dash = `${name}-dash`;\n const term = process.env['TERM'] ?? 'xterm-256color';\n return [\n 'exec',\n '-it',\n '-e',\n `TERM=${term}`,\n '--user',\n CONTAINER_USER,\n container,\n 'tmux',\n 'new-session',\n '-A',\n '-d',\n '-s',\n dash,\n '-t',\n name,\n ';',\n 'set',\n '-t',\n dash,\n 'status',\n 'off',\n ';',\n 'attach',\n '-t',\n dash,\n ];\n}\n\n/**\n * tmux command-list (separator-prefixed) that remaps the prefix and turns\n * the inner tmux status bar off. The outer host UI (the wrapped-pty footer\n * for `agentbox claude` / `agentbox shell`, the dashboard's own status row\n * for the right pane) already shows the box name + the detach hint, so the\n * inner bar is double-footer — strip it.\n *\n * `Ctrl+a` is the **primary** prefix (matches the dashboard's quit chord), and\n * tmux's default `Ctrl+b` is kept as a **secondary** prefix (`prefix2 C-b`) so\n * existing tmux muscle memory + integrations that send `Ctrl+b <key>` keep\n * working. Either prefix triggers the same key table — so `Ctrl+a q` *and*\n * `Ctrl+b q` both detach. `Ctrl+a Ctrl+a` sends a literal `Ctrl+a` through to\n * Claude (`send-prefix`); `Ctrl+b Ctrl+b` does the same for `Ctrl+b` via\n * `send-prefix -2`. `prefix`/`bind-key` are tmux server-global (no `-t`);\n * that's fine because each box's tmux server hosts only the claude session.\n * Applied here (not in the image) so existing boxes pick it up on the next\n * fresh session with no rebuild.\n *\n * Appended after `tmux new-session …` in {@link startClaudeSession}; the bare\n * `;` elements are tmux's command separator (execa array args, no host shell,\n * so they reach tmux verbatim). `status off` is a session option scoped with\n * `-t <session>` — the dashboard's grouped `<name>-dash` session has its own\n * option scope and runs its own `status off` in {@link buildClaudeDashboardAttachArgv}.\n */\nexport function buildClaudeStatusBarArgs(sessionName: string): string[] {\n const s = sessionName;\n return [\n // Server-global (no -t): primary prefix Ctrl+a (dashboard parity), keep\n // tmux's default Ctrl+b as a secondary prefix so users with existing\n // muscle memory / integrations aren't broken. `q` is the same key under\n // both prefixes (single key table) -> Ctrl+a q AND Ctrl+b q both detach.\n // `send-prefix` / `send-prefix -2` let a double-tap of either prefix\n // reach Claude as that literal key.\n ';', 'set', '-g', 'prefix', 'C-a',\n ';', 'set', '-g', 'prefix2', 'C-b',\n ';', 'bind-key', 'C-a', 'send-prefix',\n ';', 'bind-key', 'C-b', 'send-prefix', '-2',\n ';', 'bind-key', 'q', 'detach-client',\n // Hide the inner tmux status bar — the wrapped-pty footer (for\n // `agentbox claude` / `agentbox shell`) and the dashboard's own status\n // row already show the box name + detach hint; without `status off`\n // they double up.\n ';', 'set', '-t', s, 'status', 'off',\n ];\n}\n\n/**\n * The `docker` argv for an interactive login shell in a box — the same shape\n * `agentbox shell` uses (vscode user, image WORKDIR `/workspace`, `bash -l`).\n * Handed to node-pty by the dashboard's \"open a shell\" action.\n */\nexport function buildShellArgv(container: string): string[] {\n const term = process.env['TERM'] ?? 'xterm-256color';\n return ['exec', '-it', '-e', `TERM=${term}`, '--user', CONTAINER_USER, container, 'bash', '-l'];\n}\n\n/**\n * The `docker run` argv for an interactive `claude auth login` in a throwaway\n * container. Mounts the claude-config volume at `~/.claude` so the written\n * credentials persist; runs before any box exists. `extraArgs` are appended\n * verbatim (e.g. `['--claudeai']`, `['--sso']`).\n *\n * `DISPLAY` is blanked: the box image bakes `DISPLAY=:1` (a VNC X server) and\n * `claude auth login` would otherwise try to open a browser on that invisible\n * display. An empty `DISPLAY` forces claude's terminal URL/paste-code flow.\n */\nexport function buildClaudeLoginRunArgv(opts: {\n volume: string;\n image: string;\n extraArgs: string[];\n}): string[] {\n const term = process.env['TERM'] ?? 'xterm-256color';\n return [\n 'run',\n '-it',\n '--rm',\n '-e',\n `TERM=${term}`,\n '-e',\n 'DISPLAY=',\n '-v',\n `${opts.volume}:${CONTAINER_CLAUDE_DIR}`,\n '--user',\n CONTAINER_USER,\n opts.image,\n 'claude',\n 'auth',\n 'login',\n ...opts.extraArgs,\n ];\n}\n\n/**\n * Run an interactive docker argv (from {@link buildClaudeLoginRunArgv}) with\n * the user's terminal attached. Returns the exit code; a null status (killed /\n * failed to spawn) is reported as 1.\n */\nexport function runInteractiveClaudeLogin(dockerArgv: string[]): { exitCode: number } {\n const child = spawnSync('docker', dockerArgv, { stdio: 'inherit' });\n return { exitCode: child.status ?? 1 };\n}\n\nexport interface WarmUpClaudeResult {\n /** True once a headless `claude -p` request actually succeeded. */\n warmed: boolean;\n /** How many attempts were made (1 = warm on the first try). */\n attempts: number;\n}\n\n/**\n * After a fresh `claude auth login`, the *first* Claude Code inference request\n * on the newly minted Claude.ai subscription token is rejected by the API with\n * `400 role 'system' is not supported on this model` — the account/token needs\n * one inference round-trip to be provisioned. A later process then works\n * (confirmed empirically: the first in-box session 400s, every later\n * box/session on the same credentials succeeds).\n *\n * Absorb that sacrificial request here: run a headless `claude -p` in a\n * throwaway container against the shared volume the login just wrote to,\n * retrying until one request actually succeeds — so the user's real box\n * session is never the first request. `--dangerously-skip-permissions` keeps\n * the headless run from stalling on a trust/permission prompt (this is a\n * throwaway sandbox container; nothing it does is persisted beyond the volume).\n *\n * Best-effort and time-boxed: if it never succeeds we return `warmed: false`\n * and the caller proceeds anyway — the box then behaves exactly as it did\n * before this warm-up existed.\n */\nexport async function warmUpClaudeCredentials(\n volume: string,\n image: string,\n opts: { onProgress?: (line: string) => void } = {},\n): Promise<WarmUpClaudeResult> {\n const MAX_ATTEMPTS = 6;\n const SLEEP_MS = 5000;\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n opts.onProgress?.(`checking credentials... ${attempt}/${MAX_ATTEMPTS}`);\n const res = await execa(\n 'docker',\n [\n 'run',\n '--rm',\n '-v',\n `${volume}:${CONTAINER_CLAUDE_DIR}`,\n '--user',\n CONTAINER_USER,\n '-e',\n 'DISABLE_AUTOUPDATER=1',\n image,\n 'claude',\n '--dangerously-skip-permissions',\n '-p',\n 'ok',\n ],\n { reject: false, timeout: 60_000 },\n );\n // `claude -p` can exit 0 while printing an API error as the turn's text,\n // so success needs a clean exit AND no error signature in the output.\n const out = `${res.stdout ?? ''}\\n${res.stderr ?? ''}`;\n const apiError = /API Error|is not supported on this model|\"type\":\\s*\"error\"/i.test(out);\n if (res.exitCode === 0 && !apiError) return { warmed: true, attempts: attempt };\n if (attempt < MAX_ATTEMPTS) await delay(SLEEP_MS);\n }\n return { warmed: false, attempts: MAX_ATTEMPTS };\n}\n\nexport function formatDetachNotice(ref: string): string {\n return `Session detached. Reattach with: agentbox claude attach ${ref}`;\n}\n\nexport function attachClaudeSession(\n container: string,\n sessionName?: string,\n reattachRef?: string,\n): never {\n const child = spawnSync('docker', buildClaudeAttachArgv(container, sessionName), {\n stdio: 'inherit',\n });\n const code = child.status ?? 0;\n if (reattachRef && code === 0) {\n // Overwrite tmux's own `[detached (from session …)]` line (printed just\n // above the cursor on a clean detach). Best-effort cosmetics: if the\n // terminal ignores the cursor moves, our line still prints below it.\n process.stdout.write('\\x1b[1A\\x1b[2K\\r' + formatDetachNotice(reattachRef) + '\\n');\n }\n process.exit(code);\n}\n\nexport interface ClaudeSessionInfo {\n running: boolean;\n sessionName: string;\n /** ISO-8601 timestamp from tmux's `#{session_created}` format string, or null when not running. */\n startedAt: string | null;\n}\n\n/**\n * Best-effort: returns `{ running: false, …, startedAt: null }` for any non-zero exit\n * from `tmux has-session` (which includes \"no server running\" and \"no such session\").\n */\nexport async function claudeSessionInfo(\n container: string,\n sessionName?: string,\n): Promise<ClaudeSessionInfo> {\n const name = sessionName ?? DEFAULT_CLAUDE_SESSION;\n const has = await execa(\n 'docker',\n ['exec', '--user', CONTAINER_USER, container, 'tmux', 'has-session', '-t', name],\n { reject: false },\n );\n if (has.exitCode !== 0) {\n return { running: false, sessionName: name, startedAt: null };\n }\n const ts = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n CONTAINER_USER,\n container,\n 'tmux',\n 'display-message',\n '-p',\n '-t',\n name,\n '#{session_created}',\n ],\n { reject: false },\n );\n let startedAt: string | null = null;\n if (ts.exitCode === 0) {\n const secs = Number.parseInt((ts.stdout ?? '').trim(), 10);\n if (Number.isFinite(secs) && secs > 0) startedAt = new Date(secs * 1000).toISOString();\n }\n return { running: true, sessionName: name, startedAt };\n}\n\nexport interface PullClaudeResult {\n /**\n * Box-installed extensions not present on the host. `category` is one of\n * skills/agents/commands (then `name` is the dir name) or `plugins` (then\n * `name` is the `<marketplace>/<plugin>` cache key).\n */\n newItems: Array<{ category: string; name: string }>;\n /** Registry JSONs that gained box-only entries (e.g. `known_marketplaces.json`). */\n mergedRegistries: string[];\n}\n\nexport interface PullClaudeOptions {\n /** Image for the throwaway helper container; use the box's image to avoid extra pulls. */\n image: string;\n /** When true, compute the delta but write nothing. */\n dryRun?: boolean;\n}\n\nconst PULL_DIR_CATEGORIES = ['skills', 'agents', 'commands'] as const;\n\n/**\n * Immediate child item names of `dir`, or [] if it doesn't exist. Symlinks\n * count: the host's `~/.claude/skills/<name>` is a symlink into `~/.agents`\n * (Claude Code's user-skills convention), so `isDirectory()` alone would miss\n * them and every host skill would look \"new\".\n */\nasync function listChildDirs(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);\n } catch {\n return [];\n }\n}\n\nasync function readJsonFile(path: string): Promise<unknown> {\n try {\n return JSON.parse(await readFile(path, 'utf8'));\n } catch {\n return undefined;\n }\n}\n\n/**\n * Reverse of {@link ensureClaudeVolume}: pull box-installed Claude extensions\n * (skills/agents/commands dirs + plugins) from the claude-config volume back to\n * the host's `~/.claude`. Additive only — an item already present on the host\n * is never overwritten. The box need not be running; we read the *volume* via a\n * throwaway helper container (the exact mirror of the forward sync), so this\n * also works while the box is stopped.\n *\n * Plugin registry JSONs (`installed_plugins.json`, `known_marketplaces.json`)\n * are merged host-side: only box-only keys are added, with the forward sync's\n * `/home/vscode/.claude/plugins/` rewrite reversed back to the host path.\n */\nexport async function pullClaudeExtras(\n spec: ClaudeConfigSpec,\n opts: PullClaudeOptions,\n): Promise<PullClaudeResult> {\n const hostHome = homedir();\n const hostClaude = join(hostHome, '.claude');\n\n // Inventory pass: enumerate the volume's contents via a read-only helper\n // container. `--user 0` so root can read files claude wrote as uid 1000.\n // base64 -w0 keeps each registry JSON on one parseable line.\n const inventoryScript = [\n 'for cat in skills agents commands; do',\n ' [ -d \"/src/$cat\" ] || continue;',\n ' for d in \"/src/$cat\"/*/; do',\n ' [ -d \"$d\" ] || continue;',\n ' printf \"DIR %s %s\\\\n\" \"$cat\" \"$(basename \"$d\")\";',\n ' done;',\n 'done;',\n 'if [ -d /src/plugins/cache ]; then',\n ' for m in /src/plugins/cache/*/; do',\n ' [ -d \"$m\" ] || continue;',\n ' for p in \"$m\"*/; do',\n ' [ -d \"$p\" ] || continue;',\n ' printf \"PLUGIN %s/%s\\\\n\" \"$(basename \"$m\")\" \"$(basename \"$p\")\";',\n ' done;',\n ' done;',\n 'fi;',\n 'for f in installed_plugins known_marketplaces; do',\n ' [ -f \"/src/plugins/$f.json\" ] || continue;',\n ' printf \"JSON %s \" \"$f\";',\n ' base64 -w0 \"/src/plugins/$f.json\";',\n ' printf \"\\\\n\";',\n 'done',\n ].join(' ');\n\n const inv = await execa(\n 'docker',\n ['run', '--rm', '--user', '0', '-v', `${spec.volume}:/src:ro`, opts.image, 'sh', '-c', inventoryScript],\n { reject: false },\n );\n if (inv.exitCode !== 0) {\n throw new ClaudeSessionError(\n `failed to read claude-config volume ${spec.volume}: ${(inv.stderr ?? '').toString().trim() || `exit ${String(inv.exitCode)}`}`,\n );\n }\n\n const boxDirs: Record<string, string[]> = { skills: [], agents: [], commands: [] };\n const boxPlugins: string[] = [];\n const boxJson: Record<string, unknown> = {};\n for (const line of (inv.stdout ?? '').split('\\n')) {\n if (line.startsWith('DIR ')) {\n const rest = line.slice(4);\n const sp = rest.indexOf(' ');\n if (sp === -1) continue;\n const cat = rest.slice(0, sp);\n const name = rest.slice(sp + 1);\n if (cat in boxDirs) boxDirs[cat]!.push(name);\n } else if (line.startsWith('PLUGIN ')) {\n boxPlugins.push(line.slice(7));\n } else if (line.startsWith('JSON ')) {\n const rest = line.slice(5);\n const sp = rest.indexOf(' ');\n if (sp === -1) continue;\n const which = rest.slice(0, sp);\n try {\n boxJson[which] = JSON.parse(Buffer.from(rest.slice(sp + 1), 'base64').toString('utf8'));\n } catch {\n // Leave undefined; the merge helpers tolerate it.\n }\n }\n }\n\n // Compute deltas host-side (the host ~/.claude is directly accessible —\n // only the volume needed a container).\n const newItems: PullClaudeResult['newItems'] = [];\n const applyPaths: Array<{ src: string; dest: string }> = [];\n for (const cat of PULL_DIR_CATEGORIES) {\n const hostNames = await listChildDirs(join(hostClaude, cat));\n const excludes = cat === 'skills' ? SKILL_EXCLUDE_PREFIXES : [];\n for (const name of pickNewItems(boxDirs[cat] ?? [], hostNames, excludes)) {\n newItems.push({ category: cat, name });\n applyPaths.push({ src: `/src/${cat}/${name}`, dest: `/dst/${cat}/${name}` });\n }\n }\n const hostPluginKeys: string[] = [];\n for (const m of await listChildDirs(join(hostClaude, 'plugins', 'cache'))) {\n for (const p of await listChildDirs(join(hostClaude, 'plugins', 'cache', m))) {\n hostPluginKeys.push(`${m}/${p}`);\n }\n }\n for (const key of pickNewItems(boxPlugins, hostPluginKeys)) {\n newItems.push({ category: 'plugins', name: key });\n applyPaths.push({ src: `/src/plugins/cache/${key}`, dest: `/dst/plugins/cache/${key}` });\n }\n\n // Additive merge of the two plugin registries (reverses the forward path\n // rewrite). Computed regardless so the preview can report it.\n const hostInstalled = await readJsonFile(join(hostClaude, 'plugins', 'installed_plugins.json'));\n const hostMarkets = await readJsonFile(join(hostClaude, 'plugins', 'known_marketplaces.json'));\n const mergedInstalled = mergeInstalledPlugins(hostInstalled, boxJson['installed_plugins'], {\n hostHome,\n });\n const mergedMarkets = mergeKnownMarketplaces(hostMarkets, boxJson['known_marketplaces'], {\n hostHome,\n });\n const mergedRegistries: string[] = [];\n if (mergedInstalled.changed) mergedRegistries.push('installed_plugins.json');\n if (mergedMarkets.changed) mergedRegistries.push('known_marketplaces.json');\n\n if (opts.dryRun || (newItems.length === 0 && mergedRegistries.length === 0)) {\n return { newItems, mergedRegistries };\n }\n\n // Apply pass: rsync each new item dir from the volume into the host\n // ~/.claude bind mount. --ignore-existing is belt-and-suspenders (the\n // host-side delta is the real guard); --exclude=node_modules because the\n // box carries linux/amd64 binaries useless on the darwin host (claude/host\n // rebuilds lazily, same rationale as the forward sync's exclude).\n if (applyPaths.length > 0) {\n const cmds = applyPaths.map(({ src, dest }) => {\n const parent = dest.slice(0, dest.lastIndexOf('/'));\n return `mkdir -p '${parent}' && rsync -a --ignore-existing --exclude=node_modules '${src}/' '${dest}/'`;\n });\n const apply = await execa(\n 'docker',\n [\n 'run',\n '--rm',\n '--user',\n '0',\n '-v',\n `${spec.volume}:/src:ro`,\n '-v',\n `${hostClaude}:/dst`,\n opts.image,\n 'sh',\n '-c',\n cmds.join(' && '),\n ],\n { reject: false },\n );\n if (apply.exitCode !== 0) {\n throw new ClaudeSessionError(\n `failed to copy extensions from ${spec.volume}: ${(apply.stderr ?? '').toString().trim() || `exit ${String(apply.exitCode)}`}`,\n );\n }\n }\n\n // Registry JSONs are written host-side (host path is directly writable;\n // no container needed) — only when the merge actually added keys.\n if (mergedMarkets.changed || mergedInstalled.changed) {\n await mkdir(join(hostClaude, 'plugins'), { recursive: true });\n if (mergedMarkets.changed) {\n await writeFile(\n join(hostClaude, 'plugins', 'known_marketplaces.json'),\n `${JSON.stringify(mergedMarkets.data, null, 2)}\\n`,\n );\n }\n if (mergedInstalled.changed) {\n await writeFile(\n join(hostClaude, 'plugins', 'installed_plugins.json'),\n `${JSON.stringify(mergedInstalled.data, null, 2)}\\n`,\n );\n }\n }\n\n return { newItems, mergedRegistries };\n}\n","export interface HookFilterResult<T = unknown> {\n data: T;\n removedCommands: string[];\n}\n\n/**\n * Predicate for the filter: does `command` look like it points at a host path\n * outside the project, i.e. under the user's host home directory?\n *\n * Uses `.includes(hostHome + '/')` so we also catch shell-quoted forms like\n * `bash -c '/Users/marco/.config/iterm2/cc-status'`. The trailing slash gates\n * against false matches on similar-prefix home dirs (e.g. `/Users/marco`\n * shouldn't match `/Users/marco-other/...`).\n *\n * Returns false when `hostHome` is empty so we degrade safely if no home is\n * resolvable for some reason.\n */\nexport function isHostPathHookCommand(command: string, hostHome: string): boolean {\n if (typeof command !== 'string' || command.length === 0) return false;\n if (hostHome.length === 0) return false;\n return command.includes(hostHome + '/');\n}\n\ninterface HookLeaf {\n type?: string;\n command?: string;\n [k: string]: unknown;\n}\n\ninterface HookMatcherEntry {\n hooks?: unknown;\n [k: string]: unknown;\n}\n\n/**\n * Walk Claude Code's documented `hooks.<Trigger>[].hooks[]` structure and drop\n * any leaf `{ type: 'command', command: '<host-path>' }` whose `command` matches\n * {@link isHostPathHookCommand}. Empty `hooks: []` arrays and their matcher\n * wrappers are left intact — they don't break Claude and avoiding recursive\n * cleanup keeps the filter predictable.\n *\n * Returns a deep clone; input is not mutated. Tolerant of unexpected shapes\n * (string, number, null at any level): unrecognized branches pass through\n * unchanged, removed count stays accurate.\n */\nexport function filterHostHooks<T = unknown>(data: T, hostHome: string): HookFilterResult<T> {\n // structuredClone is in node >= 17; the repo targets node20, so this is safe.\n const clone = structuredClone(data) as unknown;\n const removedCommands: string[] = [];\n\n if (clone === null || typeof clone !== 'object' || Array.isArray(clone)) {\n return { data: clone as T, removedCommands };\n }\n\n const top = clone as { hooks?: unknown };\n const hooksRoot = top.hooks;\n if (hooksRoot === null || typeof hooksRoot !== 'object' || Array.isArray(hooksRoot)) {\n return { data: clone as T, removedCommands };\n }\n\n for (const triggerName of Object.keys(hooksRoot as Record<string, unknown>)) {\n const triggerValue = (hooksRoot as Record<string, unknown>)[triggerName];\n if (!Array.isArray(triggerValue)) continue;\n for (const entry of triggerValue) {\n if (entry === null || typeof entry !== 'object') continue;\n const matcher = entry as HookMatcherEntry;\n const inner = matcher.hooks;\n if (!Array.isArray(inner)) continue;\n // In-place filter on the cloned array.\n for (let i = inner.length - 1; i >= 0; i--) {\n const leaf = inner[i] as HookLeaf | null;\n if (leaf === null || typeof leaf !== 'object') continue;\n if (\n leaf.type === 'command' &&\n typeof leaf.command === 'string' &&\n isHostPathHookCommand(leaf.command, hostHome)\n ) {\n removedCommands.push(leaf.command);\n inner.splice(i, 1);\n }\n }\n }\n }\n\n return { data: clone as T, removedCommands };\n}\n\nexport interface SetInstallMethodNativeResult<T = unknown> {\n data: T;\n applied: boolean;\n}\n\nexport interface AddProjectAliasResult<T = unknown> {\n data: T;\n aliased: boolean;\n}\n\nexport interface TrustWorkspaceResult<T = unknown> {\n data: T;\n trusted: boolean;\n}\n\n/**\n * Force `projects[workspacePath].hasTrustDialogAccepted = true` in a parsed\n * `~/.claude.json`, creating the `projects` map and the project entry if\n * absent.\n *\n * The box is a sandbox: the agent is created explicitly to work in\n * `/workspace`, and the box is isolated from the host — so the folder-trust\n * dialog is pointless there. More importantly, Claude Code, when it opens an\n * *untrusted* folder, sends a malformed first API request that Anthropic\n * rejects with `400 role 'system' is not supported on this model`. Pre-trusting\n * the box's workspace skips the dialog *and* dodges that bug.\n *\n * Returns a deep-cloned, modified copy plus a flag for whether anything\n * changed (`false` when it was already trusted). Input is not mutated; no-op\n * for non-object data or an empty path.\n */\nexport function trustWorkspace<T = unknown>(\n data: T,\n workspacePath: string,\n): TrustWorkspaceResult<T> {\n const clone = structuredClone(data) as unknown;\n if (clone === null || typeof clone !== 'object' || Array.isArray(clone)) {\n return { data: clone as T, trusted: false };\n }\n if (workspacePath.length === 0) return { data: clone as T, trusted: false };\n const obj = clone as { projects?: unknown };\n if (obj.projects === null || typeof obj.projects !== 'object' || Array.isArray(obj.projects)) {\n obj.projects = {};\n }\n const projects = obj.projects as Record<string, unknown>;\n const existing = projects[workspacePath];\n const entry =\n existing !== null && typeof existing === 'object' && !Array.isArray(existing)\n ? (existing as Record<string, unknown>)\n : {};\n if (entry.hasTrustDialogAccepted === true) {\n projects[workspacePath] = entry;\n return { data: clone as T, trusted: false };\n }\n entry.hasTrustDialogAccepted = true;\n projects[workspacePath] = entry;\n return { data: clone as T, trusted: true };\n}\n\n/**\n * Claude Code keys project-scoped state (history, mcpServers, enabledPlugins,\n * trust prompts) under `projects[<absolute-workspace-path>]` in\n * `~/.claude.json`. On the host the key is something like\n * `/Users/marco/Projects/foo`; inside the box the workspace is always\n * `/workspace`. Without rewriting, the box never sees the host's project-\n * scoped settings.\n *\n * Copy (don't move) the host-keyed entry to `toPath` if present. Existing\n * `projects[toPath]` is preserved by merging the host entry on top — host\n * is authoritative for keys it sets; box-only keys (e.g. session ids\n * accumulated inside earlier boxes) stay intact.\n *\n * No-op (returns `aliased: false`) when:\n * - data isn't an object, or `projects` isn't an object\n * - fromPath equals toPath\n * - projects[fromPath] doesn't exist or isn't an object\n *\n * Returns a deep-cloned, modified copy; input is not mutated.\n */\nexport function addProjectAlias<T = unknown>(\n data: T,\n fromPath: string,\n toPath: string,\n): AddProjectAliasResult<T> {\n const clone = structuredClone(data) as unknown;\n if (clone === null || typeof clone !== 'object' || Array.isArray(clone)) {\n return { data: clone as T, aliased: false };\n }\n if (fromPath === toPath || fromPath.length === 0 || toPath.length === 0) {\n return { data: clone as T, aliased: false };\n }\n const obj = clone as { projects?: unknown };\n const projects = obj.projects;\n if (projects === null || typeof projects !== 'object' || Array.isArray(projects)) {\n return { data: clone as T, aliased: false };\n }\n const projectsMap = projects as Record<string, unknown>;\n const src = projectsMap[fromPath];\n if (src === null || typeof src !== 'object' || Array.isArray(src)) {\n return { data: clone as T, aliased: false };\n }\n const existing = projectsMap[toPath];\n if (existing !== null && typeof existing === 'object' && !Array.isArray(existing)) {\n projectsMap[toPath] = { ...(existing as Record<string, unknown>), ...(src as Record<string, unknown>) };\n } else {\n projectsMap[toPath] = structuredClone(src);\n }\n return { data: clone as T, aliased: true };\n}\n\n/**\n * Force the install-method fields in a parsed `~/.claude.json` to match the\n * box's native install. Sets exactly what `claude install` writes:\n * installMethod: \"native\"\n * autoUpdates: false\n * autoUpdatesProtectedForNative: true\n *\n * Without this, the in-box claude reports\n * `Running native installation but config install method is 'not set'` in\n * /status — the host's value (often `npm-global` on Mac, or absent) doesn't\n * match the box's `~/.local/bin/claude` install location, and merely\n * clearing the field leaves it unset rather than fixing it.\n *\n * Returns a deep-cloned, fixed copy plus a flag indicating whether any of\n * the three fields actually changed. Input is not mutated.\n */\nexport function setInstallMethodNative<T = unknown>(data: T): SetInstallMethodNativeResult<T> {\n const clone = structuredClone(data) as unknown;\n if (clone === null || typeof clone !== 'object' || Array.isArray(clone)) {\n return { data: clone as T, applied: false };\n }\n const obj = clone as Record<string, unknown>;\n const changed =\n obj.installMethod !== 'native' ||\n obj.autoUpdates !== false ||\n obj.autoUpdatesProtectedForNative !== true;\n obj.installMethod = 'native';\n obj.autoUpdates = false;\n obj.autoUpdatesProtectedForNative = true;\n return { data: clone as T, applied: changed };\n}\n","/**\n * Pure, docker-free helpers for `agentbox download claude` (box -> host pull of\n * Claude extensions). Kept separate from `claude.ts` so the delta + JSON-merge\n * logic is unit-testable without spawning containers — mirrors how\n * `claude-hooks-filter.ts` factors the forward-sync transforms.\n *\n * The forward sync (`ensureClaudeVolume` in claude.ts) is host-authoritative\n * and rewrites `$HOST_HOME/.claude/plugins/` -> `/home/vscode/.claude/plugins/`\n * in the plugin registry JSONs. This module is the reverse: additive (host\n * wins, only missing items are added) and rewrites the container path back to\n * the host path.\n */\n\n/** Categories under ~/.claude we pull box-side additions for. */\nexport const PULL_CATEGORIES = ['skills', 'plugins', 'agents', 'commands'] as const;\nexport type PullCategory = (typeof PULL_CATEGORIES)[number];\n\n/**\n * Skills whose directory name starts with one of these prefixes are agentbox's\n * own (currently just `agentbox-setup`, seeded box-only into the claude-config\n * volume by `seedSetupSkillIntoVolume` in claude.ts — never on the host).\n * Pulling them back would re-introduce them onto the host, which is exactly\n * what the box-only design avoids, so we never treat them as user-authored\n * additions.\n */\nexport const SKILL_EXCLUDE_PREFIXES = ['agentbox-'] as const;\n\n/** Container path prefix the forward sync rewrites host plugin paths to. */\nexport const CONTAINER_PLUGINS_PREFIX = '/home/vscode/.claude/plugins/';\n\n/**\n * Set-difference of `boxNames` against `hostNames`, dropping any name that\n * starts with one of `excludePrefixes`. Result is sorted for stable output.\n * Used for skills/agents/commands (top-level dir names) and plugin cache\n * (`<marketplace>/<plugin>` keys).\n */\nexport function pickNewItems(\n boxNames: string[],\n hostNames: string[],\n excludePrefixes: readonly string[] = [],\n): string[] {\n const host = new Set(hostNames);\n const seen = new Set<string>();\n const out: string[] = [];\n for (const name of boxNames) {\n if (name.length === 0 || host.has(name) || seen.has(name)) continue;\n if (excludePrefixes.some((p) => name.startsWith(p))) continue;\n seen.add(name);\n out.push(name);\n }\n return out.sort();\n}\n\n/**\n * Rewrite the forward sync's container plugin prefix back to the host's, in\n * every string anywhere in `value`. Generic over the JSON shape so it covers\n * both `installLocation` (known_marketplaces.json) and `installPath`\n * (installed_plugins.json) plus any future path-bearing field.\n */\nfunction rewritePluginPaths<T>(value: T, hostPluginsPrefix: string): T {\n if (typeof value === 'string') {\n return value.split(CONTAINER_PLUGINS_PREFIX).join(hostPluginsPrefix) as unknown as T;\n }\n if (Array.isArray(value)) {\n return value.map((v) => rewritePluginPaths(v, hostPluginsPrefix)) as unknown as T;\n }\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n out[k] = rewritePluginPaths(v, hostPluginsPrefix);\n }\n return out as T;\n }\n return value;\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\n\nexport interface MergeResult {\n /** Merged JSON to write back to the host (the host object plus added keys). */\n data: unknown;\n /** True when at least one key was added (caller should write on true only). */\n changed: boolean;\n /** Keys added to the host map, for the preview/summary. */\n addedKeys: string[];\n}\n\n/**\n * Additive object-key merge. For every key in `boxMap` absent from `hostMap`,\n * copy the box's value in (with plugin paths rewritten to the host prefix).\n * Existing host keys are never touched. Tolerant of missing / non-object\n * inputs: returns the host value unchanged with `changed: false` (matches\n * `maybeFilterTo`'s defensiveness in claude.ts).\n *\n * `selectMap` projects the registry object to the map being merged\n * (identity for known_marketplaces.json, `.plugins` for installed_plugins.json)\n * and `withMap` writes the merged map back into a clone of the host registry.\n */\nfunction additiveMerge(\n hostRoot: unknown,\n boxRoot: unknown,\n hostPluginsPrefix: string,\n selectMap: (root: unknown) => unknown,\n withMap: (hostRoot: unknown, mergedMap: Record<string, unknown>) => unknown,\n): MergeResult {\n const hostMap = selectMap(hostRoot);\n const boxMap = selectMap(boxRoot);\n if (!isPlainObject(boxMap)) {\n return { data: hostRoot, changed: false, addedKeys: [] };\n }\n const base: Record<string, unknown> = isPlainObject(hostMap) ? { ...hostMap } : {};\n const addedKeys: string[] = [];\n for (const [key, value] of Object.entries(boxMap)) {\n if (Object.prototype.hasOwnProperty.call(base, key)) continue;\n base[key] = rewritePluginPaths(value, hostPluginsPrefix);\n addedKeys.push(key);\n }\n if (addedKeys.length === 0) {\n return { data: hostRoot, changed: false, addedKeys: [] };\n }\n return { data: withMap(hostRoot, base), changed: true, addedKeys: addedKeys.sort() };\n}\n\n/**\n * known_marketplaces.json is a flat object keyed by marketplace name\n * (`{ \"<name>\": { source, installLocation, lastUpdated } }`). Add box-only\n * marketplaces; rewrite their `installLocation` back to the host path.\n */\nexport function mergeKnownMarketplaces(\n hostJson: unknown,\n boxJson: unknown,\n opts: { hostHome: string },\n): MergeResult {\n const prefix = `${opts.hostHome}/.claude/plugins/`;\n return additiveMerge(\n isPlainObject(hostJson) ? hostJson : {},\n boxJson,\n prefix,\n (root) => root,\n (_host, merged) => merged,\n );\n}\n\n/**\n * installed_plugins.json is `{ version, plugins: { \"<name>@<mkt>\": [...] } }`.\n * Add box-only entries under `.plugins`; rewrite each entry's `installPath`\n * back to the host path. The top-level `version` and any other host keys are\n * preserved as-is.\n */\nexport function mergeInstalledPlugins(\n hostJson: unknown,\n boxJson: unknown,\n opts: { hostHome: string },\n): MergeResult {\n const prefix = `${opts.hostHome}/.claude/plugins/`;\n const hostRoot = isPlainObject(hostJson) ? hostJson : { plugins: {} };\n return additiveMerge(\n hostRoot,\n boxJson,\n prefix,\n (root) => (isPlainObject(root) ? (root as Record<string, unknown>)['plugins'] : undefined),\n (host, merged) => ({ ...(host as Record<string, unknown>), plugins: merged }),\n );\n}\n\n/**\n * Collect the set of `<marketplace>/<plugin>/<version>` cache keys that\n * `installed_plugins.json` actively references — every entry's `installPath`\n * reduced to its last three path segments. The plugin cache is a fixed\n * three-level tree (`cache/<m>/<p>/<v>/`), so the last three segments uniquely\n * identify the version dir regardless of whether the path is host-rooted\n * (`/Users/...`) or container-rooted (`/home/vscode/...`).\n *\n * Used to tell stale plugin-version dirs (an old version Claude left behind\n * after an update) apart from live ones, so a rebuild pass can prune the stale\n * ones' `node_modules` and never reinstall them. Returns an empty set for\n * missing / non-object input or entries without a usable `installPath` — the\n * caller treats \"empty\" as \"can't determine, do nothing\".\n */\nexport function referencedPluginVersionKeys(installedPluginsJson: unknown): Set<string> {\n const keys = new Set<string>();\n if (!isPlainObject(installedPluginsJson)) return keys;\n const plugins = installedPluginsJson['plugins'];\n if (!isPlainObject(plugins)) return keys;\n for (const entries of Object.values(plugins)) {\n if (!Array.isArray(entries)) continue;\n for (const entry of entries) {\n if (!isPlainObject(entry)) continue;\n const installPath = entry['installPath'];\n if (typeof installPath !== 'string') continue;\n const segments = installPath.split('/').filter((s) => s.length > 0);\n if (segments.length < 3) continue;\n keys.add(segments.slice(-3).join('/'));\n }\n }\n return keys;\n}\n","import { execInBox } from './docker.js';\n\nexport interface DockerdLaunchResult {\n up: boolean;\n reason?: string;\n}\n\n/**\n * Shared image-cache volume across boxes. When a box is created with\n * `dockerCacheShared=true`, its in-box dockerd's data root is this volume\n * instead of the per-box `agentbox-docker-<id>` volume. Mutually exclusive at\n * runtime: only one box can hold the lock on `/var/lib/docker` at a time —\n * dockerd's own boltdb lock will refuse a second start. This is a power-user\n * feature for users who run boxes serially and want pulled layers to persist\n * across recreations; documented as such on the CLI flag.\n */\nexport const SHARED_DOCKER_CACHE_VOLUME = 'agentbox-docker-cache';\n\nexport function dockerVolumeName(boxId: string, shared: boolean): string {\n return shared ? SHARED_DOCKER_CACHE_VOLUME : `agentbox-docker-${boxId}`;\n}\n\n/**\n * Spawn the in-container dockerd via `/usr/local/bin/agentbox-dockerd-start`\n * detached, then poll for `/var/run/docker.sock` to become accept()-able.\n * Best-effort, mirroring {@link launchVncDaemon} — failure is logged but\n * doesn't fail box creation. Default timeout 30s: first start has to\n * initialize iptables + the storage graphdriver, which is slower than the\n * VNC stack.\n */\nexport async function launchDockerdDaemon(\n container: string,\n timeoutMs = 30_000,\n): Promise<DockerdLaunchResult> {\n const result = await execInBox(container, ['/usr/local/bin/agentbox-dockerd-start'], {\n user: 'root',\n detach: true,\n });\n if (result.exitCode !== 0) {\n return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const probe = await execInBox(\n container,\n [\n 'bash',\n '-lc',\n '[ -S /var/run/docker.sock ] && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1',\n ],\n { user: 'root' },\n );\n if (probe.exitCode === 0) return { up: true };\n await new Promise((r) => setTimeout(r, 200));\n }\n return { up: false, reason: `dockerd did not become ready within ${String(timeoutMs)}ms` };\n}\n","import { randomBytes } from 'node:crypto';\nimport { execInBox } from './docker.js';\n\nexport interface VncLaunchResult {\n up: boolean;\n reason?: string;\n}\n\n/**\n * Spawn the in-container VNC supervisor (`/usr/local/bin/agentbox-vnc-start`)\n * detached, then poll the container's TCP 6080 to confirm websockify is up.\n * Best-effort, mirroring {@link launchCtlDaemon} — failure is logged but\n * doesn't fail box creation. The password reaches the script through the\n * container's AGENTBOX_VNC_PASSWORD env, set at `docker run` time, so we don't\n * need `-e` on the exec (and the re-launch path on `agentbox start` works\n * without it too).\n */\nexport async function launchVncDaemon(\n container: string,\n timeoutMs = 5000,\n): Promise<VncLaunchResult> {\n const result = await execInBox(container, ['/usr/local/bin/agentbox-vnc-start'], {\n user: 'vscode',\n detach: true,\n });\n if (result.exitCode !== 0) {\n return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const probe = await execInBox(\n container,\n ['bash', '-lc', '(echo > /dev/tcp/127.0.0.1/6080) 2>/dev/null'],\n { user: 'vscode' },\n );\n if (probe.exitCode === 0) return { up: true };\n await new Promise((r) => setTimeout(r, 150));\n }\n return { up: false, reason: `websockify did not bind 6080 within ${String(timeoutMs)}ms` };\n}\n\nconst VNC_PASSWORD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\n/**\n * 8-char password from a 62-symbol alphabet. The 8-char cap is a real RFB\n * protocol limit — VncAuth truncates at compare time, so longer passwords give\n * no security gain. 62^8 ≈ 47 bits; adequate for the loopback-bound surface\n * we expose (host port pinned to 127.0.0.1 + OrbStack's name-based routing,\n * neither of which is reachable from off-host without explicit tunnelling).\n */\nexport function generateVncPassword(): string {\n const bytes = randomBytes(8);\n let out = '';\n for (let i = 0; i < 8; i++) {\n out += VNC_PASSWORD_ALPHABET[bytes[i]! % VNC_PASSWORD_ALPHABET.length];\n }\n return out;\n}\n\n/**\n * Container port the VNC web client (noVNC) binds inside the box. Fixed today;\n * stored on BoxRecord for future-proofing if we ever support multiple displays.\n */\nexport const VNC_CONTAINER_PORT = 6080;\n\nexport interface VncUrls {\n /** OrbStack name-based URL, e.g. http://agentbox-foo.orb.local:6080/... Present only on OrbStack hosts. */\n orbUrl?: string;\n /** Loopback URL via the auto-allocated host port, e.g. http://127.0.0.1:54321/... Present whenever vncHostPort is known. */\n loopbackUrl?: string;\n}\n\n/**\n * Build the noVNC URLs for a box, given the box record + (host engine).\n * `engine === 'orbstack'` triggers the `<container>.orb.local:6080` route;\n * either engine produces the loopback URL when the host port is resolved.\n * Returns an empty object when VNC isn't enabled or the password isn't known.\n */\nexport function buildVncUrls(\n record: {\n container: string;\n vncEnabled?: boolean;\n vncHostPort?: number;\n vncContainerPort?: number;\n vncPassword?: string;\n },\n engine: 'orbstack' | 'docker-desktop' | 'other',\n): VncUrls {\n if (!record.vncEnabled || !record.vncPassword) return {};\n const containerPort = record.vncContainerPort ?? VNC_CONTAINER_PORT;\n const qs = `autoconnect=1&password=${encodeURIComponent(record.vncPassword)}`;\n const urls: VncUrls = {};\n if (engine === 'orbstack') {\n urls.orbUrl = `http://${record.container}.orb.local:${String(containerPort)}/vnc.html?${qs}`;\n }\n if (record.vncHostPort) {\n urls.loopbackUrl = `http://127.0.0.1:${String(record.vncHostPort)}/vnc.html?${qs}`;\n }\n return urls;\n}\n","/**\n * The single container port AgentBox reserves + publishes for a box's web\n * service. The `-p 127.0.0.1:0:80` mapping is created unconditionally at\n * `create` (immutable after `docker run`); the in-box supervisor forwards :80\n * to the `expose:`-flagged service's port once `agentbox.yaml` is set. Mirrors\n * `VNC_CONTAINER_PORT`. Must equal `RESERVED_WEB_PORT` in @agentbox/ctl.\n */\nexport const WEB_CONTAINER_PORT = 80;\n","import { execa } from 'execa';\nimport { readdir, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport interface DetectedGitRepo {\n kind: 'root' | 'nested';\n /** Absolute host path of the repo working tree (== `<workspace>` for root). */\n hostMainRepo: string;\n /** Path relative to the workspace where the repo lives. Empty string for root. */\n relPathFromWorkspace: string;\n}\n\n/**\n * Look for `.git` directories at the workspace root and at every 1st-level\n * subdirectory. Worktree-form `.git` files (regular file containing\n * `gitdir: …`) are intentionally skipped — turning an existing worktree into\n * another worktree gets weird, and the user case for it is rare.\n *\n * The actual worktree creation runs inside the box (`seedWorkspace` in\n * `in-box-git.ts`) against the bind-mounted `.git/`; this function only\n * tells the host where to look.\n */\nexport async function detectGitRepos(workspace: string): Promise<DetectedGitRepo[]> {\n const out: DetectedGitRepo[] = [];\n if (await isGitDir(join(workspace, '.git'))) {\n out.push({ kind: 'root', hostMainRepo: workspace, relPathFromWorkspace: '' });\n }\n let entries: Array<{ name: string; isDirectory: () => boolean }>;\n try {\n entries = await readdir(workspace, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const e of entries) {\n if (!e.isDirectory() || e.name.startsWith('.')) continue;\n const sub = join(workspace, e.name);\n if (await isGitDir(join(sub, '.git'))) {\n out.push({ kind: 'nested', hostMainRepo: sub, relPathFromWorkspace: e.name });\n }\n }\n return out;\n}\n\nasync function isGitDir(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n return s.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Pick `<base>`, `<base>-2`, `<base>-3`, … until git reports no such branch\n * exists. Avoids collision when the user reruns `agentbox create -n same-name`\n * after destroying — the destroyed box's branch still lives in the host repo.\n */\nexport async function pickFreshBranch(hostMainRepo: string, base: string): Promise<string> {\n let candidate = base;\n let suffix = 2;\n while (await branchExists(hostMainRepo, candidate)) {\n candidate = `${base}-${String(suffix++)}`;\n if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);\n }\n return candidate;\n}\n\nasync function branchExists(hostMainRepo: string, name: string): Promise<boolean> {\n const result = await execa(\n 'git',\n ['-C', hostMainRepo, 'show-ref', '--verify', '--quiet', `refs/heads/${name}`],\n { reject: false },\n );\n return result.exitCode === 0;\n}\n\nexport class GitWorktreeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitWorktreeError';\n }\n}\n","import { execa } from 'execa';\nimport { execInBox } from './docker.js';\nimport type { DetectedGitRepo } from './git-worktree.js';\nimport { GitWorktreeError } from './git-worktree.js';\n\n/**\n * Root for per-box git-worktree directories inside the container. Each box\n * registers its worktree at a unique subpath here so the host main repo's\n * worktree registry can list multiple concurrent boxes without path\n * collision; `/workspace` is then a symlink to the per-box dir. Lives under\n * the vscode user's home so it's writable without sudo. Exported for the\n * fs-safe path helper.\n */\nexport const WORKTREE_ROOT = '/home/vscode/.agentbox-worktrees';\n\n/** Sanitize a branch name into an FS-safe path segment. */\nexport function fsSafeBranch(branch: string): string {\n return branch.replace(/[^A-Za-z0-9._-]+/g, '_');\n}\n\n/**\n * Per-box per-repo path at which `git worktree add` registers the worktree\n * inside the container. The agent's working tree path stays `/workspace`\n * (root) / `/workspace/<sub>` (nested) via symlinks created after the add.\n * Unique because the branch name carries the box name, so the host main\n * repo never sees a path collision when multiple boxes from the same\n * project run concurrently.\n */\nexport function gitWorktreePathFor(branch: string): string {\n return `${WORKTREE_ROOT}/${fsSafeBranch(branch)}`;\n}\n\n/**\n * Per-repo carry-over captured on the host before the container starts. The\n * host runs the `git stash create` + `ls-files --others` here against the\n * user's main checkout, then `seedWorkspace` replays both inside the box.\n *\n * `stashSha` and the untracked tarball are stored in the shared `.git/`\n * object database (stash) and as a buffer (untracked) so the container can\n * apply them after `git worktree add` runs against the bind-mounted `.git`.\n */\nexport interface RepoCarryOver {\n repo: DetectedGitRepo;\n /**\n * Agent-visible container path of the worktree (`/workspace` for root,\n * `/workspace/<sub>` for nested). After seedWorkspace runs this is a\n * symlink to `gitWorktreePath`.\n */\n containerPath: string;\n /**\n * Real container path where git registered the worktree\n * (`/home/vscode/.agentbox-worktrees/<fsSafeBranch>`). Per-box unique, so\n * concurrent boxes from the same project don't collide in the host main\n * repo's worktree registry.\n */\n gitWorktreePath: string;\n /** Branch name to pass to `git worktree add -b`. */\n branch: string;\n /** Stash-commit SHA (`git stash create`); null when the host main was clean. */\n stashSha: string | null;\n /**\n * NUL-separated list of repo-relative untracked paths. We tar these up\n * host-side and pipe them in inside seedWorkspace. Empty when no untracked\n * files.\n */\n untrackedNul: string;\n /** Host dir to tar from (== repo.hostMainRepo, kept here so seedWorkspace doesn't need to know about the repo shape). */\n hostSource: string;\n}\n\n/**\n * Collect host-side state for each detected repo so it can be replayed\n * inside the container by `seedWorkspace`. Pure of any docker calls — every\n * shell-out is host git.\n *\n * Branch picking is left to the caller (so it can be allocated before\n * `docker run` and recorded on the BoxRecord regardless of how the rest of\n * create proceeds).\n */\nexport async function collectRepoCarryOver(\n repo: DetectedGitRepo,\n branch: string,\n containerPath: string,\n gitWorktreePath: string,\n): Promise<RepoCarryOver> {\n // `stash create` writes a stash commit without touching the working tree or\n // stash list; empty output = clean. The commit lands in the host's `.git/`\n // object DB, which is bind-mounted into the container — so the in-box\n // worktree can `stash apply <sha>` against it.\n const stash = await execa('git', ['-C', repo.hostMainRepo, 'stash', 'create'], { reject: false });\n const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;\n\n const untracked = await execa(\n 'git',\n ['-C', repo.hostMainRepo, 'ls-files', '--others', '--exclude-standard', '-z'],\n { reject: false },\n );\n const untrackedNul = untracked.exitCode === 0 ? untracked.stdout : '';\n\n return {\n repo,\n containerPath,\n gitWorktreePath,\n branch,\n stashSha,\n untrackedNul,\n hostSource: repo.hostMainRepo,\n };\n}\n\nexport interface SeedWorkspaceOptions {\n container: string;\n /** Repos with collected carry-over, in DAG order: root first, nested after. */\n repos: RepoCarryOver[];\n onLog?: (line: string) => void;\n}\n\n/**\n * docker exec helper that throws GitWorktreeError on non-zero exit. `cwd`\n * defaults to `/` so callers that intentionally rebind `/workspace` (the\n * image's WORKDIR) don't get hit by \"chdir failed: no such file or directory\"\n * on the next exec.\n */\nasync function dexec(\n container: string,\n argv: string[],\n user: 'vscode' | 'root' = 'vscode',\n cwd: string = '/',\n): Promise<void> {\n const r = await execa(\n 'docker',\n ['exec', '-w', cwd, '--user', user, container, ...argv],\n { reject: false },\n );\n if (r.exitCode !== 0) {\n throw new GitWorktreeError(`${argv.join(' ')} failed: ${r.stderr || r.stdout}`);\n }\n}\n\n/**\n * Minimum shape `bindWorktrees` needs — keeps the helper independent of\n * the `RepoCarryOver` / `GitWorktreeRecord` types so both `seedWorkspace`\n * (after creation) and `startBox` (after `docker start`) can call it.\n */\nexport interface WorktreeBindSpec {\n kind: 'root' | 'nested';\n containerPath: string;\n gitWorktreePath: string;\n}\n\n/**\n * Apply the `/workspace` (and `/workspace/<sub>`) bind mounts that expose\n * each per-box git worktree at its canonical agent path.\n *\n * Idempotent for `startBox` re-runs: if the target is already a mountpoint\n * we unmount it first. Root bind first so the nested mount points (created\n * by the root worktree's `worktree add`) exist before we cover them.\n *\n * The mount runs as `root` because `mount(2)` requires `CAP_SYS_ADMIN` —\n * which we already grant the outer container for the in-box dockerd. The\n * bind itself respects file ownership on the source side (vscode-owned),\n * so subsequent in-container operations under `/workspace` work as vscode.\n */\n/**\n * Make the in-container parent directory of each bind-mounted `.git` owned by\n * `vscode`. Docker auto-creates the intermediate path for a bind mount\n * (e.g. `/Users/marco/Projects/Foo/` for a `.git` at `/Users/marco/Projects/Foo/.git`)\n * in the container's writable layer as `root:root 755`. The bind-mounted\n * `.git` itself keeps its host UIDs, but agents (turborepo, build caches,\n * etc.) often want to write *siblings* of `.git` at the project root —\n * `.turbo/`, `.next/`, scratch files — which fails as `vscode` if the parent\n * is root-owned. This flips just that parent dir's UID.\n *\n * NOT recursive on purpose: `chown -R` would descend into `.git` (the\n * bind-mount inode) and propagate ownership changes back to the host,\n * defeating the \"no host perms touched\" property.\n *\n * Best-effort: failures are logged, not thrown — the box still functions,\n * only sibling writes at the project root are affected.\n */\nexport async function chownGitBindParents(args: {\n container: string;\n hostMainRepos: string[];\n onLog?: (line: string) => void;\n}): Promise<void> {\n const log = args.onLog ?? (() => {});\n // Dedupe — nested-repo carry-overs can repeat hostMainRepo.\n const repos = Array.from(new Set(args.hostMainRepos));\n for (const repo of repos) {\n const result = await execInBox(args.container, ['chown', 'vscode:vscode', repo], {\n user: 'root',\n });\n if (result.exitCode === 0) {\n log(`chowned ${repo} to vscode:vscode (parent of bind-mounted .git)`);\n } else {\n const msg = (result.stderr || result.stdout || `exit ${result.exitCode}`).trim();\n log(`chown ${repo} failed (best-effort, ignoring): ${msg}`);\n }\n }\n}\n\nexport async function bindWorktrees(\n container: string,\n binds: WorktreeBindSpec[],\n onLog?: (line: string) => void,\n): Promise<void> {\n const log = onLog ?? (() => {});\n // Root first; nested mountpoints live inside the root worktree's tree so\n // the root bind has to be in place before we cover sub-paths.\n const ordered = [...binds].sort((a, b) =>\n a.kind === 'root' && b.kind !== 'root' ? -1 : a.kind !== 'root' && b.kind === 'root' ? 1 : 0,\n );\n for (const b of ordered) {\n // Best-effort unmount of any leftover bind at the target (idempotent for\n // startBox: container stop drops mounts, but a partial create might\n // leave one in place).\n await execa(\n 'docker',\n ['exec', '-w', '/', '--user', 'root', container, 'sh', '-c', `mountpoint -q ${b.containerPath} && umount ${b.containerPath} || true`],\n { reject: false },\n );\n // For nested: parent must exist. The root bind exposes the root\n // worktree's tracked tree, which typically contains <sub>/, but if the\n // root .gitignores it or the nested repo is in a fresh path, mkdir is\n // needed.\n if (b.kind === 'nested') {\n await dexec(container, ['mkdir', '-p', ctParent(b.containerPath)], 'root');\n await dexec(container, ['mkdir', '-p', b.containerPath], 'root');\n }\n await dexec(container, ['mount', '--bind', b.gitWorktreePath, b.containerPath], 'root');\n log(`bind-mounted ${b.containerPath} <- ${b.gitWorktreePath}`);\n }\n}\n\n/**\n * Materialize each per-box git worktree *inside* the container against the\n * bind-mounted `.git/`, then replay the host's uncommitted state (stash +\n * untracked) into it. Runs as `vscode` (the in-container user) so files in\n * /workspace are owned by uid 1000.\n *\n * Layout: every worktree is registered at a per-box unique path under\n * `WORKTREE_ROOT` (`/home/vscode/.agentbox-worktrees/<fsSafeBranch>`), then\n * a `mount --bind` exposes it at `/workspace` (and `/workspace/<sub>` for\n * nested repos). The uniqueness is load-bearing — the host main repo's\n * worktree registry is keyed by absolute path, so multiple concurrent\n * boxes from the same project must register at *different* paths. They\n * share the object DB (the bind-mounted `.git/`) but have independent\n * HEAD/index in their own `.git/worktrees/<subdir>`.\n *\n * The bind mount (not a symlink) is intentional: getcwd() / realpath() /\n * git rev-parse --show-toplevel / Node's process.cwd() all return\n * `/workspace`. A symlink would leak the per-box physical path everywhere\n * tools canonicalize. Cost: the mount is per-container-namespace and\n * doesn't survive `docker stop`, so `startBox` re-binds via\n * {@link bindWorktrees}. Host's `git worktree list` will show each box's\n * registered path as `/home/vscode/.agentbox-worktrees/...` (container-only\n * — host marks it `prunable`, which is harmless).\n */\nexport async function seedWorkspace(opts: SeedWorkspaceOptions): Promise<void> {\n const log = opts.onLog ?? (() => {});\n\n // Ensure the per-box worktree root exists. Idempotent — multiple boxes\n // can be created in parallel against the same image.\n await dexec(opts.container, ['mkdir', '-p', WORKTREE_ROOT]);\n\n // Phase 1: register each worktree at its per-box unique path.\n for (const r of opts.repos) {\n const main = r.repo.hostMainRepo;\n const wt = r.gitWorktreePath;\n const add = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'vscode',\n opts.container,\n 'git',\n '-C',\n main,\n 'worktree',\n 'add',\n '-b',\n r.branch,\n wt,\n 'HEAD',\n ],\n { reject: false },\n );\n if (add.exitCode !== 0) {\n throw new GitWorktreeError(\n `git worktree add ${wt} (branch ${r.branch}) failed: ${add.stderr || add.stdout}`,\n );\n }\n log(`worktree ${wt} on branch ${r.branch} (host main ${main})`);\n\n // Boxes don't carry the user's signing keys, so commit.gpgsign=true on\n // the host would make every in-box `git commit` fail. Enable per-worktree\n // config on the main repo, then disable signing on just this worktree.\n await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'vscode',\n opts.container,\n 'git',\n '-C',\n main,\n 'config',\n 'extensions.worktreeConfig',\n 'true',\n ],\n { reject: false },\n );\n await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'vscode',\n opts.container,\n 'git',\n '-C',\n wt,\n 'config',\n '--worktree',\n 'commit.gpgsign',\n 'false',\n ],\n { reject: false },\n );\n }\n\n // Phase 2: bind each worktree onto its agent-visible /workspace path.\n await bindWorktrees(\n opts.container,\n opts.repos.map((r) => ({\n kind: r.repo.kind,\n containerPath: r.containerPath,\n gitWorktreePath: r.gitWorktreePath,\n })),\n log,\n );\n\n // Phase 3: replay host carry-over into each worktree (via the\n // /workspace[*] symlinks, so the agent sees the changes at the canonical\n // paths it expects).\n for (const r of opts.repos) {\n const ct = r.containerPath;\n if (r.stashSha) {\n const withIndex = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'vscode',\n opts.container,\n 'git',\n '-C',\n ct,\n 'stash',\n 'apply',\n '--index',\n r.stashSha,\n ],\n { reject: false },\n );\n if (withIndex.exitCode !== 0) {\n const noIndex = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'vscode',\n opts.container,\n 'git',\n '-C',\n ct,\n 'stash',\n 'apply',\n r.stashSha,\n ],\n { reject: false },\n );\n if (noIndex.exitCode !== 0) {\n log(\n `warning: stash apply failed in ${ct} (${withIndex.stderr || withIndex.stdout || 'no message'})`,\n );\n } else {\n log(`applied tracked changes (without index — staged state lost) in ${ct}`);\n }\n } else {\n log(`applied tracked changes from host main into ${ct}`);\n }\n }\n if (r.untrackedNul.length > 0) {\n const tarOut = await execa('tar', ['-C', r.hostSource, '--null', '-T', '-', '-cf', '-'], {\n input: r.untrackedNul.replace(/\\0$/, ''),\n encoding: 'buffer',\n reject: false,\n });\n if (tarOut.exitCode !== 0) {\n log(`warning: tar of untracked files for ${r.repo.hostMainRepo} failed: ${tarOut.stderr}`);\n continue;\n }\n const tarIn = await execa(\n 'docker',\n ['exec', '-i', '--user', 'vscode', opts.container, 'tar', '-C', ct, '-xf', '-'],\n { input: tarOut.stdout as Buffer, reject: false },\n );\n if (tarIn.exitCode !== 0) {\n log(`warning: untracked-file copy into ${ct} failed: ${tarIn.stderr}`);\n } else {\n const count = r.untrackedNul.split('\\0').filter((s) => s.length > 0).length;\n log(`copied ${String(count)} untracked file(s) into ${ct}`);\n }\n }\n }\n}\n\n/**\n * Tar-pipe a host source dir into the container's /workspace. Used for the\n * no-git case (no detected repos), and for the `--host-snapshot` flow where\n * the source is the APFS clone instead of the live workspace.\n *\n * Runs as uid:gid 1000:1000 so extracted files are owned by `vscode` (the\n * in-container user) — same convention as `copyHostEnvFilesToBox`.\n */\nexport async function seedWorkspaceFromDir(opts: {\n container: string;\n hostSource: string;\n onLog?: (line: string) => void;\n}): Promise<void> {\n const log = opts.onLog ?? (() => {});\n const tarOut = await execa('tar', ['-C', opts.hostSource, '-cf', '-', '.'], {\n encoding: 'buffer',\n reject: false,\n });\n if (tarOut.exitCode !== 0) {\n throw new GitWorktreeError(`tar of ${opts.hostSource} failed: ${tarOut.stderr}`);\n }\n const tarIn = await execa(\n 'docker',\n ['exec', '-i', '--user', '1000:1000', opts.container, 'tar', '-C', '/workspace', '-xf', '-'],\n { input: tarOut.stdout as Buffer, reject: false },\n );\n if (tarIn.exitCode !== 0) {\n throw new GitWorktreeError(`tar extract into /workspace failed: ${tarIn.stderr}`);\n }\n log(`seeded /workspace from ${opts.hostSource}`);\n}\n\n/**\n * Remove an in-container worktree from the host's main repo's worktree\n * registry. Called from `destroyBox` per registered worktree. The registered\n * path (`gitWorktreePath`) was a container-only path (under `WORKTREE_ROOT`),\n * so `git worktree remove` will see it as missing and we go straight to\n * `worktree prune` to drop the registry entry. Best-effort throughout.\n */\nexport async function removeInBoxWorktree(args: {\n hostMainRepo: string;\n gitWorktreePath: string;\n}): Promise<void> {\n const remove = await execa(\n 'git',\n ['-C', args.hostMainRepo, 'worktree', 'remove', '--force', args.gitWorktreePath],\n { reject: false },\n );\n if (remove.exitCode === 0) return;\n await execa('git', ['-C', args.hostMainRepo, 'worktree', 'prune'], { reject: false });\n}\n\nfunction ctParent(p: string): string {\n const i = p.lastIndexOf('/');\n return i <= 0 ? '/' : p.slice(0, i);\n}\n","import { execa } from 'execa';\nimport { mkdir, readdir, rm, stat } from 'node:fs/promises';\nimport { homedir, platform } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { sanitizeMnemonic } from '@agentbox/config';\n\n/**\n * Directories whose contents are either platform-specific (built native modules,\n * compiled outputs) or large enough to be wasteful to include in a frozen\n * workspace snapshot. Pruned from the snapshot tree *after* the APFS clone\n * — removing CoW-cloned entries is essentially free.\n */\nexport const EXCLUDE_DIRS: ReadonlySet<string> = new Set([\n 'node_modules',\n '.next',\n '.nuxt',\n '.turbo',\n '.svelte-kit',\n 'dist',\n 'build',\n 'out',\n 'target',\n '.venv',\n '__pycache__',\n '.cache',\n '.parcel-cache',\n]);\n\nexport const SNAPSHOTS_ROOT = join(homedir(), '.agentbox', 'snapshots');\n\n/**\n * `<id>-<n>-<mnemonic>` when `projectIndex` is set (post-feature boxes — all\n * new boxes), else `<id>-<mnemonic>` (legacy pre-feature fallback). Mirrors\n * `boxDirSegment` in `host-export.ts` — kept structurally compatible so the\n * snapshot dir can be looked up alongside its box dir.\n */\nexport function snapshotPathFor(box: { id: string; name: string; projectIndex?: number }): string {\n const mnemonic = sanitizeMnemonic(box.name);\n const n = box.projectIndex;\n const segment =\n typeof n === 'number' && Number.isFinite(n) && n > 0\n ? `${box.id}-${String(n)}-${mnemonic}`\n : `${box.id}-${mnemonic}`;\n return join(SNAPSHOTS_ROOT, segment);\n}\n\n/**\n * Walk a directory tree and return absolute paths of every directory whose\n * basename matches `EXCLUDE_DIRS`. Does not descend into a matched directory.\n * Pure (modulo `fs.readdir`) — easy to unit-test against a fixture tree.\n */\nexport async function findExcludedDirs(\n root: string,\n excluded: ReadonlySet<string> = EXCLUDE_DIRS,\n): Promise<string[]> {\n const matches: string[] = [];\n const walk = async (dir: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const abs = join(dir, entry.name);\n if (excluded.has(entry.name)) {\n matches.push(abs);\n continue; // do not descend\n }\n await walk(abs);\n }\n };\n await walk(root);\n return matches;\n}\n\nexport interface CreateSnapshotOptions {\n source: string;\n destination: string;\n excluded?: ReadonlySet<string>;\n}\n\nexport interface CreateSnapshotResult {\n destination: string;\n prunedPaths: string[];\n}\n\n/**\n * Create a frozen workspace snapshot. On macOS (APFS) this is an instant CoW\n * clone via `cp -cR`; on other platforms it falls back to plain `cp -R`\n * (slow, but functional — the production fallback will be `rsync --exclude`).\n *\n * After the copy, prune all `EXCLUDE_DIRS` directories so the snapshot is free\n * of platform-specific artifacts before it becomes the overlay's lower layer.\n */\nexport async function createSnapshot(opts: CreateSnapshotOptions): Promise<CreateSnapshotResult> {\n const source = resolve(opts.source);\n const destination = resolve(opts.destination);\n const excluded = opts.excluded ?? EXCLUDE_DIRS;\n\n await mkdir(SNAPSHOTS_ROOT, { recursive: true });\n\n // `cp -c` only exists on macOS and is the APFS clone flag.\n const cpArgs = platform() === 'darwin' ? ['-cR'] : ['-R'];\n await execa('cp', [...cpArgs, `${source}/`, destination]);\n\n const toPrune = await findExcludedDirs(destination, excluded);\n await Promise.all(toPrune.map((p) => rm(p, { recursive: true, force: true })));\n\n return { destination, prunedPaths: toPrune };\n}\n\n/** Guard used by tests + by `create.ts` when we don't want to clobber a path. */\nexport async function snapshotExists(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n return s.isDirectory();\n } catch {\n return false;\n }\n}\n","import { stat } from 'node:fs/promises';\nimport { execInBox } from './docker.js';\n\nexport interface CtlLaunchResult {\n up: boolean;\n reason?: string;\n}\n\n/**\n * In-box path the daemon's stdout/stderr are redirected to. `docker exec -d`\n * discards stdio, so without this redirect a crash on startup leaves no trace\n * — the unix socket file lingers (Node doesn't auto-unlink on exit) and any\n * later `agentbox-ctl <op>` connect gets ECONNREFUSED with no log to explain\n * why. The file lives in the container's writable layer; it survives\n * stop/start and is wiped on destroy.\n */\nconst CTL_DAEMON_LOG = '/var/log/agentbox/ctl-daemon.log';\n\n/**\n * Spawn `agentbox-ctl daemon` detached inside the container and wait briefly\n * for the unix socket to appear on the host-mounted path. Best-effort —\n * failure is logged but doesn't fail box creation, since a missing or empty\n * agentbox.yaml is a perfectly valid state.\n */\nexport async function launchCtlDaemon(\n container: string,\n hostSocketPath: string,\n timeoutMs = 3000,\n): Promise<CtlLaunchResult> {\n // Wrap in `sh -c` so the daemon's stdio lands in a log file we can read\n // after a crash. The log dir is pre-created in the image (Dockerfile.box\n // mkdir+chown vscode); `mkdir -p` is a cheap belt-and-braces. `exec` lets\n // the shell replace itself with the daemon for a clean process tree.\n const wrapped = `mkdir -p ${CTL_DAEMON_LOG.replace(/\\/[^/]*$/, '')} && exec agentbox-ctl daemon >>${CTL_DAEMON_LOG} 2>&1`;\n const result = await execInBox(container, ['sh', '-c', wrapped], {\n user: 'vscode',\n detach: true,\n });\n if (result.exitCode !== 0) {\n return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (await pathExists(hostSocketPath)) return { up: true };\n await new Promise((r) => setTimeout(r, 100));\n }\n return {\n up: false,\n reason: `socket ${hostSocketPath} did not appear within ${String(timeoutMs)}ms`,\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","import { execa } from 'execa';\n\n/**\n * Re-own any root-owned file under /home/vscode to the uid-1000 `vscode`\n * user. Root-run `docker exec` steps (checkpoint cleanup, dockerd setup) and\n * any `sudo` the in-box agent runs can leave home-dir files owned by root;\n * since the box's shell and agent both run as `vscode`, those files become\n * silently unwritable (the original symptom: a root-owned `.bash_history`\n * dropping all shell history). Boxes are throwaway dev sandboxes, so healing\n * the whole home dir in one sweep beats per-file whack-a-mole.\n *\n * `--from=root` is load-bearing: it limits the chown to files actually owned\n * by root and skips the (vast) vscode-owned majority. A plain `chown -R`\n * issues a chown syscall per file, which on the box's overlay rootfs forces\n * a copy-up of every image-layer file into the writable layer — ~10s and\n * needless disk bloat. `--from` keeps it a fast (~1s) targeted heal.\n *\n * Best-effort, idempotent: runs at every create + start. A few files are\n * legitimately unfixable (a read-only `.gitconfig` bind-mount, git pack\n * files) — chown's per-file errors are harmless and ignored.\n */\nexport async function ensureHomeOwnedByVscode(container: string): Promise<void> {\n await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'root',\n container,\n 'chown',\n '-R',\n '--from=root',\n 'vscode:vscode',\n '/home/vscode',\n ],\n { reject: false },\n );\n}\n","import { spawn } from 'node:child_process';\nimport { randomBytes } from 'node:crypto';\nimport { existsSync, openSync } from 'node:fs';\nimport { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { request as httpRequest } from 'node:http';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { fileURLToPath } from 'node:url';\nimport {\n DEFAULT_RELAY_PORT,\n RELAY_CONTAINER_NAME,\n RELAY_IMAGE_REF,\n RELAY_NETWORK_NAME,\n type BoxWorktree,\n} from '@agentbox/relay';\nimport { containerExists, removeContainer } from './docker.js';\nimport type { GitWorktreeRecord } from './state.js';\n\nconst STATE_DIR = join(homedir(), '.agentbox');\nconst PID_FILE = join(STATE_DIR, 'relay.pid');\nconst LOG_FILE = join(STATE_DIR, 'relay.log');\n\nexport interface RelayEndpoint {\n /** URL boxes use to reach the relay from inside the container. */\n url: string;\n /** URL host-side processes (the CLI itself) use. */\n hostUrl: string;\n port: number;\n}\n\nconst PORT = DEFAULT_RELAY_PORT;\nconst ENDPOINT: RelayEndpoint = {\n // host.docker.internal is the Docker Desktop / OrbStack-supplied alias for\n // the host's loopback as seen from inside a container. The corresponding\n // `--add-host=host.docker.internal:host-gateway` flag in runBox makes the\n // resolution work on Linux native Docker too.\n url: `http://host.docker.internal:${String(PORT)}`,\n hostUrl: `http://127.0.0.1:${String(PORT)}`,\n port: PORT,\n};\n\nexport interface EnsureRelayOptions {\n onLog?: (line: string) => void;\n}\n\n/**\n * Idempotently bring up the host relay. Spawns the bundled `agentbox-relay`\n * bin as a detached node process bound to 0.0.0.0:8787 (so boxes can reach\n * it via host.docker.internal, and the CLI via 127.0.0.1). Best-effort:\n * failures throw and the caller treats it as \"relay not reachable\".\n *\n * If a legacy relay container from a previous version of agentbox is still\n * around, it's removed first so its bound DNS name doesn't shadow the new\n * host process for any old boxes that happen to still be running.\n */\nexport async function ensureRelay(opts: EnsureRelayOptions = {}): Promise<RelayEndpoint> {\n const log = opts.onLog ?? (() => {});\n await mkdir(STATE_DIR, { recursive: true });\n\n // Migration: kill the old in-docker relay if it's around. The host process\n // wants the same port; the container did NOT publish to host:8787, so there\n // is no actual port collision. We still remove it to avoid confusion (it'd\n // show up in `docker ps -a` forever otherwise).\n if (await containerExists(RELAY_CONTAINER_NAME)) {\n await removeContainer(RELAY_CONTAINER_NAME);\n log(`removed legacy relay container ${RELAY_CONTAINER_NAME}`);\n }\n\n if (await pingHealthz(500)) {\n return ENDPOINT;\n }\n\n const existingPid = await readPidFile();\n if (existingPid !== null && (await processAlive(existingPid))) {\n // Pid exists but healthz isn't responding yet — give it a beat to finish\n // startup. If it stays unresponsive, leave it alone (someone might be\n // debugging it) and let downstream POSTs fail as best-effort.\n for (let i = 0; i < 10; i++) {\n if (await pingHealthz(300)) return ENDPOINT;\n await delay(200);\n }\n log(`relay pid ${String(existingPid)} alive but /healthz unresponsive — proceeding anyway`);\n return ENDPOINT;\n }\n if (existingPid !== null) {\n await unlink(PID_FILE).catch(() => {});\n }\n\n const relayBin = resolveRelayBin();\n const logFd = openSync(LOG_FILE, 'a');\n // The relay shells out to this CLI entry for the checkpoint.create RPC\n // (it only knows the box id; the CLI resolves the rest). Resolve best-effort\n // — if not found the relay's handler reports a clear error.\n const cliEntry = resolveCliEntry();\n const child = spawn(\n process.execPath,\n [relayBin, 'serve', '--port', String(PORT), '--host', '0.0.0.0'],\n {\n detached: true,\n stdio: ['ignore', logFd, logFd],\n env: {\n ...process.env,\n ...(cliEntry ? { AGENTBOX_CLI_ENTRY: cliEntry } : {}),\n },\n },\n );\n child.unref();\n if (typeof child.pid === 'number') {\n await writeFile(PID_FILE, String(child.pid), 'utf8');\n log(`spawned relay host process (pid ${String(child.pid)}, port ${String(PORT)})`);\n }\n\n for (let i = 0; i < 25; i++) {\n if (await pingHealthz(300)) {\n log(`relay reachable on ${ENDPOINT.hostUrl}`);\n return ENDPOINT;\n }\n await delay(200);\n }\n throw new Error(\n `relay did not become reachable on ${ENDPOINT.hostUrl} within 5s; see ${LOG_FILE}`,\n );\n}\n\n/**\n * Locate the `agentbox-relay` bin spawned as a child process. Layouts:\n * 0. env override: `AGENTBOX_RELAY_BIN`\n * 1. bundled CLI (dev + published `agent-box`): this module is bundled into\n * the CLI at `<root>/dist/index.js`, the stage step puts the bin at\n * `<root>/runtime/relay/bin.cjs` (sibling of dist/ in both layouts)\n * 2. legacy workspace: `<repo>/packages/sandbox-docker/dist` ↔ `<repo>/packages/relay/dist/bin.cjs`\n * 3. legacy externalized install: `<...>/node_modules/@agentbox/relay/dist/bin.cjs`\n */\nfunction resolveRelayBin(): string {\n const override = process.env.AGENTBOX_RELAY_BIN;\n if (override && existsSync(override)) return override;\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n resolve(here, '..', 'runtime', 'relay', 'bin.cjs'),\n resolve(here, '..', '..', 'relay', 'dist', 'bin.cjs'),\n resolve(here, '..', '..', '..', '@agentbox', 'relay', 'dist', 'bin.cjs'),\n resolve(here, '..', '..', 'node_modules', '@agentbox', 'relay', 'dist', 'bin.cjs'),\n ];\n for (const c of candidates) {\n if (existsSync(c)) return c;\n }\n throw new Error(\n `could not locate @agentbox/relay bin; tried:\\n ${candidates.join('\\n ')}`,\n );\n}\n\n/**\n * Locate the agentbox CLI entry the relay spawns for `checkpoint.create`.\n * Mirrors {@link resolveRelayBin}'s two layouts:\n * 1. workspace dev: `<repo>/packages/sandbox-docker/dist` ↔ `<repo>/apps/cli/dist/index.js`\n * 2. installed: `<...>/agentbox/node_modules/@agentbox/sandbox-docker/dist` ↔ `<...>/agentbox/dist/index.js`\n * Best-effort: returns null when not found (relay reports a clear error).\n */\nfunction resolveCliEntry(): string | null {\n const override = process.env.AGENTBOX_CLI_ENTRY;\n if (override && existsSync(override)) return override;\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n // Bundled CLI (dev + published): this module IS bundled into the CLI\n // entry, so the entry is index.js next to this file.\n resolve(here, 'index.js'),\n resolve(here, '..', '..', '..', 'apps', 'cli', 'dist', 'index.js'),\n resolve(here, '..', '..', '..', '..', 'dist', 'index.js'),\n ];\n for (const c of candidates) {\n if (existsSync(c)) return c;\n }\n return null;\n}\n\nexport interface StopRelayResult {\n /** True when a live relay process was signalled (and the pidfile cleared). */\n stopped: boolean;\n /** The pid that was found in the pidfile, if any. */\n pid: number | null;\n}\n\n/**\n * Stop the host relay process and clear its pidfile. SIGTERM first, then\n * SIGKILL if it's still alive after a short grace period. Idempotent: a\n * missing/stale pidfile is not an error (returns `{ stopped: false }`).\n *\n * Used by `agentbox update` to reload the relay; the next box command brings\n * it back via {@link ensureRelay} (running the freshly-installed bin).\n */\nexport async function stopRelay(): Promise<StopRelayResult> {\n const pid = await readPidFile();\n if (pid === null) {\n return { stopped: false, pid: null };\n }\n if (!(await processAlive(pid))) {\n await unlink(PID_FILE).catch(() => {});\n return { stopped: false, pid };\n }\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // already gone between the liveness check and the signal\n }\n for (let i = 0; i < 20; i++) {\n if (!(await processAlive(pid))) break;\n await delay(100);\n }\n if (await processAlive(pid)) {\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n // best-effort\n }\n }\n await unlink(PID_FILE).catch(() => {});\n return { stopped: true, pid };\n}\n\nexport interface RelayStatus {\n /** True when /healthz responded with a 2xx. */\n running: boolean;\n /** Pidfile contents (null when missing/unparseable). */\n pid: number | null;\n /** Signal-0 probe on `pid` (false when `pid` is null). */\n pidAlive: boolean;\n /** Configured port (same value as endpoint.port). */\n port: number;\n /** URLs boxes / host-side callers use to reach the relay. */\n endpoint: RelayEndpoint;\n /** Parsed /healthz body; null when the relay isn't responding. */\n health: { boxes: number; events: number } | null;\n /** Absolute path to the pidfile. */\n pidFile: string;\n /** Absolute path to the process log. */\n logFile: string;\n}\n\n/**\n * Read-only snapshot of the host relay's liveness. Combines the two probes the\n * lifecycle code uses internally: pidfile + signal-0 + a short /healthz GET.\n * Three terminal states callers care about:\n * - running: true — healthz ok\n * - running: false, pidAlive: true — zombie (process up, healthz silent)\n * - running: false, pidAlive: false — truly down\n */\nexport async function getRelayStatus(): Promise<RelayStatus> {\n const pid = await readPidFile();\n const pidAlive = pid !== null && (await processAlive(pid));\n const health = await fetchHealthz(300);\n return {\n running: health !== null,\n pid,\n pidAlive,\n port: PORT,\n endpoint: ENDPOINT,\n health: health === null ? null : { boxes: health.boxes, events: health.events },\n pidFile: PID_FILE,\n logFile: LOG_FILE,\n };\n}\n\nfunction pingHealthz(timeoutMs: number): Promise<boolean> {\n return new Promise<boolean>((resolveP) => {\n const req = httpRequest(\n { host: '127.0.0.1', port: PORT, method: 'GET', path: '/healthz', timeout: timeoutMs },\n (res) => {\n res.resume();\n const status = res.statusCode ?? 0;\n resolveP(status >= 200 && status < 300);\n },\n );\n req.on('error', () => resolveP(false));\n req.on('timeout', () => {\n req.destroy();\n resolveP(false);\n });\n req.end();\n });\n}\n\ninterface HealthzBody {\n ok: boolean;\n boxes: number;\n events: number;\n}\n\nfunction fetchHealthz(timeoutMs: number): Promise<HealthzBody | null> {\n return new Promise<HealthzBody | null>((resolveP) => {\n const req = httpRequest(\n { host: '127.0.0.1', port: PORT, method: 'GET', path: '/healthz', timeout: timeoutMs },\n (res) => {\n const status = res.statusCode ?? 0;\n if (status < 200 || status >= 300) {\n res.resume();\n resolveP(null);\n return;\n }\n const chunks: Buffer[] = [];\n res.on('data', (c: Buffer) => chunks.push(c));\n res.on('end', () => {\n try {\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as Partial<HealthzBody>;\n if (\n typeof parsed.ok === 'boolean' &&\n typeof parsed.boxes === 'number' &&\n typeof parsed.events === 'number'\n ) {\n resolveP({ ok: parsed.ok, boxes: parsed.boxes, events: parsed.events });\n } else {\n resolveP(null);\n }\n } catch {\n resolveP(null);\n }\n });\n res.on('error', () => resolveP(null));\n },\n );\n req.on('error', () => resolveP(null));\n req.on('timeout', () => {\n req.destroy();\n resolveP(null);\n });\n req.end();\n });\n}\n\nasync function readPidFile(): Promise<number | null> {\n try {\n const text = await readFile(PID_FILE, 'utf8');\n const pid = Number.parseInt(text.trim(), 10);\n return Number.isFinite(pid) && pid > 0 ? pid : null;\n } catch {\n return null;\n }\n}\n\nasync function processAlive(pid: number): Promise<boolean> {\n try {\n // Signal 0 is the existence probe: throws if no such process.\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function generateRelayToken(): string {\n return randomBytes(32).toString('hex');\n}\n\nexport interface RegisterBoxArgs {\n boxId: string;\n token: string;\n name: string;\n /** Docker container name; lets the relay `docker pause` the box for auto-pause. */\n containerName?: string;\n /** ISO-8601 box-creation time (BoxRecord.createdAt); auto-pause tie-break. */\n createdAt?: string;\n /**\n * 1-based per-project box index. Forwarded so the relay's status-store\n * builds the same `<id>-<n>-<mnemonic>` dir segment the host's\n * `boxRunDirFor` uses. Absent for legacy boxes.\n */\n projectIndex?: number;\n /**\n * Subset of BoxRecord.gitWorktrees the relay needs to dispatch git RPCs.\n * Empty/omitted for boxes without git repos.\n */\n worktrees?: GitWorktreeRecord[];\n}\n\nexport async function registerBoxWithRelay(args: RegisterBoxArgs): Promise<void> {\n const worktrees: BoxWorktree[] = (args.worktrees ?? []).map((w) => ({\n containerPath: w.containerPath,\n hostMainRepo: w.hostMainRepo,\n branch: w.branch,\n }));\n await adminPost('/admin/register-box', {\n boxId: args.boxId,\n token: args.token,\n name: args.name,\n containerName: args.containerName,\n createdAt: args.createdAt,\n projectIndex: args.projectIndex,\n worktrees,\n });\n}\n\nexport async function forgetBoxFromRelay(boxId: string): Promise<void> {\n try {\n await adminPost('/admin/forget-box', { boxId });\n } catch {\n // best-effort\n }\n}\n\n/**\n * Best-effort: register an informational notice for a box so attached\n * `agentbox claude` footers / the dashboard show it (e.g. a spinner while a\n * checkpoint freezes the box). Returns the notice id, or null when the relay\n * is unreachable / too old to know the route — the caller treats a null id\n * as \"nothing to clear later\". Never throws: a missing notice must not fail\n * the operation it was decorating.\n */\nexport async function setRelayNotice(\n boxId: string,\n kind: string,\n message: string,\n ttlMs?: number,\n): Promise<string | null> {\n try {\n const body = await adminPostForJson('/admin/notices/set', {\n boxId,\n kind,\n message,\n ...(typeof ttlMs === 'number' ? { ttlMs } : {}),\n });\n const id = (body as { id?: unknown } | null)?.id;\n return typeof id === 'string' && id.length > 0 ? id : null;\n } catch {\n return null;\n }\n}\n\n/** Best-effort: clear a notice previously set via {@link setRelayNotice}. */\nexport async function clearRelayNotice(boxId: string, id: string): Promise<void> {\n try {\n await adminPost('/admin/notices/clear', { boxId, id });\n } catch {\n // best-effort\n }\n}\n\nasync function adminPost(path: string, body: unknown): Promise<void> {\n const json = JSON.stringify(body);\n await new Promise<void>((resolveP, rejectP) => {\n const req = httpRequest(\n {\n host: '127.0.0.1',\n port: PORT,\n method: 'POST',\n path,\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json).toString(),\n },\n timeout: 3000,\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (c: Buffer) => chunks.push(c));\n res.on('end', () => {\n const status = res.statusCode ?? 0;\n if (status >= 200 && status < 300) {\n resolveP();\n } else {\n const text = Buffer.concat(chunks).toString('utf8');\n rejectP(new Error(`relay ${path} → ${String(status)}: ${text}`));\n }\n });\n },\n );\n req.on('error', rejectP);\n req.on('timeout', () => {\n req.destroy();\n rejectP(new Error(`relay ${path} timeout`));\n });\n req.write(json);\n req.end();\n });\n}\n\n/** Like {@link adminPost} but resolves with the parsed JSON response body. */\nasync function adminPostForJson(path: string, body: unknown): Promise<unknown> {\n const json = JSON.stringify(body);\n return new Promise<unknown>((resolveP, rejectP) => {\n const req = httpRequest(\n {\n host: '127.0.0.1',\n port: PORT,\n method: 'POST',\n path,\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json).toString(),\n },\n timeout: 3000,\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (c: Buffer) => chunks.push(c));\n res.on('end', () => {\n const status = res.statusCode ?? 0;\n if (status < 200 || status >= 300) {\n rejectP(new Error(`relay ${path} → ${String(status)}`));\n return;\n }\n const text = Buffer.concat(chunks).toString('utf8');\n try {\n resolveP(text.length > 0 ? JSON.parse(text) : {});\n } catch (err) {\n rejectP(err instanceof Error ? err : new Error(String(err)));\n }\n });\n res.on('error', rejectP);\n },\n );\n req.on('error', rejectP);\n req.on('timeout', () => {\n req.destroy();\n rejectP(new Error(`relay ${path} timeout`));\n });\n req.write(json);\n req.end();\n });\n}\n\nexport interface BoxWithToken {\n id: string;\n name: string;\n container?: string;\n createdAt?: string;\n relayToken?: string;\n projectIndex?: number;\n gitWorktrees?: GitWorktreeRecord[];\n}\n\n/**\n * Re-push every known (id, token) to the relay's in-memory registry. Called\n * after `ensureRelay()` so a fresh / restarted relay learns about boxes that\n * were created in a previous CLI invocation.\n */\nexport async function rehydrateRelayRegistry(boxes: BoxWithToken[]): Promise<void> {\n for (const b of boxes) {\n if (!b.relayToken) continue;\n try {\n await registerBoxWithRelay({\n boxId: b.id,\n token: b.relayToken,\n name: b.name,\n containerName: b.container,\n createdAt: b.createdAt,\n projectIndex: b.projectIndex,\n worktrees: b.gitWorktrees,\n });\n } catch {\n // best-effort\n }\n }\n}\n\nexport { RELAY_CONTAINER_NAME, RELAY_NETWORK_NAME, RELAY_IMAGE_REF, DEFAULT_RELAY_PORT };\n","import { ensureVolume, execInBox, type DockerExecResult } from './docker.js';\n\nexport type IdeFlavor = 'vscode' | 'cursor';\n\ninterface IdeProfile {\n /** Container path the IDE's server installs into. */\n serverDir: string;\n /** Container path of the extensions subdir under serverDir. */\n extensionsDir: string;\n /** Per-box volume name = perBoxVolumePrefix + boxId. */\n perBoxVolumePrefix: string;\n /** Shared extensions volume name (never auto-removed). */\n sharedExtensionsVolume: string;\n /** Host CLI binary that opens this IDE (`code` / `cursor`). */\n cli: string;\n /** Human-readable label used in CLI output. */\n displayName: string;\n /** macOS protocol scheme for the `open` fallback (no trailing colon). */\n protocolScheme: string;\n}\n\nconst PROFILES: Record<IdeFlavor, IdeProfile> = {\n vscode: {\n serverDir: '/home/vscode/.vscode-server',\n extensionsDir: '/home/vscode/.vscode-server/extensions',\n perBoxVolumePrefix: 'agentbox-vscode-server-',\n sharedExtensionsVolume: 'agentbox-vscode-extensions',\n cli: 'code',\n displayName: 'VS Code',\n protocolScheme: 'vscode',\n },\n cursor: {\n serverDir: '/home/vscode/.cursor-server',\n extensionsDir: '/home/vscode/.cursor-server/extensions',\n perBoxVolumePrefix: 'agentbox-cursor-server-',\n sharedExtensionsVolume: 'agentbox-cursor-extensions',\n cli: 'cursor',\n displayName: 'Cursor',\n protocolScheme: 'cursor',\n },\n};\n\nexport const IDE_FLAVORS: readonly IdeFlavor[] = ['vscode', 'cursor'];\n\nexport function ideProfile(flavor: IdeFlavor): IdeProfile {\n return PROFILES[flavor];\n}\n\n/**\n * Shared across all boxes. Holds downloaded VS Code extensions so the second\n * box onward doesn't re-download them. Never auto-removed by destroy/prune\n * (parallel to SHARED_CLAUDE_VOLUME).\n */\nexport const SHARED_VSCODE_EXTENSIONS_VOLUME = PROFILES.vscode.sharedExtensionsVolume;\n\n/** Same idea for Cursor's downloaded extensions. */\nexport const SHARED_CURSOR_EXTENSIONS_VOLUME = PROFILES.cursor.sharedExtensionsVolume;\n\n/** Per-box VS Code server volume name. Holds server binary + TS cache + workspace state. */\nexport function vscodeServerVolumeName(boxId: string): string {\n return ideServerVolumeName('vscode', boxId);\n}\n\n/** Per-box Cursor server volume name. */\nexport function cursorServerVolumeName(boxId: string): string {\n return ideServerVolumeName('cursor', boxId);\n}\n\nexport function ideServerVolumeName(flavor: IdeFlavor, boxId: string): string {\n return `${PROFILES[flavor].perBoxVolumePrefix}${boxId}`;\n}\n\nexport interface IdeMounts {\n /** Volume names to ensure() before runBox. */\n volumes: string[];\n /** `-v` arg values to pass to runBox. */\n extraVolumes: string[];\n}\n\n/**\n * Build the volume mounts for one IDE flavor: per-box `.vscode-server` (or\n * `.cursor-server`) mounts first, then the shared extensions volume layered\n * over its `extensions` subdir.\n */\nexport function buildFlavorMounts(flavor: IdeFlavor, boxId: string): IdeMounts {\n const profile = PROFILES[flavor];\n const perBox = ideServerVolumeName(flavor, boxId);\n return {\n volumes: [perBox, profile.sharedExtensionsVolume],\n extraVolumes: [\n `${perBox}:${profile.serverDir}`,\n `${profile.sharedExtensionsVolume}:${profile.extensionsDir}`,\n ],\n };\n}\n\n/** VS Code subset — kept for callers that only want the VS Code mounts. */\nexport function buildVscodeMounts(boxId: string): IdeMounts {\n return buildFlavorMounts('vscode', boxId);\n}\n\n/**\n * All IDE flavors' mounts unioned together. This is what createBox uses so\n * any existing box can be opened with either IDE without recreating.\n */\nexport function buildIdeMounts(boxId: string): IdeMounts {\n const merged: IdeMounts = { volumes: [], extraVolumes: [] };\n for (const f of IDE_FLAVORS) {\n const m = buildFlavorMounts(f, boxId);\n merged.volumes.push(...m.volumes);\n merged.extraVolumes.push(...m.extraVolumes);\n }\n return merged;\n}\n\n/** Ensure VS Code's volumes exist. */\nexport async function ensureVscodeVolumes(boxId: string): Promise<void> {\n for (const v of buildFlavorMounts('vscode', boxId).volumes) await ensureVolume(v);\n}\n\n/** Ensure every IDE flavor's volumes exist. */\nexport async function ensureIdeVolumes(boxId: string): Promise<void> {\n for (const v of buildIdeMounts(boxId).volumes) await ensureVolume(v);\n}\n\n/**\n * Belt-and-suspenders chown of the server trees after the named volumes are\n * mounted. The Dockerfile pre-creates these dirs so first-mount inherits\n * vscode:vscode ownership, but a shared extensions volume might already exist\n * from a box created against an older image where the dirs weren't seeded —\n * in that case the volume is root-owned and the Dev Containers extension\n * fails with \"mkdir: cannot create directory '<server>/bin': Permission\n * denied\". One docker exec fixes it idempotently for both flavors.\n */\nexport async function repairVscodeServerOwnership(container: string): Promise<void> {\n await execInBox(container, ['chown', '-R', 'vscode:vscode', PROFILES.vscode.serverDir], {\n user: 'root',\n });\n}\n\nexport async function repairIdeOwnership(container: string): Promise<void> {\n for (const flavor of IDE_FLAVORS) {\n await execInBox(container, ['chown', '-R', 'vscode:vscode', PROFILES[flavor].serverDir], {\n user: 'root',\n });\n }\n}\n\n/**\n * VS Code's `vscode-remote://attached-container+<hex>/...` URI takes the\n * *container name* hex-encoded. Cursor uses the same URI scheme (it's a fork)\n * — pass it to `cursor --folder-uri` the same way.\n */\nexport function containerHex(containerName: string): string {\n return Buffer.from(containerName, 'utf8').toString('hex');\n}\n\n/**\n * Resource URI for an attached container. Consumed by `code --folder-uri` /\n * `cursor --folder-uri`.\n *\n * Note: the `vscode://vscode-remote/...` protocol-handler form looks similar\n * but goes through macOS `open`, which percent-encodes the `+` authority\n * separator into `%2B` and the Dev Containers extension then fails to\n * resolve it. Use `<cli> --folder-uri <this>` to bypass that.\n */\nexport function attachedContainerUri(containerName: string, workspacePath = '/workspace'): string {\n return `vscode-remote://attached-container+${containerHex(containerName)}${workspacePath}`;\n}\n\n/**\n * agentbox-managed `.vscode/tasks.json` lives in the overlay's upper layer so\n * it doesn't pollute the host's working tree. The sentinel comment lets us\n * detect our own file and regenerate it on every `agentbox code` invocation\n * without overwriting a user-authored one. The file lives at `.vscode/` —\n * Cursor reads the same path (VS Code fork), so no per-IDE variant needed.\n */\nconst SENTINEL =\n '// agentbox-managed: regenerated on `agentbox code`; remove this header to take ownership';\n\nexport type ServiceTailHint = { name: string };\n\nexport interface EnsureTasksFileResult {\n status: 'wrote' | 'skipped-user-owned' | 'skipped-no-services';\n}\n\n/**\n * Write (or skip) `/workspace/.vscode/tasks.json` inside the container. Each\n * service in `services` gets a background task that tails its log so the IDE\n * shows a dedicated terminal panel on attach.\n *\n * - File absent → write.\n * - File present with our sentinel → overwrite.\n * - File present without sentinel → skip (user owns it). Caller can force\n * by setting `regen: true`.\n */\nexport async function ensureAgentboxTasksFile(\n container: string,\n services: ServiceTailHint[],\n opts: { regen?: boolean } = {},\n): Promise<EnsureTasksFileResult> {\n if (services.length === 0) return { status: 'skipped-no-services' };\n\n const existing = await execInBox(container, ['cat', '/workspace/.vscode/tasks.json'], {\n user: 'vscode',\n });\n if (existing.exitCode === 0 && !opts.regen && !existing.stdout.includes(SENTINEL)) {\n return { status: 'skipped-user-owned' };\n }\n\n const tasks = services.map((s) => ({\n label: `agentbox: ${s.name}`,\n type: 'shell',\n command: `tail -F /var/log/agentbox/${s.name}.log`,\n isBackground: true,\n presentation: { panel: 'dedicated', reveal: 'always', echo: false },\n runOptions: { runOn: 'folderOpen' },\n problemMatcher: [] as unknown[],\n }));\n const body =\n `${SENTINEL}\\n` +\n JSON.stringify(\n {\n version: '2.0.0',\n tasks,\n },\n null,\n 2,\n ) +\n '\\n';\n\n await execInBox(container, ['mkdir', '-p', '/workspace/.vscode'], { user: 'vscode' });\n const write = await writeFileInBox(container, '/workspace/.vscode/tasks.json', body);\n if (write.exitCode !== 0) {\n throw new Error(`failed to write tasks.json in ${container}: ${write.stderr || write.stdout}`);\n }\n return { status: 'wrote' };\n}\n\nasync function writeFileInBox(\n container: string,\n path: string,\n content: string,\n): Promise<DockerExecResult> {\n const { execa } = await import('execa');\n const result = await execa(\n 'docker',\n ['exec', '-i', '--user', 'vscode', container, 'sh', '-c', `cat > ${shellQuote(path)}`],\n { input: content, reject: false },\n );\n return {\n exitCode: result.exitCode ?? -1,\n stdout: result.stdout ?? '',\n stderr: result.stderr ?? '',\n };\n}\n\nfunction shellQuote(s: string): string {\n return `'${s.replace(/'/g, `'\\\\''`)}'`;\n}\n\n// Backward-compat alias for the previous mount type name.\nexport type VscodeMounts = IdeMounts;\n","export const DEFAULT_RELAY_PORT = 8787;\nexport const RELAY_CONTAINER_NAME = 'agentbox-relay';\nexport const RELAY_NETWORK_NAME = 'agentbox-net';\nexport const RELAY_IMAGE_REF = 'agentbox/relay:dev';\nexport const RELAY_EVENT_RING_SIZE = 1000;\n\nexport interface BoxRegistration {\n boxId: string;\n token: string;\n name: string;\n /** ISO-8601 time the relay received this registration. */\n registeredAt: string;\n /** Docker container name; the relay needs it to `docker pause` an idle box. */\n containerName?: string;\n /** ISO-8601 box-creation time (BoxRecord.createdAt); used as a tie-break in auto-pause ordering. */\n createdAt?: string;\n /**\n * 1-based per-project box index (`agentbox list`'s `N` column). When set,\n * the relay writes status.json under `~/.agentbox/boxes/<id>-<n>-<mnemonic>/`\n * to match the host's `boxDirSegment` helper. Absent for legacy\n * (pre-feature) boxes; absent registrations fall back to `<id>-<mnemonic>`.\n */\n projectIndex?: number;\n /**\n * Container-path → host-worktree-dir mapping the host uses to resolve\n * git.pull/git.push RPCs. Empty when the box has no git repos.\n */\n worktrees?: BoxWorktree[];\n}\n\nexport interface BoxWorktree {\n /** Path inside the container (e.g. /workspace, /workspace/app). */\n containerPath: string;\n /**\n * Absolute host path of the main repo whose `.git/` is shared with the\n * container. `git push/fetch` RPCs run with `git -C <hostMainRepo>` — the\n * worktree's working tree lives inside the container's writable layer, but\n * refs/objects are in this shared `.git/`, so push from the main repo dir\n * sees the in-container commits.\n */\n hostMainRepo: string;\n /** Branch the in-container worktree was created on (`agentbox/<box-name>`). */\n branch: string;\n}\n\nexport interface RelayEvent {\n /** Monotonic per-relay-process id, useful for `since=` polling. */\n id: number;\n /** Box id that posted the event. */\n boxId: string;\n /** Free-form event type, e.g. 'service-state', 'task-state', 'notify'. */\n type: string;\n /** ISO-8601 timestamp the relay assigned on receipt. */\n receivedAt: string;\n /** ISO-8601 client-supplied timestamp, if any. */\n ts?: string;\n /** Arbitrary JSON payload. */\n payload?: unknown;\n}\n\nexport interface PostEventBody {\n type: string;\n ts?: string;\n payload?: unknown;\n}\n\nexport interface PostRpcBody {\n method: string;\n params?: unknown;\n}\n\nexport interface RegisterBoxBody {\n boxId: string;\n token: string;\n name: string;\n containerName?: string;\n createdAt?: string;\n /**\n * 1-based per-project box index. Optional — additive; older boxes and\n * legacy (pre-feature) records register without it and the status path\n * falls back to `<id>-<mnemonic>`.\n */\n projectIndex?: number;\n worktrees?: BoxWorktree[];\n}\n\nexport interface GitRpcParams {\n /** Container path identifying which worktree to run against. Defaults to /workspace. */\n path?: string;\n /** Remote name; defaults to 'origin'. */\n remote?: string;\n /** Extra argv tail appended after the standard args (e.g. ['--set-upstream', 'origin', 'branch']). */\n args?: string[];\n}\n\nexport interface GitRpcResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface CheckpointRpcParams {\n /** Checkpoint name; defaults host-side to `<box-name>-<next>`. */\n name?: string;\n /** Flatten lower+upper into one tree instead of a layered delta. */\n merged?: boolean;\n /** Mark the new checkpoint as the project default. */\n setDefault?: boolean;\n /**\n * If a checkpoint with the same name exists, rm it (manifest + image)\n * before capturing. Makes the call safe to retry — useful when the\n * agent's harness lost the previous invocation's stdout and can't tell\n * whether it succeeded.\n */\n replace?: boolean;\n}\n\n/**\n * First-cut prompt UX is a y/N confirmation in the host wrapper's footer.\n * `select` / `text` are reserved for a follow-up that grows the footer to\n * two rows; keeping the kind in the wire from day one means the host\n * wrapper can ignore unknown kinds gracefully when an older box hits a\n * newer relay (and vice-versa).\n */\nexport type PromptKind = 'confirm';\n\nexport interface PromptContext {\n /** Short label, e.g. \"git push\" or \"cp toHost: /workspace/x -> ~/dl/x\". */\n command?: string;\n /** Container path of the calling cwd, when known. */\n cwd?: string;\n /** Full argv; wrapper truncates for the footer. */\n argv?: string[];\n}\n\n/**\n * The shape pushed over SSE on `event: prompt-ask`. Also the shape the\n * relay-internal `askPrompt()` helper produces. Not a /rpc method — the\n * relay generates these itself when it is about to take a host-side\n * action; the in-box ctl never asks for prompts directly.\n */\nexport interface PromptAskEvent {\n /** Relay-generated UUIDv4 (the wrapper echoes it back in the answer). */\n id: string;\n kind: PromptKind;\n /** Primary question; wrapper truncates to footer width. */\n message: string;\n /** Optional second-line context; wrapper may show or skip. */\n detail?: string;\n /** Default when the user just hits Enter; default 'n' so y/N is the safe shape. */\n defaultAnswer?: 'y' | 'n';\n context?: PromptContext;\n}\n\n/** Body of `POST /admin/prompts/answer`. */\nexport interface PromptAnswerBody {\n id: string;\n answer: 'y' | 'n';\n /** Set when the user dismissed the prompt (Esc / Ctrl-c); treated as 'n'. */\n cancelled?: boolean;\n}\n\n/**\n * Notice kinds. `checkpoint` is the first — a box is transiently frozen\n * while a checkpoint is captured (`docker commit` pauses the container).\n * Kept open-ended like {@link PromptKind} so an older wrapper degrades\n * gracefully (renders the message) when a newer relay sends a new kind.\n */\nexport type NoticeKind = 'checkpoint';\n\n/**\n * The shape pushed over SSE on `event: notice-set`. Unlike a prompt, a\n * notice is purely informational — there is no answer and the box's RPC\n * is not blocked on it. The host wrapper renders it as an animated footer\n * line so the user knows the box is busy, not stuck. Cleared by a\n * `notice-clear` event carrying `{ id }`.\n */\nexport interface BoxNoticeEvent {\n /** Relay-generated UUIDv4. */\n id: string;\n kind: NoticeKind;\n /** Warning text; the wrapper truncates to footer width. */\n message: string;\n}\n\n/** Body of `POST /admin/notices/set`; the response is `{ id }`. */\nexport interface SetNoticeBody {\n boxId: string;\n kind: NoticeKind;\n message: string;\n /** Auto-expiry backstop in ms; defaults relay-side. */\n ttlMs?: number;\n}\n\n/** Body of `POST /admin/notices/clear`. */\nexport interface ClearNoticeBody {\n boxId: string;\n id: string;\n}\n\nexport interface CpRpcParams {\n /** Container-side path. */\n boxPath: string;\n /** Host-side path (dst for toHost, src for fromHost). */\n hostPath: string;\n /** Defaults true; relay always uses `docker exec tar` (recursive). */\n recursive?: boolean;\n}\n\nexport interface BrowserOpenRpcParams {\n /** Absolute http(s) URL to open in the host's default browser. */\n url: string;\n}\n\nexport type DownloadKind = 'workspace' | 'env' | 'config' | 'claude';\n\nexport interface DownloadRpcParams {\n kind: DownloadKind;\n /**\n * Host destination override. Reserved — the v1 relay ignores it and uses\n * the host CLI's defaults (`box.workspacePath`, or `~/.claude`). Kept in\n * the wire so a later upgrade can land without bumping the type.\n */\n hostPath?: string;\n /** Reserved for per-kind flags (e.g. workspace: includeNodeModules). */\n options?: Record<string, unknown>;\n}\n","import type { BoxRegistration, RelayEvent } from './types.js';\nimport { RELAY_EVENT_RING_SIZE } from './types.js';\n\nexport class BoxRegistry {\n private readonly map = new Map<string, BoxRegistration>();\n\n register(reg: BoxRegistration): void {\n this.map.set(reg.boxId, reg);\n }\n\n forget(boxId: string): boolean {\n return this.map.delete(boxId);\n }\n\n /** Returns the registration whose token matches, or null. */\n authenticate(token: string): BoxRegistration | null {\n if (token.length === 0) return null;\n for (const reg of this.map.values()) {\n if (reg.token === token) return reg;\n }\n return null;\n }\n\n get(boxId: string): BoxRegistration | undefined {\n return this.map.get(boxId);\n }\n\n list(): BoxRegistration[] {\n return [...this.map.values()];\n }\n\n size(): number {\n return this.map.size;\n }\n}\n\nexport class EventBuffer {\n private readonly buf: RelayEvent[] = [];\n private nextId = 1;\n constructor(private readonly capacity: number = RELAY_EVENT_RING_SIZE) {}\n\n append(input: Omit<RelayEvent, 'id' | 'receivedAt'>): RelayEvent {\n const ev: RelayEvent = {\n id: this.nextId++,\n receivedAt: new Date().toISOString(),\n ...input,\n };\n this.buf.push(ev);\n if (this.buf.length > this.capacity) this.buf.shift();\n return ev;\n }\n\n /** Returns events with id > since. If `box` is given, filters to that box. */\n since(since: number, box?: string): RelayEvent[] {\n const out: RelayEvent[] = [];\n for (const ev of this.buf) {\n if (ev.id <= since) continue;\n if (box && ev.boxId !== box) continue;\n out.push(ev);\n }\n return out;\n }\n\n all(): RelayEvent[] {\n return this.buf.slice();\n }\n\n size(): number {\n return this.buf.length;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport type { ServerResponse } from 'node:http';\nimport type { PromptAnswerBody, PromptAskEvent } from './types.js';\n\n/**\n * Resolution shape passed back through `askPrompt`'s Promise. Mirrors the\n * shape the wrapper POSTs to `/admin/prompts/answer` minus the id (the\n * caller already knows it).\n */\nexport interface PromptResolution {\n answer: 'y' | 'n';\n cancelled?: boolean;\n}\n\ninterface PendingPromptEntry {\n ev: PromptAskEvent;\n boxId: string;\n resolve: (r: PromptResolution) => void;\n createdAt: string;\n}\n\n/**\n * In-memory pending prompts map. The relay's host-action handlers (git push,\n * cp.*, download.*) put a pending entry here and await the Promise; the\n * wrapper's POST to `/admin/prompts/answer` resolves it. Entries live for\n * however long the user takes — per the design we block indefinitely until\n * a wrapper attaches and answers.\n */\nexport class PendingPrompts {\n private readonly entries = new Map<string, PendingPromptEntry>();\n\n add(boxId: string, ev: PromptAskEvent): Promise<PromptResolution> {\n return new Promise<PromptResolution>((resolve) => {\n this.entries.set(ev.id, {\n ev,\n boxId,\n resolve,\n createdAt: new Date().toISOString(),\n });\n });\n }\n\n /**\n * Idempotent: returns true if a pending entry was found + resolved, false\n * otherwise. The /admin/prompts/answer handler uses the bool to decide\n * 204 vs 404 — the wrapper treats both as \"we're done.\"\n */\n resolve(id: string, answer: 'y' | 'n', cancelled?: boolean): boolean {\n const entry = this.entries.get(id);\n if (!entry) return false;\n this.entries.delete(id);\n entry.resolve({ answer, cancelled });\n return true;\n }\n\n /**\n * Snapshot of all pending prompts for a given box; used to flush the\n * backlog to a newly-attached SSE subscriber.\n */\n forBox(boxId: string): PromptAskEvent[] {\n const out: PromptAskEvent[] = [];\n for (const entry of this.entries.values()) {\n if (entry.boxId === boxId) out.push(entry.ev);\n }\n return out;\n }\n\n /** boxId that owns a pending prompt id, or null when unknown. */\n boxFor(id: string): string | null {\n const entry = this.entries.get(id);\n return entry ? entry.boxId : null;\n }\n\n size(): number {\n return this.entries.size;\n }\n}\n\n/**\n * Tracks the set of host-side wrappers (SSE clients) currently subscribed\n * per box. `broadcast` writes to every subscriber so the user can answer\n * from whichever attached window they happen to be in.\n */\nexport class PromptSubscribers {\n private readonly byBox = new Map<string, Set<ServerResponse>>();\n\n add(boxId: string, res: ServerResponse): void {\n let set = this.byBox.get(boxId);\n if (!set) {\n set = new Set();\n this.byBox.set(boxId, set);\n }\n set.add(res);\n }\n\n remove(boxId: string, res: ServerResponse): void {\n const set = this.byBox.get(boxId);\n if (!set) return;\n set.delete(res);\n if (set.size === 0) this.byBox.delete(boxId);\n }\n\n forBox(boxId: string): ServerResponse[] {\n const set = this.byBox.get(boxId);\n return set ? Array.from(set) : [];\n }\n\n /**\n * Fire-and-forget broadcast. SSE writes that fail (closed socket) are\n * swallowed — the `res.on('close')` handler in the server route already\n * deregisters the dead subscriber.\n */\n broadcast(boxId: string, event: string, data: unknown): void {\n const set = this.byBox.get(boxId);\n if (!set) return;\n const payload = `event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`;\n for (const res of set) {\n try {\n res.write(payload);\n } catch {\n /* dead socket; close handler will deregister */\n }\n }\n }\n}\n\n/**\n * Internal API used by handleGitRpc / handleCpRpc / handleDownloadRpc.\n * Generates a UUID, adds a pending entry, broadcasts the SSE event, and\n * awaits the answer. Respects `process.env.AGENTBOX_PROMPT === 'off'` —\n * auto-accepts without broadcasting (useful for headless scripts and tests).\n */\nexport async function askPrompt(\n prompts: PendingPrompts,\n subscribers: PromptSubscribers,\n boxId: string,\n params: Omit<PromptAskEvent, 'id'>,\n): Promise<PromptResolution> {\n if (process.env.AGENTBOX_PROMPT === 'off') {\n return { answer: 'y' };\n }\n const ev: PromptAskEvent = { id: randomUUID(), ...params };\n const promise = prompts.add(boxId, ev);\n subscribers.broadcast(boxId, 'prompt-ask', ev);\n return promise;\n}\n\n/** Helper for the answer body — used by the relay server to validate. */\nexport function isPromptAnswerBody(v: unknown): v is PromptAnswerBody {\n if (!v || typeof v !== 'object') return false;\n const o = v as Record<string, unknown>;\n if (typeof o.id !== 'string' || o.id.length === 0) return false;\n if (o.answer !== 'y' && o.answer !== 'n') return false;\n if (o.cancelled !== undefined && typeof o.cancelled !== 'boolean') return false;\n return true;\n}\n","import { randomUUID } from 'node:crypto';\nimport type { PromptSubscribers } from './prompts.js';\nimport type { BoxNoticeEvent, NoticeKind } from './types.js';\n\n/**\n * Default lifespan of a notice when its owner never clears it explicitly.\n * Longer than the relay's checkpoint RPC timeout (600s) so a notice still\n * self-expires even if the host CLI is SIGKILLed before its `finally` runs.\n */\nconst DEFAULT_NOTICE_TTL_MS = 660_000;\n\ninterface NoticeEntry {\n ev: BoxNoticeEvent;\n boxId: string;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * In-memory per-box informational notices, broadcast over the same SSE\n * channel as confirmation prompts. Unlike {@link import('./prompts.js').PendingPrompts}\n * these are fire-and-forget: there is no awaited promise and no answer. A\n * notice marks a box as transiently busy (a checkpoint freezes it via\n * `docker commit`); the host wrapper renders a spinner so the user can\n * tell the box from stuck.\n *\n * Deliberately NOT gated by `AGENTBOX_PROMPT=off` (which `askPrompt`\n * honours): a notice is informational, not a consent gate, so suppressing\n * it would only hide useful feedback.\n */\nexport class BoxNotices {\n /** keyed by notice id. */\n private readonly entries = new Map<string, NoticeEntry>();\n\n constructor(private readonly subscribers: PromptSubscribers) {}\n\n /**\n * Register a notice for `boxId` and broadcast `notice-set`. At most one\n * notice per (box, kind) is kept — a fresh `set` for the same kind\n * replaces the previous one (and cancels its TTL timer so a stale timer\n * can't later fire a `notice-clear` racing the replacement). Returns the\n * generated notice id.\n */\n set(boxId: string, kind: NoticeKind, message: string, ttlMs?: number): string {\n for (const [id, entry] of this.entries) {\n if (entry.boxId === boxId && entry.ev.kind === kind) {\n clearTimeout(entry.timer);\n this.entries.delete(id);\n }\n }\n const ev: BoxNoticeEvent = { id: randomUUID(), kind, message };\n const ttl = typeof ttlMs === 'number' && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;\n const timer = setTimeout(() => {\n // Safety net: a notice whose owner died without clearing it self-expires.\n if (this.entries.delete(ev.id)) {\n this.subscribers.broadcast(boxId, 'notice-clear', { id: ev.id });\n }\n }, ttl);\n if (typeof timer.unref === 'function') timer.unref();\n this.entries.set(ev.id, { ev, boxId, timer });\n this.subscribers.broadcast(boxId, 'notice-set', ev);\n return ev.id;\n }\n\n /**\n * Clear a notice by id. Idempotent: returns false when no such notice\n * exists (already cleared / expired). Broadcasts `notice-clear` on a hit.\n */\n clear(id: string): boolean {\n const entry = this.entries.get(id);\n if (!entry) return false;\n clearTimeout(entry.timer);\n this.entries.delete(id);\n this.subscribers.broadcast(entry.boxId, 'notice-clear', { id });\n return true;\n }\n\n /** Snapshot of active notices for a box; replayed to a new SSE subscriber. */\n forBox(boxId: string): BoxNoticeEvent[] {\n const out: BoxNoticeEvent[] = [];\n for (const entry of this.entries.values()) {\n if (entry.boxId === boxId) out.push(entry.ev);\n }\n return out;\n }\n\n size(): number {\n return this.entries.size;\n }\n}\n","import { mkdir, rename, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n/**\n * A box status snapshot as received from the in-box daemon. The relay treats\n * it structurally (it has no dep on `@agentbox/ctl`); the rich type lives in\n * `@agentbox/ctl` (`BoxStatus`) and is what the host CLI parses back.\n */\nexport type BoxStatusSnapshot = Record<string, unknown>;\n\n/**\n * Mirrors `sanitizeMnemonic` in @agentbox/config — duplicated here so the relay\n * stays dep-free. Two source-of-truth files; the schema-drift-style guarantee\n * is that boxes only land in `<id>-<mnemonic>/` if both impls agree.\n */\nfunction sanitizeMnemonic(raw: string): string {\n return (\n raw\n .toLowerCase()\n .replace(/-/g, '_')\n .replace(/[^a-z0-9_]+/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_+|_+$/g, '')\n .slice(0, 32) || 'unnamed'\n );\n}\n\n/**\n * Mirrors `boxRunDirFor` / `boxDirSegment` in @agentbox/sandbox-docker — kept\n * in sync by hand. When `projectIndex` (`agentbox list`'s `N`) is set the\n * segment is `<id>-<n>-<mnemonic>` so directories sort cleanly within a\n * project; legacy (pre-feature) boxes register without it and fall back to\n * the original `<id>-<mnemonic>` shape.\n */\nfunction boxRunDirFor(boxId: string, name: string, projectIndex?: number): string {\n const mnemonic = sanitizeMnemonic(name);\n const segment =\n typeof projectIndex === 'number' && Number.isFinite(projectIndex) && projectIndex > 0\n ? `${boxId}-${String(projectIndex)}-${mnemonic}`\n : `${boxId}-${mnemonic}`;\n return join(homedir(), '.agentbox', 'boxes', segment);\n}\n\nfunction boxStatusPathFor(boxId: string, name: string, projectIndex?: number): string {\n return join(boxRunDirFor(boxId, name, projectIndex), 'status.json');\n}\n\n/**\n * Structural guard: a valid box-status payload is an object with `schema === 1`\n * and a non-empty `boxId` string. The relay persists it verbatim; the host\n * reader does the strict typing.\n */\nexport function isValidBoxStatus(payload: unknown): payload is BoxStatusSnapshot {\n if (typeof payload !== 'object' || payload === null) return false;\n const o = payload as Record<string, unknown>;\n return o.schema === 1 && typeof o.boxId === 'string' && o.boxId.length > 0;\n}\n\n/**\n * In-memory latest-status map plus a durable per-box file at\n * `~/.agentbox/boxes/<id>/status.json`. The relay is a single process so it is\n * the single writer; the atomic tmp+rename means the host CLI never reads a\n * torn file. The on-disk copy is what makes status survive box pause/stop,\n * relay restart, and host reboot.\n */\nexport class BoxStatusStore {\n private readonly map = new Map<string, BoxStatusSnapshot>();\n\n get(boxId: string): BoxStatusSnapshot | undefined {\n return this.map.get(boxId);\n }\n\n /**\n * Update the in-memory entry and best-effort persist it to disk. `name` is\n * the box's user-facing name (from the registry); `projectIndex` is the\n * 1-based per-project `N`. Together they form the on-disk dir\n * `~/.agentbox/boxes/<id>-<n>-<mnemonic>/status.json` (or\n * `<id>-<mnemonic>/` if N is absent — legacy boxes).\n */\n async set(\n boxId: string,\n name: string,\n projectIndex: number | undefined,\n status: BoxStatusSnapshot,\n ): Promise<void> {\n this.map.set(boxId, status);\n const target = boxStatusPathFor(boxId, name, projectIndex);\n const tmp = `${target}.${String(process.pid)}.tmp`;\n try {\n await mkdir(boxRunDirFor(boxId, name, projectIndex), { recursive: true });\n await writeFile(tmp, JSON.stringify(status), 'utf8');\n await rename(tmp, target);\n } catch {\n await rm(tmp, { force: true }).catch(() => {});\n }\n }\n\n /** Drop the in-memory entry (the on-disk file is wiped with the box dir). */\n delete(boxId: string): void {\n this.map.delete(boxId);\n }\n}\n","import { spawn } from 'node:child_process';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { BoxNotices } from './notices.js';\nimport { askPrompt, isPromptAnswerBody, PendingPrompts, PromptSubscribers } from './prompts.js';\nimport { BoxRegistry, EventBuffer } from './registry.js';\nimport { BoxStatusStore, isValidBoxStatus } from './status-store.js';\nimport type {\n BoxRegistration,\n BoxWorktree,\n BrowserOpenRpcParams,\n CheckpointRpcParams,\n ClearNoticeBody,\n CpRpcParams,\n DownloadKind,\n DownloadRpcParams,\n GitRpcParams,\n GitRpcResult,\n PostEventBody,\n PostRpcBody,\n PromptAnswerBody,\n RegisterBoxBody,\n RelayEvent,\n SetNoticeBody,\n} from './types.js';\n\nexport interface RelayServerOptions {\n port: number;\n /** Bind address; defaults to '0.0.0.0' so the container reachable from other containers on the same docker network. */\n host?: string;\n logger?: (line: string) => void;\n}\n\nexport interface RelayServerHandle {\n server: Server;\n registry: BoxRegistry;\n events: EventBuffer;\n statusStore: BoxStatusStore;\n prompts: PendingPrompts;\n subscribers: PromptSubscribers;\n notices: BoxNotices;\n url: string;\n close: () => Promise<void>;\n}\n\n/** Event type whose payload is a durable BoxStatus snapshot (persisted, not ringed). */\nconst BOX_STATUS_EVENT = 'box-status';\n\nconst MAX_BODY_BYTES = 1024 * 1024; // 1 MiB hard cap; relay is for control-plane traffic, not payloads.\nconst GIT_RPC_TIMEOUT_MS = 120_000; // git push/pull can be slow on big repos.\nconst CHECKPOINT_RPC_TIMEOUT_MS = 600_000; // capturing node_modules/build trees can be slow.\nconst DOWNLOAD_RPC_TIMEOUT_MS = 600_000; // claude/workspace pulls over rsync can take minutes.\nconst CP_RPC_TIMEOUT_MS = 300_000; // single-file/dir cp; tar pipe through docker exec.\nconst BROWSER_OPEN_RPC_TIMEOUT_MS = 15_000; // `open` hands off to the browser and returns at once.\nconst SSE_HEARTBEAT_MS = 15_000; // every 15s; wrapper reconnects if it sees no traffic for ~30s.\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: unknown,\n contentType: string = 'application/json',\n): void {\n const text = body == null ? '' : typeof body === 'string' ? body : JSON.stringify(body);\n res.statusCode = status;\n if (text.length > 0) {\n res.setHeader('Content-Type', contentType);\n res.setHeader('Content-Length', Buffer.byteLength(text).toString());\n res.end(text);\n } else {\n res.end();\n }\n}\n\nasync function readJsonBody<T>(req: IncomingMessage): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n let total = 0;\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => {\n total += chunk.length;\n if (total > MAX_BODY_BYTES) {\n reject(new Error('request body too large'));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => {\n const text = Buffer.concat(chunks).toString('utf8');\n if (text.length === 0) {\n resolve({} as T);\n return;\n }\n try {\n resolve(JSON.parse(text) as T);\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n });\n req.on('error', reject);\n });\n}\n\nfunction bearerToken(req: IncomingMessage): string {\n const raw = req.headers.authorization;\n if (typeof raw !== 'string') return '';\n const m = /^Bearer\\s+(.+)$/i.exec(raw.trim());\n return m ? m[1]!.trim() : '';\n}\n\nfunction isLoopbackAddress(addr: string | undefined): boolean {\n if (!addr) return false;\n return (\n addr === '127.0.0.1' ||\n addr === '::1' ||\n addr === '::ffff:127.0.0.1' ||\n addr.startsWith('127.')\n );\n}\n\n/**\n * Build the relay HTTP server. Routes:\n * POST /events — bearer auth (box token), appends to ring buffer.\n * POST /rpc — bearer auth; dispatches git.push/fetch, cp.*, download.*, checkpoint.create on the host.\n * POST /admin/register-box — loopback only.\n * POST /admin/forget-box — loopback only.\n * GET /admin/box-status — loopback only; query `box`; latest snapshot.\n * GET /admin/events — loopback only; query `box`, `since`.\n * GET /admin/registry — loopback only; list registered boxes (token redacted).\n * GET /admin/prompts/stream — loopback only; SSE; pushes prompt-ask/prompt-resolved/notice-set/notice-clear/ping events.\n * POST /admin/prompts/answer — loopback only; resolves a pending prompt by id.\n * POST /admin/notices/set — loopback only; sets an informational box notice (returns {id}).\n * POST /admin/notices/clear — loopback only; clears a box notice by id.\n * GET /healthz — liveness probe (no auth).\n */\nexport function createRelayServer(opts: RelayServerOptions): RelayServerHandle {\n const log = opts.logger ?? (() => {});\n const registry = new BoxRegistry();\n const events = new EventBuffer();\n const statusStore = new BoxStatusStore();\n const prompts = new PendingPrompts();\n const subscribers = new PromptSubscribers();\n const notices = new BoxNotices(subscribers);\n const host = opts.host ?? '0.0.0.0';\n\n const server = createServer((req, res) => {\n handle(req, res).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err);\n log(`relay: handler error: ${msg}`);\n if (!res.headersSent) send(res, 500, { error: 'internal error' });\n else res.end();\n });\n });\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'relay'}`);\n const route = `${req.method ?? 'GET'} ${url.pathname}`;\n\n if (route === 'GET /healthz') {\n send(res, 200, { ok: true, boxes: registry.size(), events: events.size() });\n return;\n }\n\n // Admin endpoints are reachable from loopback only. The relay binds to\n // 0.0.0.0 so containers can reach /events and /rpc via host.docker.internal,\n // but admin operations (register-box, forget-box, list events, etc.) are\n // for the host CLI and must not be exposed to boxes.\n if (url.pathname.startsWith('/admin/')) {\n if (!isLoopbackAddress(req.socket.remoteAddress)) {\n send(res, 403, { error: 'admin endpoints are loopback-only' });\n return;\n }\n }\n\n if (route === 'POST /events') {\n const reg = authBox(req, res, registry);\n if (!reg) return;\n const body = await readJsonBody<PostEventBody>(req);\n if (!body || typeof body.type !== 'string' || body.type.length === 0) {\n send(res, 400, { error: 'missing \"type\" string' });\n return;\n }\n // box-status is durable state, not an event: persist the latest snapshot\n // per box and skip the ring buffer (a 15s heartbeat per box would\n // otherwise evict the useful git/service events from the 1000-cap ring).\n if (body.type === BOX_STATUS_EVENT) {\n if (!isValidBoxStatus(body.payload)) {\n send(res, 400, { error: 'invalid box-status payload' });\n return;\n }\n await statusStore.set(reg.boxId, reg.name, reg.projectIndex, body.payload);\n log(`box-status box=${reg.boxId}`);\n send(res, 202, { ok: true });\n return;\n }\n const ev = events.append({\n boxId: reg.boxId,\n type: body.type,\n ts: typeof body.ts === 'string' ? body.ts : undefined,\n payload: body.payload,\n });\n log(`event ${String(ev.id)} box=${reg.boxId} type=${body.type}`);\n send(res, 202, { id: ev.id });\n return;\n }\n\n if (route === 'POST /rpc') {\n const reg = authBox(req, res, registry);\n if (!reg) return;\n const body = await readJsonBody<PostRpcBody>(req);\n if (!body || typeof body.method !== 'string' || body.method.length === 0) {\n send(res, 400, { error: 'missing \"method\" string' });\n return;\n }\n log(`rpc box=${reg.boxId} method=${body.method}`);\n if (body.method === 'git.push' || body.method === 'git.fetch') {\n // Only `push` mutates the user's remote; fetch is read-only and noisy.\n if (body.method === 'git.push') {\n const params = body.params as GitRpcParams | undefined;\n const verdict = await askPrompt(prompts, subscribers, reg.boxId, {\n kind: 'confirm',\n message: `Allow git push from box ${reg.name}?`,\n detail: `${params?.remote ?? 'origin'} ${(params?.args ?? []).join(' ')}`.trim(),\n defaultAnswer: 'n',\n context: {\n command: 'git push',\n cwd: params?.path,\n argv: params?.args,\n },\n });\n if (verdict.answer !== 'y') {\n send(res, 500, { exitCode: 10, stdout: '', stderr: 'denied by user\\n' });\n return;\n }\n }\n const result = await handleGitRpc(reg, body.method, body.params as GitRpcParams | undefined);\n const status = result.exitCode === 0 ? 200 : 500;\n send(res, status, result);\n return;\n }\n if (body.method === 'cp.toHost' || body.method === 'cp.fromHost') {\n const params = body.params as CpRpcParams | undefined;\n if (!params || typeof params.boxPath !== 'string' || typeof params.hostPath !== 'string') {\n send(res, 400, { error: 'cp.* requires {boxPath, hostPath} strings' });\n return;\n }\n const direction = body.method === 'cp.toHost' ? 'box -> host' : 'host -> box';\n const verdict = await askPrompt(prompts, subscribers, reg.boxId, {\n kind: 'confirm',\n message: `Allow cp (${direction}) on ${reg.name}?`,\n detail:\n body.method === 'cp.toHost'\n ? `${params.boxPath} -> ${params.hostPath}`\n : `${params.hostPath} -> ${params.boxPath}`,\n defaultAnswer: 'n',\n context: {\n command: body.method,\n argv: [params.boxPath, params.hostPath],\n },\n });\n if (verdict.answer !== 'y') {\n send(res, 500, { exitCode: 10, stdout: '', stderr: 'denied by user\\n' });\n return;\n }\n const result = await handleCpRpc(reg, body.method, params);\n const status = result.exitCode === 0 ? 200 : 500;\n send(res, status, result);\n return;\n }\n if (\n body.method === 'download.workspace' ||\n body.method === 'download.env' ||\n body.method === 'download.config' ||\n body.method === 'download.claude'\n ) {\n const params = body.params as DownloadRpcParams | undefined;\n const kind = (body.method.split('.')[1] ?? 'workspace') as DownloadKind;\n const verdict = await askPrompt(prompts, subscribers, reg.boxId, {\n kind: 'confirm',\n message: `Allow download (${kind}) from ${reg.name}?`,\n detail: params?.hostPath ?? '(default host location)',\n defaultAnswer: 'n',\n context: {\n command: body.method,\n argv: params?.hostPath ? [params.hostPath] : [],\n },\n });\n if (verdict.answer !== 'y') {\n send(res, 500, { exitCode: 10, stdout: '', stderr: 'denied by user\\n' });\n return;\n }\n const result = await handleDownloadRpc(reg, kind);\n const status = result.exitCode === 0 ? 200 : 500;\n send(res, status, result);\n return;\n }\n if (body.method === 'checkpoint.create') {\n const result = await handleCheckpointRpc(\n reg,\n body.params as CheckpointRpcParams | undefined,\n );\n const status = result.exitCode === 0 ? 200 : 500;\n send(res, status, result);\n return;\n }\n if (body.method === 'browser.open') {\n const params = body.params as BrowserOpenRpcParams | undefined;\n const url = typeof params?.url === 'string' ? params.url.trim() : '';\n if (!isOpenableUrl(url)) {\n // Un-gated by design, but the scheme guard keeps a box from\n // handing the host's `open` a file path or app instead of a URL.\n send(res, 400, {\n exitCode: 64,\n stdout: '',\n stderr: 'browser.open: only http/https URLs are allowed\\n',\n });\n return;\n }\n events.append({ boxId: reg.boxId, type: 'browser-open', payload: { url } });\n const result = await runHostCommand(['open', url], BROWSER_OPEN_RPC_TIMEOUT_MS);\n const status = result.exitCode === 0 ? 200 : 500;\n send(res, status, result);\n return;\n }\n events.append({\n boxId: reg.boxId,\n type: 'rpc-unknown',\n payload: { method: body.method },\n });\n send(res, 501, { error: 'rpc method not implemented', method: body.method });\n return;\n }\n\n if (route === 'POST /admin/register-box') {\n const body = await readJsonBody<RegisterBoxBody>(req);\n if (\n !body ||\n typeof body.boxId !== 'string' ||\n typeof body.token !== 'string' ||\n typeof body.name !== 'string' ||\n body.boxId.length === 0 ||\n body.token.length === 0\n ) {\n send(res, 400, { error: 'expected {boxId, token, name}' });\n return;\n }\n const worktrees = sanitizeWorktrees(body.worktrees);\n // Only accept a finite positive integer; everything else (including the\n // common `undefined` from legacy boxes) drops to `undefined` and the\n // status-store falls back to the `<id>-<mnemonic>` segment shape.\n const projectIndex =\n typeof body.projectIndex === 'number' &&\n Number.isFinite(body.projectIndex) &&\n body.projectIndex > 0\n ? Math.trunc(body.projectIndex)\n : undefined;\n const reg: BoxRegistration = {\n boxId: body.boxId,\n token: body.token,\n name: body.name,\n registeredAt: new Date().toISOString(),\n containerName:\n typeof body.containerName === 'string' && body.containerName.length > 0\n ? body.containerName\n : undefined,\n createdAt:\n typeof body.createdAt === 'string' && body.createdAt.length > 0\n ? body.createdAt\n : undefined,\n projectIndex,\n worktrees,\n };\n registry.register(reg);\n log(\n `registered box ${reg.boxId} (${reg.name})` +\n (worktrees && worktrees.length > 0 ? ` with ${String(worktrees.length)} worktree(s)` : ''),\n );\n send(res, 204, null);\n return;\n }\n\n if (route === 'POST /admin/forget-box') {\n const body = await readJsonBody<{ boxId?: string }>(req);\n if (!body || typeof body.boxId !== 'string' || body.boxId.length === 0) {\n send(res, 400, { error: 'expected {boxId}' });\n return;\n }\n const existed = registry.forget(body.boxId);\n statusStore.delete(body.boxId);\n log(`forgot box ${body.boxId} (existed=${String(existed)})`);\n send(res, 204, null);\n return;\n }\n\n if (route === 'GET /admin/box-status') {\n const box = url.searchParams.get('box') ?? '';\n const status = box ? statusStore.get(box) : undefined;\n if (!status) {\n send(res, 404, { error: 'no status for box', box });\n return;\n }\n send(res, 200, status);\n return;\n }\n\n if (route === 'GET /admin/events') {\n const since = Number.parseInt(url.searchParams.get('since') ?? '0', 10) || 0;\n const box = url.searchParams.get('box') ?? undefined;\n const list = events.since(since, box ?? undefined);\n send(res, 200, { events: list });\n return;\n }\n\n if (route === 'GET /admin/registry') {\n // Redact tokens; callers on the admin path don't need them and we don't\n // want them showing up in logs if someone curls this.\n const redacted = registry.list().map((r) => ({\n boxId: r.boxId,\n name: r.name,\n registeredAt: r.registeredAt,\n containerName: r.containerName,\n createdAt: r.createdAt,\n projectIndex: r.projectIndex,\n worktrees: r.worktrees ?? [],\n }));\n send(res, 200, { boxes: redacted });\n return;\n }\n\n if (route === 'GET /admin/prompts/stream') {\n // Per-box SSE channel. The wrapper (apps/cli/src/wrapped-pty) subscribes\n // and stays connected; we push prompt-ask events on broadcast and a\n // periodic ping so the wrapper can detect a dead socket without traffic.\n // `boxId=` is required so a host with multiple boxes only sees its own\n // box's prompts (the wrapper attaches per-box anyway).\n const boxId = url.searchParams.get('boxId') ?? '';\n if (boxId.length === 0) {\n send(res, 400, { error: 'missing boxId query param' });\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n // Helps with proxies (e.g. nginx) that would otherwise buffer the\n // chunked response. The relay binds to loopback so this is belt-and-\n // suspenders, but the cost is one extra header.\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n res.write(': connected\\n\\n');\n subscribers.add(boxId, res);\n // Flush any prompts that arrived while no wrapper was attached — per\n // the design we block indefinitely on the in-box RPC, so a backlog can\n // build up between detach and reattach.\n for (const ev of prompts.forBox(boxId)) {\n res.write(`event: prompt-ask\\ndata: ${JSON.stringify(ev)}\\n\\n`);\n }\n // Then any active notices, so a wrapper attaching mid-checkpoint still\n // sees the in-progress warning (prompts first — they outrank notices).\n for (const ev of notices.forBox(boxId)) {\n res.write(`event: notice-set\\ndata: ${JSON.stringify(ev)}\\n\\n`);\n }\n const heartbeat = setInterval(() => {\n try {\n res.write(`event: ping\\ndata: {\"ts\":\"${new Date().toISOString()}\"}\\n\\n`);\n } catch {\n /* dead socket; close handler below removes */\n }\n }, SSE_HEARTBEAT_MS);\n if (typeof heartbeat.unref === 'function') heartbeat.unref();\n res.on('close', () => {\n clearInterval(heartbeat);\n subscribers.remove(boxId, res);\n });\n return;\n }\n\n if (route === 'POST /admin/prompts/answer') {\n const body = await readJsonBody<PromptAnswerBody>(req);\n if (!isPromptAnswerBody(body)) {\n send(res, 400, { error: 'expected {id, answer:\"y\"|\"n\", cancelled?}' });\n return;\n }\n // Find which box this id belongs to before resolving, so we can target\n // the prompt-resolved broadcast (other wrappers on the same box clear\n // their stale footer).\n const targetBox = prompts.boxFor(body.id);\n const hit = prompts.resolve(body.id, body.answer, body.cancelled);\n if (!hit) {\n // Already answered (idempotent) or never existed.\n send(res, 404, { error: 'no pending prompt with that id' });\n return;\n }\n if (targetBox) {\n subscribers.broadcast(targetBox, 'prompt-resolved', { id: body.id });\n }\n send(res, 204, null);\n return;\n }\n\n if (route === 'POST /admin/notices/set') {\n const body = await readJsonBody<SetNoticeBody>(req);\n if (\n !body ||\n typeof body.boxId !== 'string' ||\n body.boxId.length === 0 ||\n typeof body.kind !== 'string' ||\n body.kind.length === 0 ||\n typeof body.message !== 'string' ||\n body.message.length === 0\n ) {\n send(res, 400, { error: 'expected {boxId, kind, message}' });\n return;\n }\n const ttlMs =\n typeof body.ttlMs === 'number' && Number.isFinite(body.ttlMs) && body.ttlMs > 0\n ? body.ttlMs\n : undefined;\n const id = notices.set(body.boxId, body.kind, body.message, ttlMs);\n log(`notice-set box=${body.boxId} kind=${body.kind} id=${id}`);\n send(res, 200, { id });\n return;\n }\n\n if (route === 'POST /admin/notices/clear') {\n const body = await readJsonBody<ClearNoticeBody>(req);\n if (!body || typeof body.id !== 'string' || body.id.length === 0) {\n send(res, 400, { error: 'expected {boxId, id}' });\n return;\n }\n notices.clear(body.id);\n log(`notice-clear id=${body.id}`);\n send(res, 204, null);\n return;\n }\n\n send(res, 404, { error: 'not found', route });\n }\n\n function authBox(\n req: IncomingMessage,\n res: ServerResponse,\n reg: BoxRegistry,\n ): BoxRegistration | null {\n const token = bearerToken(req);\n if (token.length === 0) {\n send(res, 401, { error: 'missing bearer token' });\n return null;\n }\n const match = reg.authenticate(token);\n if (!match) {\n send(res, 401, { error: 'unknown box token' });\n return null;\n }\n return match;\n }\n\n return {\n server,\n registry,\n events,\n statusStore,\n prompts,\n subscribers,\n notices,\n url: `http://${host}:${String(opts.port)}`,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n }),\n };\n}\n\n\nfunction sanitizeWorktrees(input: BoxWorktree[] | undefined): BoxWorktree[] | undefined {\n if (!Array.isArray(input)) return undefined;\n const out: BoxWorktree[] = [];\n for (const w of input) {\n if (\n w &&\n typeof w.containerPath === 'string' &&\n typeof w.hostMainRepo === 'string' &&\n typeof w.branch === 'string'\n ) {\n out.push({\n containerPath: w.containerPath,\n hostMainRepo: w.hostMainRepo,\n branch: w.branch,\n });\n }\n }\n return out;\n}\n\n/**\n * Resolve `params.path` (a path inside the container) to the registered\n * worktree whose hostMainRepo + branch the relay should run git in.\n * `/workspace` maps to the root repo; `/workspace/<sub>` maps to the nested\n * repo when one is registered (longest prefix wins).\n */\nfunction resolveWorktree(reg: BoxRegistration, containerPath: string): BoxWorktree | null {\n const trees = reg.worktrees ?? [];\n if (trees.length === 0) return null;\n const exact = trees.find((w) => w.containerPath === containerPath);\n if (exact) return exact;\n const prefixMatches = trees\n .filter((w) => containerPath === w.containerPath || containerPath.startsWith(w.containerPath + '/'))\n .sort((a, b) => b.containerPath.length - a.containerPath.length);\n return prefixMatches[0] ?? trees.find((w) => w.containerPath === '/workspace') ?? null;\n}\n\n/**\n * git.push / git.fetch: run `git -C <hostMainRepo> <op> <remote> <branch>\n * [args]` on the host with the user's creds. The in-container worktree's\n * working tree isn't on the host, so we operate on the shared `.git/` from\n * the main repo dir — refs already point at the in-container commits\n * (committed there against the bind-mounted .git).\n *\n * git.pull is intentionally NOT handled here: a pull merges into the\n * working tree, which lives inside the container. The in-box\n * `agentbox-ctl git pull` calls git.fetch via RPC, then runs a local merge.\n */\nasync function handleGitRpc(\n reg: BoxRegistration,\n method: 'git.push' | 'git.fetch',\n params: GitRpcParams | undefined,\n): Promise<GitRpcResult> {\n const containerPath = params?.path ?? '/workspace';\n const worktree = resolveWorktree(reg, containerPath);\n if (!worktree) {\n return {\n exitCode: 64,\n stdout: '',\n stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`,\n };\n }\n const op = method === 'git.push' ? 'push' : 'fetch';\n const remote = params?.remote ?? 'origin';\n const argv = ['git', '-C', worktree.hostMainRepo, op, remote, worktree.branch];\n if (Array.isArray(params?.args)) {\n for (const a of params.args) {\n if (typeof a === 'string') argv.push(a);\n }\n }\n return runHostCommand(argv);\n}\n\n/**\n * cp.toHost / cp.fromHost: copy a file/dir between box and host. Shells\n * out to the installed agentbox CLI's `cp` subcommand — that command\n * already knows how to handle the docker exec tar pipe + chown + auto-\n * unpause; duplicating that here would drift. `AGENTBOX_CLI_ENTRY` is set\n * by `ensureRelay` when it spawns this process.\n *\n * Caller (the /rpc route) already gated this with askPrompt and rejected\n * non-'y' answers; we never reach this code without consent.\n */\nasync function handleCpRpc(\n reg: BoxRegistration,\n method: 'cp.toHost' | 'cp.fromHost',\n params: CpRpcParams,\n): Promise<GitRpcResult> {\n const entry = process.env.AGENTBOX_CLI_ENTRY;\n if (!entry) {\n return {\n exitCode: 64,\n stdout: '',\n stderr: 'relay: AGENTBOX_CLI_ENTRY not set; cannot run cp host-side',\n };\n }\n // `agentbox cp` is positional: <src> [dst]. Direction is encoded by which\n // arg carries the `<boxName>:` prefix.\n const boxRef = `${reg.name}:${params.boxPath}`;\n const argv =\n method === 'cp.toHost'\n ? [process.execPath, entry, 'cp', boxRef, params.hostPath]\n : [process.execPath, entry, 'cp', params.hostPath, boxRef];\n return runHostCommand(argv, CP_RPC_TIMEOUT_MS);\n}\n\n/**\n * download.{workspace,env,config,claude}: ask the installed agentbox CLI\n * to pull box contents to the host. Same decoupling rationale as cp — the\n * CLI owns rsync exclude lists, gitignore handling, claude registry\n * merging. The relay passes `-y` so the host CLI doesn't try to prompt\n * (we already did, via the host wrapper, before reaching this handler).\n */\nasync function handleDownloadRpc(\n reg: BoxRegistration,\n kind: DownloadKind,\n): Promise<GitRpcResult> {\n // params.hostPath is reserved in the wire shape; the v1 relay ignores it\n // and lets the host CLI use its defaults (box.workspacePath or ~/.claude).\n const entry = process.env.AGENTBOX_CLI_ENTRY;\n if (!entry) {\n return {\n exitCode: 64,\n stdout: '',\n stderr: 'relay: AGENTBOX_CLI_ENTRY not set; cannot run download host-side',\n };\n }\n const argv = [process.execPath, entry, 'download'];\n // `workspace` is the default download (no subcommand); the other three\n // are subcommands of `download`.\n if (kind !== 'workspace') argv.push(kind);\n argv.push(reg.name, '-y');\n return runHostCommand(argv, DOWNLOAD_RPC_TIMEOUT_MS);\n}\n\n/**\n * Capture a checkpoint host-side by shelling out to the installed agentbox\n * CLI (same decoupling philosophy as `handleGitRpc` spawning `git`). The\n * relay only knows the box id; the CLI resolves the BoxRecord (project root,\n * checkpoint config, snapshot storage) from it. `AGENTBOX_CLI_ENTRY` is set\n * by `ensureRelay` when it spawns this process.\n */\nasync function handleCheckpointRpc(\n reg: BoxRegistration,\n params: CheckpointRpcParams | undefined,\n): Promise<GitRpcResult> {\n const entry = process.env.AGENTBOX_CLI_ENTRY;\n if (!entry) {\n return {\n exitCode: 64,\n stdout: '',\n stderr: 'relay: AGENTBOX_CLI_ENTRY not set; cannot run checkpoint host-side',\n };\n }\n const argv = [process.execPath, entry, 'checkpoint', 'create', reg.boxId];\n if (params?.name) argv.push('--name', params.name);\n if (params?.merged === true) argv.push('--merged');\n if (params?.setDefault === true) argv.push('--set-default');\n if (params?.replace === true) argv.push('--replace');\n return runHostCommand(argv, CHECKPOINT_RPC_TIMEOUT_MS);\n}\n\n/**\n * Guard for the `browser.open` RPC: only absolute http/https URLs may be\n * handed to the host's `open`. Rejecting every other scheme (`file:`,\n * `javascript:`, bare paths) keeps an in-box agent from opening host files\n * or apps under the guise of \"opening a link\".\n */\nexport function isOpenableUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === 'http:' || url.protocol === 'https:';\n}\n\nfunction runHostCommand(\n argv: string[],\n timeoutMs: number = GIT_RPC_TIMEOUT_MS,\n): Promise<GitRpcResult> {\n return new Promise<GitRpcResult>((resolve) => {\n const [cmd, ...rest] = argv;\n if (!cmd) {\n resolve({ exitCode: 64, stdout: '', stderr: 'empty command' });\n return;\n }\n const child = spawn(cmd, rest, {\n env: process.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let stdout = '';\n let stderr = '';\n let settled = false;\n const finish = (exitCode: number): void => {\n if (settled) return;\n settled = true;\n resolve({ exitCode, stdout, stderr });\n };\n const timer = setTimeout(() => {\n child.kill('SIGTERM');\n stderr += `\\nrelay: command timed out after ${String(timeoutMs)}ms\\n`;\n finish(124);\n }, timeoutMs);\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout += chunk.toString('utf8');\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf8');\n });\n child.on('error', (err) => {\n clearTimeout(timer);\n stderr += String(err.message ?? err);\n finish(127);\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n finish(code ?? -1);\n });\n });\n}\n\nexport async function startRelayServer(opts: RelayServerOptions): Promise<RelayServerHandle> {\n const handle = createRelayServer(opts);\n await new Promise<void>((resolve, reject) => {\n handle.server.once('error', reject);\n handle.server.listen(opts.port, opts.host ?? '0.0.0.0', () => {\n handle.server.removeListener('error', reject);\n resolve();\n });\n });\n return handle;\n}\n\nexport type { BoxRegistration, RelayEvent };\n","import { spawn } from 'node:child_process';\nimport { readFile } from 'node:fs/promises';\nimport {\n BUILT_IN_DEFAULTS,\n GLOBAL_CONFIG_FILE,\n parseUserConfig,\n type UserConfig,\n} from '@agentbox/config';\nimport type { BoxRegistry, EventBuffer } from './registry.js';\nimport type { BoxStatusStore } from './status-store.js';\n\nexport interface AutopauseConfig {\n enabled: boolean;\n maxRunningBoxes: number;\n idleMinutes: number;\n}\n\nexport type ContainerState = 'running' | 'paused' | 'stopped' | 'missing';\nexport type ClaudeState = 'working' | 'idle' | 'waiting' | 'unknown';\n\n/** One box's runtime facts the pure selector reasons about. No I/O, no clock. */\nexport interface BoxScanEntry {\n boxId: string;\n containerName: string;\n /** docker inspect status === 'running'. */\n running: boolean;\n /** Latest reported claude activity, or null when no snapshot exists. */\n claudeState: ClaudeState | null;\n /** ms the box has been idle (now - claude.updatedAt) when idle; else null. */\n idleMs: number | null;\n /** Box creation time as epoch ms; 0 when unknown/unparseable. */\n createdAt: number;\n}\n\n/**\n * Pure selection: given each running box's idle facts and the config, return\n * the boxIds to pause, in pause order. Pauses only enough to bring the running\n * count back to `maxRunningBoxes`, picking provably-idle boxes longest-idle\n * first (tie-break: oldest box, then boxId for determinism).\n */\nexport function selectBoxesToPause(entries: BoxScanEntry[], cfg: AutopauseConfig): string[] {\n if (!cfg.enabled) return [];\n const runningCount = entries.reduce((n, e) => (e.running ? n + 1 : n), 0);\n const excess = runningCount - cfg.maxRunningBoxes;\n if (excess <= 0) return [];\n\n const idleThresholdMs = cfg.idleMinutes * 60_000;\n const candidates = entries.filter(\n (e) => e.running && e.claudeState === 'idle' && e.idleMs != null && e.idleMs >= idleThresholdMs,\n );\n candidates.sort(\n (a, b) =>\n (b.idleMs as number) - (a.idleMs as number) ||\n a.createdAt - b.createdAt ||\n (a.boxId < b.boxId ? -1 : a.boxId > b.boxId ? 1 : 0),\n );\n return candidates.slice(0, excess).map((e) => e.boxId);\n}\n\n/**\n * Global+built-in autopause config. The relay is host-wide (not project\n * scoped), so it deliberately ignores the project/workspace layers that\n * `loadEffectiveConfig` would apply. Re-read every tick so\n * `agentbox config set --global autopause.*` takes effect without a relay\n * restart. A missing or malformed global file falls back to built-in defaults.\n */\nexport async function loadAutopauseConfig(): Promise<AutopauseConfig> {\n const d = BUILT_IN_DEFAULTS.autopause;\n let global: Partial<UserConfig> = {};\n try {\n global = parseUserConfig(await readFile(GLOBAL_CONFIG_FILE, 'utf8'), GLOBAL_CONFIG_FILE);\n } catch {\n // ENOENT (no global config yet) or a parse error -> built-in defaults.\n }\n const a = global.autopause ?? {};\n return {\n enabled: a.enabled ?? d.enabled,\n maxRunningBoxes: a.maxRunningBoxes ?? d.maxRunningBoxes,\n idleMinutes: a.idleMinutes ?? d.idleMinutes,\n };\n}\n\nexport interface AutopauseLoopDeps {\n registry: BoxRegistry;\n statusStore: BoxStatusStore;\n events: EventBuffer;\n log: (line: string) => void;\n /** Injectable for tests; defaults to the global-config loader. */\n loadConfig?: () => Promise<AutopauseConfig>;\n /** Injectable for tests; defaults to `docker inspect`. */\n inspectStatus?: (containerName: string) => Promise<ContainerState>;\n /** Injectable for tests; defaults to `docker pause`. */\n pause?: (containerName: string) => Promise<void>;\n intervalMs?: number;\n}\n\nexport interface AutopauseLoopHandle {\n /** Stop scheduling and await any in-flight tick. */\n stop: () => Promise<void>;\n}\n\nconst DEFAULT_INTERVAL_MS = 60_000;\n\nexport function startAutopauseLoop(deps: AutopauseLoopDeps): AutopauseLoopHandle {\n const loadConfig = deps.loadConfig ?? loadAutopauseConfig;\n const inspectStatus = deps.inspectStatus ?? inspectContainerState;\n const pause = deps.pause ?? pauseContainer;\n const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;\n const { registry, statusStore, events, log } = deps;\n\n let ticking = false;\n let stopped = false;\n let inFlight: Promise<void> = Promise.resolve();\n\n async function tick(): Promise<void> {\n // A slow tick (many `docker inspect`s) must not overlap the next interval.\n if (ticking) return;\n ticking = true;\n try {\n const cfg = await loadConfig();\n if (!cfg.enabled) return;\n\n const entries: BoxScanEntry[] = [];\n for (const reg of registry.list()) {\n if (!reg.containerName) continue; // pre-feature box; can't pause it\n const state = await inspectStatus(reg.containerName);\n const claude = readClaude(statusStore.get(reg.boxId));\n const idleMs =\n claude.state === 'idle' && claude.updatedAt ? msSince(claude.updatedAt) : null;\n entries.push({\n boxId: reg.boxId,\n containerName: reg.containerName,\n running: state === 'running',\n claudeState: claude.state,\n idleMs,\n createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0,\n });\n }\n\n const toPause = selectBoxesToPause(entries, cfg);\n if (toPause.length === 0) return;\n\n const byId = new Map(entries.map((e) => [e.boxId, e]));\n const runningBefore = entries.reduce((n, e) => (e.running ? n + 1 : n), 0);\n for (const boxId of toPause) {\n const e = byId.get(boxId);\n if (!e) continue;\n try {\n await pause(e.containerName);\n const mins = e.idleMs != null ? Math.round(e.idleMs / 60_000) : null;\n events.append({\n boxId,\n type: 'autopause',\n payload: {\n containerName: e.containerName,\n action: 'paused',\n idleMs: e.idleMs,\n runningBefore,\n max: cfg.maxRunningBoxes,\n },\n });\n log(\n `autopause: paused box ${boxId} (${e.containerName})` +\n (mins != null ? ` after ~${String(mins)}m idle` : '') +\n `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`,\n );\n } catch (err) {\n // docker failure is non-fatal: log, record, keep going.\n const msg = err instanceof Error ? err.message : String(err);\n log(`autopause: docker pause ${e.containerName} failed: ${msg}`);\n events.append({\n boxId,\n type: 'autopause',\n payload: { containerName: e.containerName, action: 'pause-failed', error: msg },\n });\n }\n }\n } catch (err) {\n // The loop must never crash the relay or stop scheduling.\n const msg = err instanceof Error ? err.message : String(err);\n log(`autopause: tick error: ${msg}`);\n } finally {\n ticking = false;\n }\n }\n\n const timer = setInterval(() => {\n if (stopped) return;\n inFlight = tick();\n }, intervalMs);\n // The HTTP server keeps the process alive; the timer shouldn't on its own.\n timer.unref();\n\n return {\n stop: async () => {\n stopped = true;\n clearInterval(timer);\n await inFlight.catch(() => {});\n },\n };\n}\n\ninterface ClaudeSnap {\n state: ClaudeState | null;\n updatedAt: string | null;\n}\n\nfunction readClaude(snap: Record<string, unknown> | undefined): ClaudeSnap {\n const c = snap && typeof snap === 'object' ? (snap as { claude?: unknown }).claude : undefined;\n if (!c || typeof c !== 'object') return { state: null, updatedAt: null };\n const o = c as Record<string, unknown>;\n const state =\n o.state === 'working' || o.state === 'idle' || o.state === 'waiting' || o.state === 'unknown'\n ? o.state\n : null;\n return { state, updatedAt: typeof o.updatedAt === 'string' ? o.updatedAt : null };\n}\n\nfunction msSince(iso: string): number | null {\n const t = Date.parse(iso);\n return Number.isNaN(t) ? null : Date.now() - t;\n}\n\nfunction toEpoch(iso: string): number {\n const t = Date.parse(iso);\n return Number.isNaN(t) ? 0 : t;\n}\n\nconst INSPECT_TIMEOUT_MS = 15_000;\nconst PAUSE_TIMEOUT_MS = 30_000;\n\ninterface DockerResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nfunction runDocker(args: string[], timeoutMs: number): Promise<DockerResult> {\n return new Promise<DockerResult>((resolve) => {\n const child = spawn('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] });\n let stdout = '';\n let stderr = '';\n let settled = false;\n const finish = (exitCode: number): void => {\n if (settled) return;\n settled = true;\n resolve({ exitCode, stdout, stderr });\n };\n const timer = setTimeout(() => {\n child.kill('SIGTERM');\n stderr += `\\nrelay: docker ${args.join(' ')} timed out after ${String(timeoutMs)}ms\\n`;\n finish(124);\n }, timeoutMs);\n child.stdout?.on('data', (c: Buffer) => {\n stdout += c.toString('utf8');\n });\n child.stderr?.on('data', (c: Buffer) => {\n stderr += c.toString('utf8');\n });\n child.on('error', (err) => {\n clearTimeout(timer);\n stderr += String(err.message ?? err);\n finish(127);\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n finish(code ?? -1);\n });\n });\n}\n\n/** Mirrors `inspectContainerStatus` in @agentbox/sandbox-docker (no dep on it — cycle). */\nasync function inspectContainerState(name: string): Promise<ContainerState> {\n const r = await runDocker(['inspect', '--format', '{{.State.Status}}', name], INSPECT_TIMEOUT_MS);\n if (r.exitCode !== 0) return 'missing';\n switch (r.stdout.trim()) {\n case 'running':\n return 'running';\n case 'paused':\n return 'paused';\n case 'created':\n case 'exited':\n case 'dead':\n case 'restarting':\n case 'removing':\n return 'stopped';\n default:\n return 'missing';\n }\n}\n\nasync function pauseContainer(name: string): Promise<void> {\n const r = await runDocker(['pause', name], PAUSE_TIMEOUT_MS);\n if (r.exitCode !== 0) {\n throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);\n }\n}\n","export type ServiceState =\n | 'pending'\n | 'waiting'\n | 'starting'\n | 'running'\n | 'ready'\n | 'unhealthy'\n | 'crashed'\n | 'backoff'\n | 'stopped';\n\nexport type TaskState = 'pending' | 'waiting' | 'running' | 'done' | 'failed' | 'skipped';\n\nexport interface ServiceStatus {\n name: string;\n state: ServiceState;\n pid: number | null;\n restarts: number;\n lastExitCode: number | null;\n startedAt: string | null;\n readyAt: string | null;\n nextRetryAt: string | null;\n blockedOn: string[];\n command: string;\n}\n\nexport interface StatusReply {\n services: ServiceStatus[];\n tasks: TaskStatus[];\n ports: BoxStatusPort[];\n}\n\nexport interface WaitReadyArgs {\n timeoutMs?: number;\n units?: string[];\n}\n\nexport type WaitReadyReply =\n | { ready: true }\n | { ready: false; timedOut: string[]; failed: string[] };\n\nexport interface TaskStatus {\n name: string;\n state: TaskState;\n pid: number | null;\n lastExitCode: number | null;\n startedAt: string | null;\n finishedAt: string | null;\n command: string;\n}\n\nexport interface LogEvent {\n service: string;\n ts: string;\n stream: 'stdout' | 'stderr';\n line: string;\n}\n\nexport interface ReloadResult {\n added: string[];\n removed: string[];\n changed: string[];\n}\n\n/**\n * Coarse activity state of the in-box Claude Code session, fed by Claude Code\n * hooks via `agentbox-ctl claude-state <state>`. `unknown` is the initial value\n * before any hook has fired (or for boxes whose image predates the hooks).\n */\nexport type ClaudeActivityState = 'working' | 'idle' | 'waiting' | 'unknown';\n\nexport const CLAUDE_ACTIVITY_STATES: readonly ClaudeActivityState[] = [\n 'working',\n 'idle',\n 'waiting',\n 'unknown',\n];\n\nexport interface BoxStatusServiceEntry {\n name: string;\n state: ServiceState;\n /** Configured `ready_when` port for this service, else null. */\n port: number | null;\n /**\n * The service's `expose:` mapping (container `as` → in-box `port`) when it is\n * the designated web service, else absent. Additive field — snapshots written\n * before this existed simply lack it (schema stays 1; treat absent as none).\n */\n expose?: { port: number; as: number };\n}\n\nexport interface BoxStatusTaskEntry {\n name: string;\n state: TaskState;\n}\n\nexport interface BoxStatusPort {\n port: number;\n /** Name of the service whose `ready_when` port matches, else null (ad-hoc). */\n service: string | null;\n}\n\nexport interface BoxStatusClaude {\n state: ClaudeActivityState;\n /** ISO-8601 time the last claude-state hook fired, or null if none yet. */\n updatedAt: string | null;\n /** Whether the claude tmux session was present at snapshot time. */\n sessionRunning: boolean;\n /**\n * Human-readable title Claude Code set on its terminal (the in-box tmux\n * pane title), sanitized. Additive field — snapshots written before this\n * existed simply lack it (schema stays 1; treat absent as no title).\n */\n sessionTitle?: string;\n}\n\n/**\n * Durable snapshot of a box's runtime status. The in-box daemon builds it and\n * pushes it to the host relay, which persists it to\n * `~/.agentbox/boxes/<id>/status.json` so `agentbox status` / `list` /\n * `inspect` can show it even when the box is paused or stopped.\n */\nexport interface BoxStatus {\n /** Schema version; bump on incompatible changes so old readers can reject. */\n schema: 1;\n boxId: string;\n /** ISO-8601 time the daemon built this snapshot. */\n timestamp: string;\n services: BoxStatusServiceEntry[];\n tasks: BoxStatusTaskEntry[];\n /** Live-discovered listening TCP ports inside the box. */\n ports: BoxStatusPort[];\n claude: BoxStatusClaude;\n}\n\nexport const BOX_STATUS_SCHEMA = 1 as const;\n\n/** Relay event type carrying a `BoxStatus` payload. */\nexport const BOX_STATUS_EVENT = 'box-status';\n\nexport type CtlRequest =\n | { op: 'status' }\n | { op: 'task-status' }\n | { op: 'wait-ready'; timeoutMs?: number; units?: string[] }\n | { op: 'run-task'; name: string; force?: boolean }\n | { op: 'logs'; service: string; tail?: number; follow?: boolean }\n | { op: 'restart'; service: string }\n | { op: 'stop'; service: string }\n | { op: 'start'; service: string }\n | { op: 'reload' }\n | { op: 'ping' }\n | { op: 'claude-session'; sessionName?: string }\n | { op: 'claude-state'; state: ClaudeActivityState };\n\nexport type CtlResponse = { ok: true; data: unknown } | { ok: false; error: string };\n\n/**\n * Status of the in-container tmux session running Claude Code. The daemon\n * doesn't own this session lifecycle — it probes via `tmux has-session` and\n * `tmux display-message`. Missing tmux server / missing session both surface\n * as `running: false`.\n */\nexport interface ClaudeSessionStatus {\n running: boolean;\n sessionName: string;\n /** ISO-8601 timestamp from tmux's `#{session_created}`, or null when not running. */\n startedAt: string | null;\n /**\n * Sanitized tmux `#{pane_title}` (the title Claude Code set on its\n * terminal), or null when not running / no meaningful title.\n */\n title: string | null;\n}\n\nexport const DEFAULT_SOCKET_PATH = '/run/agentbox/ctl.sock';\nexport const DEFAULT_CONFIG_PATH = '/workspace/agentbox.yaml';\nexport const DEFAULT_LOG_DIR = '/var/log/agentbox';\nexport const DEFAULT_CLAUDE_SESSION_NAME = 'claude';\n","import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { createConnection, type Socket } from 'node:net';\nimport type {\n ClaudeActivityState,\n ClaudeSessionStatus,\n CtlRequest,\n CtlResponse,\n LogEvent,\n ReloadResult,\n ServiceStatus,\n StatusReply,\n TaskStatus,\n WaitReadyArgs,\n WaitReadyReply,\n} from './types.js';\n\nexport interface ConnectOptions {\n socketPath: string;\n /** Default 3000 ms. */\n timeoutMs?: number;\n}\n\ninterface NodeErrno extends Error {\n code?: string;\n}\n\n/**\n * Best-effort daemon respawn when the unix socket is dead. `docker exec -d`\n * leaves no log when the daemon crashes on startup and Node doesn't unlink\n * unix sockets on exit, so an orphaned file is the symptom we see most often.\n * Spawning the bin detached and polling for a fresh listener recovers without\n * needing host involvement — mirrors `ensureRelay()` on the host side.\n *\n * Gated on `AGENTBOX=1` (set on every box at `docker run`) so unit tests on\n * the host — which start an ephemeral server with no `agentbox-ctl` bin\n * anywhere on PATH — don't accidentally spawn anything.\n */\nasync function tryReviveDaemon(socketPath: string): Promise<boolean> {\n if (process.env.AGENTBOX !== '1') return false;\n try {\n const child = spawn('agentbox-ctl', ['daemon'], {\n detached: true,\n stdio: 'ignore',\n });\n child.unref();\n } catch {\n return false;\n }\n // The daemon `unlink`s any stale socket file before `listen()`, so once the\n // file reappears it's bound to a live listener.\n for (let i = 0; i < 50; i++) {\n await new Promise((r) => setTimeout(r, 100));\n if (existsSync(socketPath)) return true;\n }\n return false;\n}\n\nasync function connectOnce(opts: ConnectOptions): Promise<Socket> {\n const sock = createConnection(opts.socketPath);\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n sock.destroy();\n reject(new Error(`connect ${opts.socketPath} timed out`));\n }, opts.timeoutMs ?? 3000);\n sock.once('connect', () => {\n clearTimeout(timer);\n resolve();\n });\n sock.once('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n return sock;\n}\n\nasync function connect(opts: ConnectOptions): Promise<Socket> {\n try {\n return await connectOnce(opts);\n } catch (err) {\n // Only ECONNREFUSED (stale socket file, no listener) and ENOENT (no file\n // at all) suggest a missing daemon. Anything else (EACCES, ETIMEDOUT, …)\n // is a real failure we shouldn't try to paper over.\n const code = (err as NodeErrno).code;\n if (code !== 'ECONNREFUSED' && code !== 'ENOENT') throw err;\n const revived = await tryReviveDaemon(opts.socketPath);\n if (!revived) throw err;\n return await connectOnce(opts);\n }\n}\n\nasync function sendOneShot<T>(opts: ConnectOptions, req: CtlRequest): Promise<T> {\n const sock = await connect(opts);\n sock.write(`${JSON.stringify(req)}\\n`);\n let buf = '';\n sock.setEncoding('utf8');\n for await (const chunk of sock) {\n buf += chunk as string;\n const idx = buf.indexOf('\\n');\n if (idx !== -1) {\n const line = buf.slice(0, idx);\n sock.end();\n return parseResponse<T>(line);\n }\n }\n // Connection closed before we got a full line.\n if (buf.length > 0) return parseResponse<T>(buf);\n throw new Error('connection closed with no response');\n}\n\nfunction parseResponse<T>(line: string): T {\n const parsed = JSON.parse(line) as CtlResponse;\n if (parsed.ok) return parsed.data as T;\n throw new Error(parsed.error);\n}\n\nexport async function ping(opts: ConnectOptions): Promise<'pong'> {\n return sendOneShot<'pong'>(opts, { op: 'ping' });\n}\n\nexport async function status(opts: ConnectOptions): Promise<StatusReply> {\n return sendOneShot<StatusReply>(opts, { op: 'status' });\n}\n\nexport async function taskStatus(opts: ConnectOptions): Promise<TaskStatus[]> {\n return sendOneShot<TaskStatus[]>(opts, { op: 'task-status' });\n}\n\nexport async function waitReady(\n opts: ConnectOptions,\n args: WaitReadyArgs = {},\n): Promise<WaitReadyReply> {\n return sendOneShot<WaitReadyReply>(opts, {\n op: 'wait-ready',\n timeoutMs: args.timeoutMs,\n units: args.units,\n });\n}\n\nexport async function runTask(\n opts: ConnectOptions,\n name: string,\n force?: boolean,\n): Promise<TaskStatus> {\n return sendOneShot<TaskStatus>(opts, { op: 'run-task', name, force });\n}\n\nexport async function restart(opts: ConnectOptions, service: string): Promise<ServiceStatus> {\n return sendOneShot<ServiceStatus>(opts, { op: 'restart', service });\n}\n\nexport async function stop(opts: ConnectOptions, service: string): Promise<ServiceStatus> {\n return sendOneShot<ServiceStatus>(opts, { op: 'stop', service });\n}\n\nexport async function start(opts: ConnectOptions, service: string): Promise<ServiceStatus> {\n return sendOneShot<ServiceStatus>(opts, { op: 'start', service });\n}\n\nexport async function reload(opts: ConnectOptions): Promise<ReloadResult> {\n return sendOneShot<ReloadResult>(opts, { op: 'reload' });\n}\n\nexport async function claudeSession(\n opts: ConnectOptions & { sessionName?: string },\n): Promise<ClaudeSessionStatus> {\n return sendOneShot<ClaudeSessionStatus>(opts, {\n op: 'claude-session',\n sessionName: opts.sessionName,\n });\n}\n\nexport async function claudeState(\n opts: ConnectOptions,\n state: ClaudeActivityState,\n): Promise<'ok'> {\n return sendOneShot<'ok'>(opts, { op: 'claude-state', state });\n}\n\nexport interface LogsResult {\n initial: LogEvent[];\n /**\n * When `follow: true` was passed, this async iterator yields further events\n * until the caller breaks out (which closes the socket).\n */\n follow?: AsyncIterableIterator<LogEvent>;\n}\n\nexport async function logs(\n opts: ConnectOptions,\n args: { service: string; tail?: number; follow?: boolean },\n): Promise<LogsResult> {\n const sock = await connect(opts);\n sock.write(`${JSON.stringify({ op: 'logs', ...args })}\\n`);\n\n const lines = createLineIterator(sock);\n const first = await lines.next();\n if (first.done) {\n sock.end();\n throw new Error('connection closed with no response');\n }\n const parsed = JSON.parse(first.value) as CtlResponse;\n if (!parsed.ok) {\n sock.end();\n throw new Error(parsed.error);\n }\n const data = parsed.data as { events: LogEvent[]; follow: boolean };\n if (!data.follow) {\n sock.end();\n return { initial: data.events };\n }\n\n const followGen = (async function* () {\n try {\n for await (const line of lines) {\n const p = JSON.parse(line) as CtlResponse;\n if (p.ok && p.data && typeof p.data === 'object' && 'event' in p.data) {\n yield (p.data as { event: LogEvent }).event;\n }\n }\n } finally {\n sock.end();\n }\n })();\n\n return { initial: data.events, follow: followGen };\n}\n\nasync function* createLineIterator(sock: Socket): AsyncIterableIterator<string> {\n let buf = '';\n sock.setEncoding('utf8');\n for await (const chunk of sock) {\n buf += chunk as string;\n let idx = buf.indexOf('\\n');\n while (idx !== -1) {\n yield buf.slice(0, idx);\n buf = buf.slice(idx + 1);\n idx = buf.indexOf('\\n');\n }\n }\n if (buf.length > 0) yield buf;\n}\n","import type { BoxStatusPort, ServiceStatus, TaskStatus } from './types.js';\n\nexport function renderStatusTable(rows: ServiceStatus[]): string {\n if (rows.length === 0) return '(no services configured)';\n const headers = ['NAME', 'STATE', 'PID', 'RESTARTS', 'LAST EXIT', 'BLOCKED ON', 'COMMAND'];\n const data: string[][] = rows.map((r) => [\n r.name,\n r.state,\n r.pid === null ? '-' : String(r.pid),\n String(r.restarts),\n r.lastExitCode === null ? '-' : String(r.lastExitCode),\n r.blockedOn.length === 0 ? '-' : r.blockedOn.join(','),\n truncate(r.command, 40),\n ]);\n return renderTable(headers, data);\n}\n\nexport function renderTaskTable(rows: TaskStatus[]): string {\n if (rows.length === 0) return '(no tasks configured)';\n const headers = ['NAME', 'STATE', 'EXIT', 'STARTED', 'FINISHED', 'COMMAND'];\n const data: string[][] = rows.map((r) => [\n r.name,\n r.state,\n r.lastExitCode === null ? '-' : String(r.lastExitCode),\n r.startedAt ?? '-',\n r.finishedAt ?? '-',\n truncate(r.command, 40),\n ]);\n return renderTable(headers, data);\n}\n\nexport function renderPortsTable(rows: BoxStatusPort[]): string {\n if (rows.length === 0) return '(no ports listening)';\n const named = rows.filter((r) => r.service);\n const other = rows\n .filter((r) => !r.service)\n .map((r) => r.port)\n .sort((a, b) => a - b);\n const lines: string[] = [];\n if (named.length > 0) {\n lines.push(\n renderTable(\n ['PORT', 'SERVICE'],\n named.map((r) => [`:${String(r.port)}`, r.service ?? '-']),\n ),\n );\n }\n // Collapse the unattributed ports (VNC, dev-tool worker IPC, multi-port\n // services like inngest) into one line so they don't drown the real services.\n if (other.length > 0) {\n lines.push(`other (${other.length}): ${other.join(', ')}`);\n }\n return lines.join('\\n');\n}\n\nfunction renderTable(headers: string[], data: string[][]): string {\n const widths = headers.map((h, i) =>\n Math.max(h.length, ...data.map((row) => (row[i] ?? '').length)),\n );\n const fmt = (row: string[]): string =>\n row.map((cell, i) => cell.padEnd(widths[i] ?? cell.length)).join(' ');\n return [fmt(headers), ...data.map(fmt)].join('\\n');\n}\n\nfunction truncate(s: string, n: number): string {\n if (s.length <= n) return s;\n return s.slice(0, n - 1) + '…';\n}\n","import { readFile } from 'node:fs/promises';\nimport { parse as parseYaml } from 'yaml';\n\nexport type RestartPolicy = 'always' | 'on-failure' | 'never';\nexport type ProbeOnTimeout = 'kill' | 'mark_unhealthy';\n\nexport interface BackoffSpec {\n initialMs: number;\n maxMs: number;\n factor: number;\n}\n\nexport interface PortProbe {\n kind: 'port';\n port: number;\n host: string;\n intervalMs: number;\n initialDelayMs: number;\n timeoutMs: number;\n onTimeout: ProbeOnTimeout;\n}\n\nexport interface LogMatchProbe {\n kind: 'log_match';\n pattern: RegExp;\n timeoutMs: number;\n onTimeout: ProbeOnTimeout;\n}\n\nexport interface HttpProbe {\n kind: 'http';\n url: string;\n expectStatus?: number;\n intervalMs: number;\n initialDelayMs: number;\n timeoutMs: number;\n onTimeout: ProbeOnTimeout;\n}\n\nexport type ReadyProbe = PortProbe | LogMatchProbe | HttpProbe;\n\nexport interface ExposeSpec {\n /** The port this service listens on inside the box. */\n port: number;\n /** Container port forwarded to it. Only 80 is reserved/published today. */\n as: number;\n}\n\nexport interface TaskSpec {\n name: string;\n command: string | string[];\n cwd?: string;\n env?: Record<string, string>;\n needs: string[];\n}\n\nexport interface ServiceSpec {\n name: string;\n command: string | string[];\n cwd?: string;\n env?: Record<string, string>;\n autostart: boolean;\n restart: RestartPolicy;\n backoff: BackoffSpec;\n needs: string[];\n readyWhen?: ReadyProbe;\n /** When set, container port `expose.as` forwards to `127.0.0.1:expose.port`. */\n expose?: ExposeSpec;\n}\n\nexport interface CtlConfig {\n services: ServiceSpec[];\n tasks: TaskSpec[];\n}\n\nexport const DEFAULT_BACKOFF: BackoffSpec = {\n initialMs: 500,\n maxMs: 30_000,\n factor: 2,\n};\n\nexport const DEFAULT_PROBE_INTERVAL_MS = 500;\nexport const DEFAULT_PROBE_INITIAL_DELAY_MS = 0;\nexport const DEFAULT_PROBE_TIMEOUT_MS = 60_000;\nexport const DEFAULT_PROBE_HOST = '127.0.0.1';\nexport const DEFAULT_PROBE_ON_TIMEOUT: ProbeOnTimeout = 'kill';\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction parseEnv(raw: unknown, where: string): Record<string, string> | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.env must be a mapping of string → string`);\n }\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(raw)) {\n if (typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean') {\n throw new ConfigError(`${where}.env.${k} must be a scalar`);\n }\n out[k] = String(v);\n }\n return out;\n}\n\nfunction parseCommand(raw: unknown, where: string): string | string[] {\n if (typeof raw === 'string') {\n if (raw.trim().length === 0) {\n throw new ConfigError(`${where}.command must not be empty`);\n }\n return raw;\n }\n if (Array.isArray(raw)) {\n if (raw.length === 0) {\n throw new ConfigError(`${where}.command array must not be empty`);\n }\n const argv: string[] = [];\n for (const [i, item] of raw.entries()) {\n if (typeof item !== 'string') {\n throw new ConfigError(`${where}.command[${String(i)}] must be a string`);\n }\n argv.push(item);\n }\n return argv;\n }\n throw new ConfigError(`${where}.command must be a string or array of strings`);\n}\n\nfunction parseRestart(raw: unknown, where: string): RestartPolicy {\n if (raw === undefined) return 'on-failure';\n if (raw === 'always' || raw === 'on-failure' || raw === 'never') return raw;\n throw new ConfigError(`${where}.restart must be one of: always, on-failure, never`);\n}\n\nconst BACKOFF_KEYS = new Set(['initial_ms', 'max_ms', 'factor']);\n\nfunction parseBackoff(raw: unknown, where: string): BackoffSpec {\n if (raw === undefined) return { ...DEFAULT_BACKOFF };\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.backoff must be a mapping`);\n }\n rejectUnknownKeys(raw, BACKOFF_KEYS, `${where}.backoff`);\n const initialMs = parseNonNegativeInt(\n raw.initial_ms,\n `${where}.backoff.initial_ms`,\n DEFAULT_BACKOFF.initialMs,\n );\n const maxMs = parseNonNegativeInt(raw.max_ms, `${where}.backoff.max_ms`, DEFAULT_BACKOFF.maxMs);\n const factor = parseFactor(raw.factor, `${where}.backoff.factor`, DEFAULT_BACKOFF.factor);\n if (maxMs < initialMs) {\n throw new ConfigError(`${where}.backoff.max_ms must be >= initial_ms`);\n }\n return { initialMs, maxMs, factor };\n}\n\nfunction rejectUnknownKeys(\n obj: Record<string, unknown>,\n allowed: Set<string>,\n where: string,\n): void {\n for (const key of Object.keys(obj)) {\n if (!allowed.has(key)) {\n throw new ConfigError(`${where} has unknown key \"${key}\"`);\n }\n }\n}\n\nfunction parseNonNegativeInt(raw: unknown, where: string, fallback: number): number {\n if (raw === undefined) return fallback;\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) {\n throw new ConfigError(`${where} must be a non-negative number`);\n }\n return Math.floor(raw);\n}\n\nfunction parsePositiveInt(raw: unknown, where: string, fallback: number): number {\n if (raw === undefined) return fallback;\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 1) {\n throw new ConfigError(`${where} must be a positive integer`);\n }\n return Math.floor(raw);\n}\n\nfunction parseFactor(raw: unknown, where: string, fallback: number): number {\n if (raw === undefined) return fallback;\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 1) {\n throw new ConfigError(`${where} must be a number >= 1`);\n }\n return raw;\n}\n\nfunction parseOnTimeout(raw: unknown, where: string): ProbeOnTimeout {\n if (raw === undefined) return DEFAULT_PROBE_ON_TIMEOUT;\n if (raw === 'kill' || raw === 'mark_unhealthy') return raw;\n throw new ConfigError(`${where} must be one of: kill, mark_unhealthy`);\n}\n\nfunction parseNeeds(raw: unknown, where: string): string[] {\n if (raw === undefined || raw === null) return [];\n if (!Array.isArray(raw)) {\n throw new ConfigError(`${where} must be an array of unit names`);\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const [i, item] of raw.entries()) {\n if (typeof item !== 'string') {\n throw new ConfigError(`${where}[${String(i)}] must be a string`);\n }\n if (!/^[A-Za-z0-9_-]+$/.test(item)) {\n throw new ConfigError(`${where}[${String(i)}] \"${item}\" must match [A-Za-z0-9_-]+`);\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\nconst PROBE_KEYS = new Set([\n 'port',\n 'host',\n 'log_match',\n 'http',\n 'expect_status',\n 'interval_ms',\n 'initial_delay_ms',\n 'timeout_ms',\n 'on_timeout',\n]);\n\nfunction parseReadyWhen(raw: unknown, where: string): ReadyProbe | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.ready_when must be a mapping`);\n }\n rejectUnknownKeys(raw, PROBE_KEYS, `${where}.ready_when`);\n\n const kinds: Array<'port' | 'log_match' | 'http'> = [];\n if (raw.port !== undefined) kinds.push('port');\n if (raw.log_match !== undefined) kinds.push('log_match');\n if (raw.http !== undefined) kinds.push('http');\n if (kinds.length === 0) {\n throw new ConfigError(\n `${where}.ready_when must declare exactly one of: port, log_match, http`,\n );\n }\n if (kinds.length > 1) {\n throw new ConfigError(\n `${where}.ready_when may declare only one of: port, log_match, http (got ${kinds.join(', ')})`,\n );\n }\n\n const timeoutMs = parsePositiveInt(\n raw.timeout_ms,\n `${where}.ready_when.timeout_ms`,\n DEFAULT_PROBE_TIMEOUT_MS,\n );\n const onTimeout = parseOnTimeout(raw.on_timeout, `${where}.ready_when.on_timeout`);\n\n const kind = kinds[0]!;\n if (kind === 'log_match') {\n if (raw.host !== undefined || raw.expect_status !== undefined || raw.interval_ms !== undefined || raw.initial_delay_ms !== undefined) {\n throw new ConfigError(\n `${where}.ready_when.log_match cannot be combined with host/expect_status/interval_ms/initial_delay_ms`,\n );\n }\n const pat = assertString(raw.log_match, `${where}.ready_when.log_match`);\n let pattern: RegExp;\n try {\n pattern = new RegExp(pat);\n } catch (err) {\n throw new ConfigError(\n `${where}.ready_when.log_match is not a valid regex: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return { kind: 'log_match', pattern, timeoutMs, onTimeout };\n }\n\n const intervalMs = parsePositiveInt(\n raw.interval_ms,\n `${where}.ready_when.interval_ms`,\n DEFAULT_PROBE_INTERVAL_MS,\n );\n const initialDelayMs = parseNonNegativeInt(\n raw.initial_delay_ms,\n `${where}.ready_when.initial_delay_ms`,\n DEFAULT_PROBE_INITIAL_DELAY_MS,\n );\n\n if (kind === 'port') {\n if (raw.expect_status !== undefined) {\n throw new ConfigError(`${where}.ready_when.expect_status only applies to http probes`);\n }\n const port = parsePositiveInt(raw.port, `${where}.ready_when.port`, 0);\n if (port < 1 || port > 65535) {\n throw new ConfigError(`${where}.ready_when.port must be between 1 and 65535`);\n }\n const host =\n raw.host === undefined\n ? DEFAULT_PROBE_HOST\n : assertString(raw.host, `${where}.ready_when.host`);\n return { kind: 'port', port, host, intervalMs, initialDelayMs, timeoutMs, onTimeout };\n }\n\n if (raw.host !== undefined) {\n throw new ConfigError(`${where}.ready_when.host only applies to port probes`);\n }\n const url = assertString(raw.http, `${where}.ready_when.http`);\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new ConfigError(`${where}.ready_when.http must be a valid URL`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new ConfigError(`${where}.ready_when.http must use http(s):// (got ${parsed.protocol})`);\n }\n let expectStatus: number | undefined;\n if (raw.expect_status !== undefined) {\n expectStatus = parsePositiveInt(raw.expect_status, `${where}.ready_when.expect_status`, 0);\n if (expectStatus < 100 || expectStatus > 599) {\n throw new ConfigError(`${where}.ready_when.expect_status must be between 100 and 599`);\n }\n }\n return { kind: 'http', url, expectStatus, intervalMs, initialDelayMs, timeoutMs, onTimeout };\n}\n\n/**\n * The only container port AgentBox reserves + publishes for a web service today\n * (see `WEB_CONTAINER_PORT` host-side in @agentbox/sandbox-docker). A service's\n * `expose.as` must equal this — any other value would parse fine but be\n * unreachable from the host, so we reject it loudly.\n */\nexport const RESERVED_WEB_PORT = 80;\n\nconst SERVICE_KEYS = new Set([\n 'command',\n 'cwd',\n 'env',\n 'autostart',\n 'restart',\n 'backoff',\n 'needs',\n 'ready_when',\n 'expose',\n 'ide',\n]);\n\nconst EXPOSE_KEYS = new Set(['port', 'as']);\n\nfunction parseExpose(raw: unknown, where: string): ExposeSpec | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.expose must be a mapping`);\n }\n rejectUnknownKeys(raw, EXPOSE_KEYS, `${where}.expose`);\n if (raw.port === undefined) {\n throw new ConfigError(`${where}.expose.port is required`);\n }\n const port = parsePortNumber(raw.port, `${where}.expose.port`);\n const as = raw.as === undefined ? RESERVED_WEB_PORT : parsePortNumber(raw.as, `${where}.expose.as`);\n if (as !== RESERVED_WEB_PORT) {\n throw new ConfigError(\n `${where}.expose.as must be ${String(RESERVED_WEB_PORT)} (the only container port AgentBox publishes)`,\n );\n }\n return { port, as };\n}\n\nfunction parsePortNumber(raw: unknown, where: string): number {\n if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1 || raw > 65535) {\n throw new ConfigError(`${where} must be an integer between 1 and 65535`);\n }\n return raw;\n}\n\nfunction parseService(name: string, raw: unknown): ServiceSpec {\n const where = `services.${name}`;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where} must be a mapping`);\n }\n rejectUnknownKeys(raw, SERVICE_KEYS, where);\n const command = parseCommand(raw.command, where);\n const cwd = raw.cwd === undefined ? undefined : assertString(raw.cwd, `${where}.cwd`);\n const env = parseEnv(raw.env, where);\n const autostart =\n raw.autostart === undefined ? true : assertBool(raw.autostart, `${where}.autostart`);\n const restart = parseRestart(raw.restart, where);\n const backoff = parseBackoff(raw.backoff, where);\n const needs = parseNeeds(raw.needs, `${where}.needs`);\n const readyWhen = parseReadyWhen(raw.ready_when, where);\n const expose = parseExpose(raw.expose, where);\n return { name, command, cwd, env, autostart, restart, backoff, needs, readyWhen, expose };\n}\n\nconst TASK_KEYS = new Set(['command', 'cwd', 'env', 'needs']);\n\nfunction parseTask(name: string, raw: unknown): TaskSpec {\n const where = `tasks.${name}`;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where} must be a mapping`);\n }\n rejectUnknownKeys(raw, TASK_KEYS, where);\n const command = parseCommand(raw.command, where);\n const cwd = raw.cwd === undefined ? undefined : assertString(raw.cwd, `${where}.cwd`);\n const env = parseEnv(raw.env, where);\n const needs = parseNeeds(raw.needs, `${where}.needs`);\n return { name, command, cwd, env, needs };\n}\n\nfunction assertString(raw: unknown, where: string): string {\n if (typeof raw !== 'string') throw new ConfigError(`${where} must be a string`);\n return raw;\n}\n\nfunction assertBool(raw: unknown, where: string): boolean {\n if (typeof raw !== 'boolean') throw new ConfigError(`${where} must be a boolean`);\n return raw;\n}\n\n// `defaults` is the host-side config layer (read by @agentbox/config) — the\n// supervisor doesn't touch it, but we accept it here so `ctl validate` doesn't\n// flag it as unknown. Strict typo-detection still applies (top-level keys\n// outside this set are rejected).\nconst TOP_LEVEL_KEYS = new Set(['services', 'tasks', 'ide', 'defaults']);\n\nfunction validateUnitGraph(tasks: TaskSpec[], services: ServiceSpec[]): void {\n const names = new Set<string>();\n for (const t of tasks) {\n if (names.has(t.name)) {\n throw new ConfigError(`unit name \"${t.name}\" declared more than once (task vs service collision)`);\n }\n names.add(t.name);\n }\n for (const s of services) {\n if (names.has(s.name)) {\n throw new ConfigError(`unit name \"${s.name}\" declared more than once (task vs service collision)`);\n }\n names.add(s.name);\n }\n\n const deps = new Map<string, string[]>();\n for (const t of tasks) deps.set(t.name, t.needs);\n for (const s of services) deps.set(s.name, s.needs);\n\n for (const [unit, list] of deps) {\n for (const dep of list) {\n if (!names.has(dep)) {\n throw new ConfigError(`unit \"${unit}\" needs unknown unit \"${dep}\"`);\n }\n if (dep === unit) {\n throw new ConfigError(`unit \"${unit}\" cannot depend on itself`);\n }\n }\n }\n\n // DFS with three-color marking; record the cycle path in the error.\n const WHITE = 0, GRAY = 1, BLACK = 2;\n const color = new Map<string, number>();\n for (const name of deps.keys()) color.set(name, WHITE);\n const stack: string[] = [];\n\n function visit(name: string): void {\n color.set(name, GRAY);\n stack.push(name);\n for (const dep of deps.get(name)!) {\n const c = color.get(dep) ?? WHITE;\n if (c === GRAY) {\n const startIdx = stack.indexOf(dep);\n const cycle = stack.slice(startIdx).concat(dep).join(' → ');\n throw new ConfigError(`cyclic dependency: ${cycle}`);\n }\n if (c === WHITE) visit(dep);\n }\n stack.pop();\n color.set(name, BLACK);\n }\n\n for (const name of deps.keys()) {\n if (color.get(name) === WHITE) visit(name);\n }\n}\n\nexport function parseConfig(text: string): CtlConfig {\n let doc: unknown;\n try {\n doc = parseYaml(text);\n } catch (err) {\n throw new ConfigError(`yaml parse error: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (doc === null || doc === undefined) return { services: [], tasks: [] };\n if (!isPlainObject(doc)) {\n throw new ConfigError('top-level config must be a mapping');\n }\n rejectUnknownKeys(doc, TOP_LEVEL_KEYS, '(root)');\n\n const services: ServiceSpec[] = [];\n const servicesRaw = doc.services;\n if (servicesRaw !== undefined && servicesRaw !== null) {\n if (!isPlainObject(servicesRaw)) {\n throw new ConfigError('services must be a mapping of name → service');\n }\n for (const [name, raw] of Object.entries(servicesRaw)) {\n if (!/^[A-Za-z0-9_-]+$/.test(name)) {\n throw new ConfigError(`service name \"${name}\" must match [A-Za-z0-9_-]+`);\n }\n services.push(parseService(name, raw));\n }\n }\n\n const tasks: TaskSpec[] = [];\n const tasksRaw = doc.tasks;\n if (tasksRaw !== undefined && tasksRaw !== null) {\n if (!isPlainObject(tasksRaw)) {\n throw new ConfigError('tasks must be a mapping of name → task');\n }\n for (const [name, raw] of Object.entries(tasksRaw)) {\n if (!/^[A-Za-z0-9_-]+$/.test(name)) {\n throw new ConfigError(`task name \"${name}\" must match [A-Za-z0-9_-]+`);\n }\n tasks.push(parseTask(name, raw));\n }\n }\n\n // ide: parsed only enough to confirm it's an object; contents are host-side\n // and the supervisor doesn't touch them. Schema is permissive here too.\n if (doc.ide !== undefined && doc.ide !== null && !isPlainObject(doc.ide)) {\n throw new ConfigError('ide must be a mapping');\n }\n\n // defaults: host-side layered-config block. We only require it to be a\n // mapping here; @agentbox/config validates the leaves strictly when the\n // host loads the file. Letting ctl deep-validate would force a circular\n // dependency on the host-only @agentbox/config package — see CLAUDE.md.\n if (doc.defaults !== undefined && doc.defaults !== null && !isPlainObject(doc.defaults)) {\n throw new ConfigError('defaults must be a mapping');\n }\n\n validateUnitGraph(tasks, services);\n\n const exposed = services.filter((s) => s.expose);\n if (exposed.length > 1) {\n throw new ConfigError(\n `at most one service may set expose: (got: ${exposed.map((s) => s.name).join(', ')})`,\n );\n }\n\n return { services, tasks };\n}\n\nexport async function loadConfig(path: string): Promise<CtlConfig> {\n let text: string;\n try {\n text = await readFile(path, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { services: [], tasks: [] };\n }\n throw err;\n }\n return parseConfig(text);\n}\n\nexport function describeCommand(cmd: string | string[]): string {\n return Array.isArray(cmd) ? cmd.join(' ') : cmd;\n}\n"],"mappings":"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","names":["execa","readdir","stat","join","mkdir","rm","homedir","stat","execa","randomBytes","mkdir","readFile","writeFile","homedir","join","resolve","delay","join","readdir","stat","execa","homedir","mkdir","rm","pathExists","delay","writeFile","resolve","readFile","randomBytes","execa","readFile","isPlainObject","restart","readFile"]}
|