@madarco/agentbox 0.13.0 → 0.14.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/CHANGELOG.md +65 -0
- package/README.md +11 -8
- package/dist/{_cloud-attach-HJC672UR.js → _cloud-attach-GUBB5RH2.js} +4 -4
- package/dist/{chunk-R5XIDQFR.js → chunk-BKU34KYY.js} +170 -6
- package/dist/chunk-BKU34KYY.js.map +1 -0
- package/dist/{chunk-QYRK5H6Q.js → chunk-BYCLD6D6.js} +17 -9
- package/dist/chunk-BYCLD6D6.js.map +1 -0
- package/dist/chunk-LDMYHWUS.js +346 -0
- package/dist/chunk-LDMYHWUS.js.map +1 -0
- package/dist/{chunk-2LF5YILI.js → chunk-RSKG7AFU.js} +80 -6
- package/dist/chunk-RSKG7AFU.js.map +1 -0
- package/dist/{chunk-4NQXNQ53.js → chunk-TBSIJVSN.js} +149 -47
- package/dist/chunk-TBSIJVSN.js.map +1 -0
- package/dist/{chunk-B4QG2MCW.js → chunk-TCS5HXJX.js} +381 -174
- package/dist/chunk-TCS5HXJX.js.map +1 -0
- package/dist/{chunk-ECLLV5JH.js → chunk-VATTS2MR.js} +156 -5
- package/dist/chunk-VATTS2MR.js.map +1 -0
- package/dist/{chunk-SNTHHWKY.js → chunk-XKH7NTT7.js} +80 -22
- package/dist/chunk-XKH7NTT7.js.map +1 -0
- package/dist/dist-34RKQ74M.js +662 -0
- package/dist/dist-34RKQ74M.js.map +1 -0
- package/dist/{dist-OPIBZ7XM.js → dist-3IMQNTTV.js} +14 -69
- package/dist/dist-3IMQNTTV.js.map +1 -0
- package/dist/{dist-OG6NW6SM.js → dist-4DPOL5A7.js} +5 -3
- package/dist/{dist-JAN5VABY.js → dist-57M6ZA7H.js} +25 -177
- package/dist/dist-57M6ZA7H.js.map +1 -0
- package/dist/{dist-7KVUIKJX.js → dist-J2IHD5T7.js} +37 -226
- package/dist/dist-J2IHD5T7.js.map +1 -0
- package/dist/index.js +1376 -1029
- package/dist/index.js.map +1 -1
- package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js → prepared-state-MQHD3M5F-Q27AZU53.js} +2 -2
- package/package.json +8 -6
- package/runtime/docker/Dockerfile.box +21 -26
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +37 -1
- package/runtime/docker/packages/ctl/dist/bin.cjs +40 -16
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +17 -6
- package/runtime/docker/packages/sandbox-docker/scripts/chromium-resolver +57 -0
- package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +2 -1
- package/runtime/e2b/agentbox-checkpoint-cleanup +52 -0
- package/runtime/e2b/agentbox-codex-hooks.json +68 -0
- package/runtime/e2b/agentbox-open +28 -0
- package/runtime/e2b/agentbox-setup-skill.md +233 -0
- package/runtime/e2b/agentbox-vnc-start +102 -0
- package/runtime/e2b/attach-helper.cjs +167 -0
- package/runtime/e2b/claude-managed-settings.json +116 -0
- package/runtime/e2b/ctl.cjs +23864 -0
- package/runtime/e2b/custom-system-CLAUDE.md +46 -0
- package/runtime/e2b/gh-shim +344 -0
- package/runtime/e2b/git-shim +131 -0
- package/runtime/e2b/scripts/build-template.sh +295 -0
- package/runtime/hetzner/agentbox-setup-skill.md +37 -1
- package/runtime/hetzner/agentbox-vnc-start +17 -6
- package/runtime/hetzner/claude-managed-settings.json +2 -1
- package/runtime/hetzner/ctl.cjs +40 -16
- package/runtime/relay/bin.cjs +297 -228
- package/runtime/vercel/agentbox-setup-skill.md +37 -1
- package/runtime/vercel/agentbox-vnc-start +17 -6
- package/runtime/vercel/claude-managed-settings.json +2 -1
- package/runtime/vercel/ctl.cjs +40 -16
- package/share/agentbox-setup/SKILL.md +37 -1
- package/share/host-skills/agentbox-info/SKILL.md +26 -34
- package/dist/chunk-2LF5YILI.js.map +0 -1
- package/dist/chunk-4NQXNQ53.js.map +0 -1
- package/dist/chunk-B4QG2MCW.js.map +0 -1
- package/dist/chunk-ECLLV5JH.js.map +0 -1
- package/dist/chunk-QYRK5H6Q.js.map +0 -1
- package/dist/chunk-R5XIDQFR.js.map +0 -1
- package/dist/chunk-SNTHHWKY.js.map +0 -1
- package/dist/dist-7KVUIKJX.js.map +0 -1
- package/dist/dist-JAN5VABY.js.map +0 -1
- package/dist/dist-OPIBZ7XM.js.map +0 -1
- /package/dist/{_cloud-attach-HJC672UR.js.map → _cloud-attach-GUBB5RH2.js.map} +0 -0
- /package/dist/{dist-OG6NW6SM.js.map → dist-4DPOL5A7.js.map} +0 -0
- /package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js.map → prepared-state-MQHD3M5F-Q27AZU53.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../packages/sandbox-hetzner/src/index.ts","../../../packages/sandbox-hetzner/src/backend.ts","../../../packages/sandbox-hetzner/src/cloud-init.ts","../../../packages/sandbox-hetzner/src/poll.ts","../../../packages/sandbox-hetzner/src/prepare.ts","../../../packages/sandbox-hetzner/src/ssh-key.ts","../../../packages/sandbox-hetzner/src/ssh-cli.ts","../../../packages/sandbox-hetzner/src/ssh-tunnel.ts"],"sourcesContent":["/**\n * The Hetzner Cloud VPS sandbox provider. A thin `CloudBackend` over OpenSSH\n * + the Hetzner Cloud REST API, composed via `@agentbox/sandbox-cloud`'s\n * `createCloudProvider` for the provider-agnostic scaffolding (workspace\n * seeding, ctl launch, state, relay polling, VNC, checkpoints).\n *\n * **Phase 2 status:** the SDK shim (auth + REST client + retry + firewall\n * + egress IP) is wired; `hetznerBackend` is a stub whose per-method bodies\n * throw `notImplemented` until Phase 4 plumbs in SSH ControlMaster +\n * provisioning. The provider is registered so `--provider hetzner` resolves\n * cleanly and the build is honest about what's missing.\n */\n\nimport type { Provider } from '@agentbox/core';\nimport { createCloudProvider } from '@agentbox/sandbox-cloud';\nimport { hetznerBackend, HETZNER_DEFAULT_BOX_IMAGE_REF } from './backend.js';\nimport { prepareHetznerProvider } from './prepare.js';\nimport { currentHetznerBaseFingerprintLive } from './prepared-state.js';\n\nconst cloudProvider = createCloudProvider(hetznerBackend, {\n defaultResources: { cpu: 2, memory: 4, disk: 40 },\n});\n\nexport const hetznerProvider: Provider = {\n ...cloudProvider,\n prepare: prepareHetznerProvider,\n baseFingerprint: () => currentHetznerBaseFingerprintLive(),\n};\n\nexport { hetznerBackend, HETZNER_DEFAULT_BOX_IMAGE_REF };\nexport { ensureHetznerEnvLoaded } from './env-loader.js';\nexport {\n ensureHetznerCredentials,\n readHetznerCredStatus,\n secretsPath,\n maskKey,\n type EnsureHetznerCredentialsOptions,\n type HetznerCredStatus,\n} from './credentials.js';\nexport {\n ensureHetznerBaseSnapshot,\n prepareHetzner,\n prepareHetznerProvider,\n type PrepareHetznerOptions,\n type PrepareHetznerResult,\n} from './prepare.js';\nexport { generateBoxCloudInit, generatePrepareCloudInit, type BoxCloudInitOptions, type PrepareCloudInitOptions } from './cloud-init.js';\nexport {\n RUNTIME_ASSETS,\n candidatesFor,\n resolveRuntimeAssets,\n type ResolvedAsset,\n type RuntimeAsset,\n} from './runtime-assets.js';\nexport { mintPrepareKey, mintSshKey, type MintedSshKey } from './ssh-key.js';\nexport {\n scpDownload,\n scpUpload,\n sshExec,\n sshOptArgs,\n waitForSsh,\n type SshExecOptions,\n type SshExecResult,\n type SshTargetArgs,\n} from './ssh-cli.js';\nexport { pollUntil, type PollOptions } from './poll.js';\nexport {\n currentHetznerBaseFingerprintLive,\n preparedStatePath,\n readPreparedState,\n writePreparedState,\n updatePreparedState,\n type PreparedBaseSnapshot,\n type PreparedHetznerState,\n} from './prepared-state.js';\nexport {\n makeHetznerClient,\n HetznerApiError,\n DEFAULT_HCLOUD_ENDPOINT,\n type CreateFirewallRequest,\n type CreateServerRequest,\n type HetznerAction,\n type HetznerClient,\n type HetznerFirewall,\n type HetznerFirewallRule,\n type HetznerImage,\n type HetznerServer,\n type HetznerServerStatus,\n type HetznerSshKey,\n} from './client.js';\nexport { detectEgressIp, type DetectEgressIpOptions } from './egress-ip.js';\nexport {\n createPerBoxFirewall,\n deletePerBoxFirewall,\n normalizeSourceCidr,\n sshOnlyInboundRule,\n syncFirewallSource,\n type CreateFirewallOptions,\n} from './firewall.js';\nexport { withHetznerRetry, isAttemptTimeout, isRetriable } from './retry.js';\n","/**\n * Hetzner `CloudBackend` — maps the provider-neutral cloud primitives onto\n * OpenSSH + the Hetzner Cloud REST API.\n *\n * Design rationale + the safety/tunneling model live in the plan at\n * `~/.claude/plans/how-to-safely-create-parallel-pebble.md` and the live\n * status doc at `docs/hertzner_backlog.md`. The short version:\n *\n * - 1:1 VPS-per-box. Each box gets a per-box ed25519 keypair (private key\n * never leaves the host) and a per-box Hetzner Cloud Firewall locked to\n * the host's egress IP.\n * - All comms (exec, file I/O, port forwards for bridge / web / VNC) flow\n * over one persistent `ssh` ControlMaster owned by `SshTunnelManager`.\n * - `previewUrl(port)` mints an `ssh -L 127.0.0.1:<localPort>:127.0.0.1:<remote>`\n * on demand. The cloud-provider scaffolding (`createCloudProvider`) then\n * decorates those URLs with Portless aliases for symmetric\n * `<box-name>.localhost` semantics — handled provider-side rather than\n * here so the backend stays focused on plumbing.\n * - Checkpoints map to Hetzner `create_image` snapshots; default no-pause,\n * opt-in pause via `createSnapshot({pause: true})`.\n */\n\nimport { existsSync } from 'node:fs';\nimport { rm, rename, mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { execa } from 'execa';\nimport { resolve as resolvePath } from 'node:path';\nimport type {\n CloudBackend,\n CloudExecResult,\n CloudFileEntry,\n CloudHandle,\n CloudPreviewUrl,\n CloudProvisionRequest,\n CloudSandboxSummary,\n CloudState,\n} from '@agentbox/core';\nimport { generateBoxCloudInit } from './cloud-init.js';\nimport {\n HetznerApiError,\n makeHetznerClient,\n type HetznerClient,\n type HetznerImage,\n type HetznerServer,\n type HetznerServerStatus,\n} from './client.js';\nimport { detectEgressIp } from './egress-ip.js';\nimport {\n createPerBoxFirewall,\n deletePerBoxFirewall,\n normalizeSourceCidr,\n} from './firewall.js';\nimport { pollUntil } from './poll.js';\nimport { readPreparedState } from './prepared-state.js';\nimport { ensureHetznerBaseSnapshot } from './prepare.js';\nimport { mintSshKey } from './ssh-key.js';\nimport { waitForSsh, sshOptArgs, type SshTargetArgs } from './ssh-cli.js';\nimport { SshTunnelManager, defaultBoxSshDir } from './ssh-tunnel.js';\nimport { withHetznerRetry } from './retry.js';\n\nexport const HETZNER_DEFAULT_BOX_IMAGE_REF = 'agentbox-base';\n\n/**\n * The cloud-provider scaffolding defaults `req.image` to `'agentbox/box:dev'`\n * (the docker provider's local image tag) when nothing else is specified.\n * That value is meaningless on Hetzner — we recognize it (alongside our own\n * sentinel + plain undefined) as \"use the hetzner base snapshot.\"\n */\nconst SCAFFOLDING_FALLBACK_IMAGE = 'agentbox/box:dev';\nconst VPS_USER = 'vscode';\nconst PROVISION_SSH_DEADLINE_MS = 5 * 60_000;\nconst ACTION_DEADLINE_MS = 5 * 60_000;\nconst SNAPSHOT_DEADLINE_MS = 20 * 60_000;\n// `cx22` was deprecated by Hetzner in early 2026; `cx23` is the drop-in\n// replacement with the same 2 vCPU / 4 GB / 40 GB shape on x86.\nconst HETZNER_DEFAULT_SERVER_TYPE = 'cx23';\nconst HETZNER_DEFAULT_LOCATION = 'nbg1';\n\n/** Module-level tunnel manager — one ControlMaster per box for this process. */\nconst tunnels = new SshTunnelManager();\n\n/**\n * Map Hetzner's per-server status onto the four-value `CloudState` everyone\n * else consumes. Transitional ones ('starting', 'rebuilding', …) are reported\n * as 'running' so callers don't ping-pong; 'off' maps to 'paused' because\n * Hetzner's stop = power off (vs Daytona's archive).\n */\nfunction mapState(s: HetznerServerStatus | string | undefined): CloudState {\n switch (s) {\n case 'running':\n return 'running';\n case 'starting':\n case 'initializing':\n case 'stopping':\n case 'migrating':\n case 'rebuilding':\n return 'running';\n case 'off':\n return 'paused';\n case 'deleting':\n case 'unknown':\n default:\n return 'missing';\n }\n}\n\nfunction client(): HetznerClient {\n return makeHetznerClient();\n}\n\nasync function getServerStrict(id: number): Promise<HetznerServer> {\n const s = await client().getServer(id);\n if (!s) {\n throw new Error(`hetzner: server ${String(id)} not found (already destroyed?)`);\n }\n return s;\n}\n\n/** Lookup an image by description (\"snapshot name\" in user-facing terms). */\nasync function findImageByDescription(c: HetznerClient, description: string): Promise<HetznerImage | null> {\n const all = await c.listImages({ type: 'snapshot' });\n return all.find((i) => i.description === description) ?? null;\n}\n\n/**\n * Resolve a `CloudProvisionRequest.image|snapshot` into a Hetzner image id.\n * Precedence: `req.snapshot` → `req.image`:\n * - the sentinel `agentbox-base` → load id from `hetzner-prepared.json`.\n * - a numeric string → use as-is.\n * - any other string → treated as a snapshot description (checkpoint name)\n * and looked up via the API.\n */\nasync function resolveImageId(c: HetznerClient, req: CloudProvisionRequest): Promise<number | string> {\n const ref = req.snapshot ?? req.image;\n if (!ref || ref === HETZNER_DEFAULT_BOX_IMAGE_REF || ref === SCAFFOLDING_FALLBACK_IMAGE) {\n await ensureHetznerBaseSnapshot();\n const state = readPreparedState();\n if (!state.base) {\n throw new Error(\n 'no Hetzner base snapshot found — run `agentbox prepare --provider hetzner` to bake one.',\n );\n }\n return state.base.imageId;\n }\n if (/^\\d+$/.test(ref)) {\n return Number.parseInt(ref, 10);\n }\n // Try snapshot-by-description first (checkpoints use that), then fall through\n // to Hetzner stock images (the user passed e.g. `ubuntu-24.04`).\n const snap = await findImageByDescription(c, ref);\n if (snap) return snap.id;\n return ref;\n}\n\n/**\n * Capture per-box state we need across method calls. Lives on disk under\n * `~/.agentbox/hetzner/boxes/<sandboxId>/` and is the source of truth for\n * the SSH identity + firewall id + cached IP.\n */\ninterface PerBoxState {\n dir: string;\n identity: string;\n knownHosts: string;\n firewallId?: number;\n firewallSource?: string;\n vpsIp?: string;\n}\n\nfunction perBoxDir(sandboxId: string): string {\n return resolvePath(defaultBoxSshDir(sandboxId), '..');\n}\n\nasync function ensurePerBoxState(sandboxId: string): Promise<PerBoxState> {\n const dir = perBoxDir(sandboxId);\n const sshDir = join(dir, 'ssh');\n await mkdir(sshDir, { recursive: true, mode: 0o700 });\n return {\n dir,\n identity: join(sshDir, 'id_ed25519'),\n knownHosts: join(sshDir, 'known_hosts'),\n };\n}\n\nfunction bashScript(s: string): string {\n // Always run remote commands under bash -lc so /etc/profile.d/agentbox.sh\n // (and the PATH prepend / DISPLAY / AGENT_BROWSER_* it sets) get sourced.\n return `bash -lc ${shellQuote(s)}`;\n}\n\nfunction shellQuote(s: string): string {\n return `'${s.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction buildSshTarget(state: PerBoxState, vpsIp: string, controlPath?: string): SshTargetArgs {\n return {\n host: vpsIp,\n user: VPS_USER,\n identity: state.identity,\n knownHosts: state.knownHosts,\n controlPath,\n };\n}\n\nasync function ensureTunnel(sandboxId: string, state: PerBoxState, vpsIp: string): Promise<void> {\n if (tunnels.has(sandboxId)) return;\n await tunnels.open({\n boxId: sandboxId,\n vpsHost: vpsIp,\n identity: state.identity,\n });\n}\n\n/**\n * Open the ControlMaster (if not already up) and return an SshTargetArgs\n * whose `controlPath` is set to the live master so exec/scp reuse it.\n */\nasync function ensureLiveTarget(sandboxId: string): Promise<{\n target: SshTargetArgs;\n state: PerBoxState;\n vpsIp: string;\n}> {\n const id = Number.parseInt(sandboxId, 10);\n if (!Number.isFinite(id)) {\n throw new Error(`hetzner: invalid sandboxId ${sandboxId}`);\n }\n const server = await getServerStrict(id);\n const vpsIp = server.public_net.ipv4?.ip;\n if (!vpsIp) {\n throw new Error(`hetzner: server ${String(id)} has no IPv4 address`);\n }\n const state = await ensurePerBoxState(sandboxId);\n if (!existsSync(state.identity)) {\n throw new Error(\n `hetzner: per-box SSH key missing for sandbox ${sandboxId} (expected at ${state.identity}). ` +\n `If this box was created by a different host, you'll need to re-create it on this host.`,\n );\n }\n await ensureTunnel(sandboxId, state, vpsIp);\n const controlPath = tunnels.controlPath(sandboxId);\n return { target: buildSshTarget(state, vpsIp, controlPath), state, vpsIp };\n}\n\nexport const hetznerBackend: CloudBackend = {\n name: 'hetzner',\n\n async provision(req: CloudProvisionRequest): Promise<CloudHandle> {\n const c = client();\n const onLog = req.onLog ?? (() => {});\n const progress = (s: string) => onLog(`hetzner: ${s}`);\n\n // 1. Gate on the base snapshot existing (lifts the Phase-2 placeholder).\n await ensureHetznerBaseSnapshot();\n const imageRef = await resolveImageId(c, req);\n\n // 2. Detect egress IP + normalize firewall source.\n const egressOverride =\n req.env?.AGENTBOX_HETZNER_FIREWALL_SOURCE ?? process.env.AGENTBOX_HETZNER_FIREWALL_SOURCE;\n const source = egressOverride\n ? normalizeSourceCidr(egressOverride)\n : `${await detectEgressIp({ onLog })}/32`;\n progress(`firewall source: ${source}`);\n\n // 3. Mint per-box SSH key into a temp dir keyed by a fresh uuid; we\n // rename it to `~/.agentbox/hetzner/boxes/<sandboxId>/ssh/` once the\n // server id is known.\n const stamp = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);\n const tempDir = resolvePath(\n process.env.HOME ?? process.cwd(),\n '.agentbox',\n 'hetzner',\n `pending-${stamp}`,\n 'ssh',\n );\n const key = await mintSshKey(tempDir, `agentbox-box-${req.name}-${stamp}`);\n\n let firewallId: number | null = null;\n let serverId: number | null = null;\n try {\n // 4. Firewall.\n const firewall = await createPerBoxFirewall(c, {\n name: `agentbox-${req.name}-${stamp}`,\n sourceCidr: source,\n labels: {\n 'agentbox.box': req.name,\n 'agentbox.role': 'box',\n },\n });\n firewallId = firewall.id;\n\n // 5. Cloud-init for the box: vscode user, pubkey, /etc/hosts alias,\n // optional box.env passthrough from req.env.\n const boxEnv: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.env ?? {})) {\n if (k.startsWith('AGENTBOX_')) boxEnv[k] = v;\n }\n const cloudInit = generateBoxCloudInit({\n sshPubkey: key.publicKey,\n boxName: req.name,\n boxEnv: Object.keys(boxEnv).length > 0 ? boxEnv : undefined,\n });\n\n // 6. Create the server.\n const serverType = (req.size && req.size.trim()) || HETZNER_DEFAULT_SERVER_TYPE;\n progress(\n `creating VPS '${req.name}' from image ${String(imageRef)} (${serverType} / ${HETZNER_DEFAULT_LOCATION})`,\n );\n const created = await withHetznerRetry(\n { method: 'createServer', retryOnAmbiguous: false, attemptTimeoutMs: 120_000 },\n () =>\n c.createServer({\n name: `agentbox-${req.name}-${stamp}`,\n server_type: serverType,\n image: imageRef,\n location: HETZNER_DEFAULT_LOCATION,\n user_data: cloudInit,\n firewalls: [{ firewall: firewall.id }],\n labels: {\n 'agentbox.managed': 'true',\n 'agentbox.role': 'box',\n 'agentbox.box': req.name,\n 'agentbox.firewall': String(firewall.id),\n },\n start_after_create: true,\n }),\n );\n serverId = created.server.id;\n const vpsIp = created.server.public_net.ipv4?.ip;\n if (!vpsIp) {\n throw new Error(`hetzner: server ${String(serverId)} came up without an IPv4 address`);\n }\n progress(`server ${String(serverId)} provisioned at ${vpsIp}; waiting for ssh`);\n\n // 7. Move the freshly-minted key from its temp location into the\n // sandboxId-keyed final dir, then rm the temp parent. We move file-\n // by-file rather than renaming the whole dir because `ensurePerBoxState`\n // had to mkdir the final dir before we knew the sandbox id (so the\n // tunnel manager + state-restoration paths can find it later).\n const sandboxId = String(serverId);\n const state = await ensurePerBoxState(sandboxId);\n await rename(key.privatePath, state.identity);\n await rename(key.publicPath, `${state.identity}.pub`);\n // Drop the now-empty temp dir + its `pending-XXX` parent.\n await rm(key.dir, { recursive: true, force: true });\n const pendingParent = resolvePath(key.dir, '..');\n await rm(pendingParent, { recursive: true, force: true });\n\n // 8. Wait for sshd to accept the new key.\n const up = await waitForSsh(buildSshTarget(state, vpsIp), PROVISION_SSH_DEADLINE_MS);\n if (!up) {\n throw new Error(`hetzner: ssh on ${vpsIp} did not come up within ${String(PROVISION_SSH_DEADLINE_MS / 1000)}s`);\n }\n\n // 9. Open ControlMaster.\n await ensureTunnel(sandboxId, state, vpsIp);\n progress('ssh up; ControlMaster open');\n\n // Agent credentials are seeded by `createCloudProvider`'s unified\n // post-provision step (`seedAgentVolumesIfFresh`) via `uploadFile` +\n // `exec` over the live ControlMaster — the symlinks baked into\n // install-box.sh route ~/.claude/.credentials.json etc. through to\n // `~/.agentbox-creds/<agent>/`.\n return { sandboxId };\n } catch (err) {\n // Cleanup on failure: server + firewall + temp ssh dir.\n if (serverId !== null) {\n progress(`cleanup — deleting server ${String(serverId)} after provision failure`);\n try {\n await c.deleteServer(serverId);\n } catch (cleanupErr) {\n onLog(\n `hetzner: WARN — failed to delete server ${String(serverId)}; check the Hetzner dashboard manually. ${\n cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)\n }`,\n );\n }\n }\n if (firewallId !== null) {\n try {\n await deletePerBoxFirewall(c, firewallId);\n } catch {\n // best-effort\n }\n }\n // Clean the temp ssh dir (or its renamed location if we got that far).\n try {\n if (existsSync(key.dir)) await rm(key.dir, { recursive: true, force: true });\n if (serverId !== null) {\n const finalDir = perBoxDir(String(serverId));\n if (existsSync(finalDir)) await rm(finalDir, { recursive: true, force: true });\n }\n } catch {\n // best-effort\n }\n throw err;\n }\n },\n\n async get(sandboxId: string): Promise<CloudHandle | null> {\n const id = Number.parseInt(sandboxId, 10);\n if (!Number.isFinite(id)) return null;\n const server = await client().getServer(id);\n return server ? { sandboxId } : null;\n },\n\n async list(): Promise<CloudSandboxSummary[]> {\n const servers = await client().listServers({ label_selector: 'agentbox.managed=true' });\n return servers.map((s) => ({\n sandboxId: String(s.id),\n name: s.labels['agentbox.box'] ?? s.name,\n createdAt: s.created,\n state: mapState(s.status),\n }));\n },\n\n async start(h: CloudHandle): Promise<void> {\n const id = Number.parseInt(h.sandboxId, 10);\n await client().powerOn(id);\n // The API reports `running` ~10-30s before sshd actually accepts\n // connections. Callers (the provider's `reEnsureCloudBox` relaunch) SSH-exec\n // immediately after start()/resume(), so wait for the API state AND for ssh\n // to be ready — otherwise the first exec hits `Connection refused` and the\n // daemons never get relaunched. Mirrors the provision flow's waitForSsh.\n await pollUntil(\n `server ${h.sandboxId} running`,\n async () => {\n const s = await client().getServer(id);\n return s?.status === 'running' ? s : null;\n },\n { deadlineMs: ACTION_DEADLINE_MS, intervalMs: 2_000, maxIntervalMs: 8_000 },\n );\n const server = await getServerStrict(id);\n const vpsIp = server.public_net.ipv4?.ip;\n if (!vpsIp) {\n throw new Error(`hetzner: server ${h.sandboxId} has no IPv4 address after start`);\n }\n const state = await ensurePerBoxState(h.sandboxId);\n const up = await waitForSsh(buildSshTarget(state, vpsIp), PROVISION_SSH_DEADLINE_MS);\n if (!up) {\n throw new Error(\n `hetzner: ssh on ${vpsIp} did not come up within ${String(PROVISION_SSH_DEADLINE_MS / 1000)}s after start`,\n );\n }\n },\n\n async stop(h: CloudHandle): Promise<void> {\n const id = Number.parseInt(h.sandboxId, 10);\n // Try graceful shutdown first; fall back to power-off after a short wait.\n try {\n await client().shutdown(id);\n await pollUntil(\n `server ${h.sandboxId} off`,\n async () => {\n const s = await client().getServer(id);\n return s?.status === 'off' ? s : null;\n },\n { deadlineMs: 60_000, intervalMs: 2_000, maxIntervalMs: 8_000 },\n );\n } catch {\n await client().powerOff(id);\n }\n await tunnels.close(h.sandboxId);\n },\n\n async pause(h: CloudHandle): Promise<void> {\n // Hetzner has no archive primitive. Pause ≡ stop.\n await this.stop(h);\n },\n\n async resume(h: CloudHandle): Promise<void> {\n await this.start(h);\n },\n\n async destroy(h: CloudHandle): Promise<void> {\n const id = Number.parseInt(h.sandboxId, 10);\n await tunnels.close(h.sandboxId);\n // Discover the per-box firewall via labels so we don't need to\n // round-trip through the server's `firewalls[]` (which is absent from\n // our typed slice anyway).\n const c = client();\n let firewallId: number | undefined;\n try {\n const server = await c.getServer(id);\n firewallId = server\n ? Number.parseInt(server.labels['agentbox.firewall'] ?? '', 10)\n : undefined;\n } catch {\n // ignore — we'll still try to delete the server\n }\n try {\n await c.deleteServer(id);\n } catch (err) {\n if (!(err instanceof HetznerApiError && (err.statusCode === 404 || err.code === 'not_found'))) {\n throw err;\n }\n }\n if (firewallId && Number.isFinite(firewallId)) {\n await deletePerBoxFirewall(c, firewallId);\n }\n // Clean the per-box ssh dir.\n const dir = perBoxDir(h.sandboxId);\n try {\n await rm(dir, { recursive: true, force: true });\n } catch {\n // best-effort\n }\n },\n\n async state(h: CloudHandle): Promise<CloudState> {\n const id = Number.parseInt(h.sandboxId, 10);\n const s = await client().getServer(id);\n return s ? mapState(s.status) : 'missing';\n },\n\n async exec(h, cmd, opts): Promise<CloudExecResult> {\n const { target } = await ensureLiveTarget(h.sandboxId);\n const argv = [\n ...sshOptArgs(target),\n `${target.user}@${target.host}`,\n bashScript(opts?.cwd ? `cd ${shellQuote(opts.cwd)} && ${cmd}` : cmd),\n ];\n const res = await execa('ssh', argv, {\n reject: false,\n timeout: opts?.attemptTimeoutMs ?? 120_000,\n env: { ...process.env, ...(opts?.env ?? {}) },\n });\n return {\n exitCode: typeof res.exitCode === 'number' ? res.exitCode : 1,\n stdout: typeof res.stdout === 'string' ? res.stdout : '',\n stderr: typeof res.stderr === 'string' ? res.stderr : '',\n };\n },\n\n async uploadFile(h, localPath, remotePath): Promise<void> {\n const { target } = await ensureLiveTarget(h.sandboxId);\n const argv = [\n ...sshOptArgs(target),\n localPath,\n `${target.user}@${target.host}:${remotePath}`,\n ];\n const res = await execa('scp', argv, { reject: false, timeout: 300_000 });\n if (res.exitCode !== 0) {\n throw new Error(`hetzner: scp upload failed (exit ${String(res.exitCode)}): ${res.stderr || ''}`);\n }\n },\n\n async downloadFile(h, remotePath, localPath): Promise<void> {\n const { target } = await ensureLiveTarget(h.sandboxId);\n const argv = [\n ...sshOptArgs(target),\n `${target.user}@${target.host}:${remotePath}`,\n localPath,\n ];\n const res = await execa('scp', argv, { reject: false, timeout: 300_000 });\n if (res.exitCode !== 0) {\n throw new Error(`hetzner: scp download failed (exit ${String(res.exitCode)}): ${res.stderr || ''}`);\n }\n },\n\n async listFiles(h, remoteDir): Promise<CloudFileEntry[]> {\n const res = await this.exec(\n h,\n // -L for nicer dir-detection on symlinks; `--printf` is non-portable so\n // use a small awk wrap that prints `<name>\\t<d|f>` per entry.\n `find ${shellQuote(remoteDir)} -mindepth 1 -maxdepth 1 -printf '%f\\\\t%y\\\\n'`,\n );\n if (res.exitCode !== 0) return [];\n return res.stdout\n .split(/\\r?\\n/)\n .filter((line) => line.length > 0)\n .map((line) => {\n const [name, kind] = line.split('\\t');\n return { name: name ?? line, isDir: kind === 'd' };\n });\n },\n\n async previewUrl(h, port): Promise<CloudPreviewUrl> {\n const { state, vpsIp } = await ensureLiveTarget(h.sandboxId);\n void state;\n void vpsIp;\n const localPort = await tunnels.forward(h.sandboxId, port);\n // Plain loopback URL — no preview-token here (SSH local forward is\n // already auth-gated by the tunnel itself). The cloud-provider layer\n // adds the Portless alias for symmetric `<box-name>.localhost` URLs.\n return { url: `http://127.0.0.1:${String(localPort)}` };\n },\n\n async signedPreviewUrl(h, port, _ttl): Promise<CloudPreviewUrl> {\n // SSH tunnels have no signed-URL primitive — they're already loopback-\n // only and the cloud-provider layer's bridge-token enforces auth. The\n // signed form is functionally equivalent to the unsigned one here.\n void _ttl;\n return this.previewUrl(h, port);\n },\n\n async refreshPreviewUrl(h, port): Promise<CloudPreviewUrl> {\n // Tear down the (likely dead) ControlMaster + every cached `-L` forward\n // for this box and re-open from scratch. Called by the host\n // CloudBoxPoller when ECONNREFUSED on the local port shows that the\n // master died (host sleep/wake, transient network blip). Without this\n // the poller would back off forever against a stale localPort.\n const { state, vpsIp } = await ensureLiveTarget(h.sandboxId);\n void state;\n void vpsIp;\n await tunnels.refresh({\n boxId: h.sandboxId,\n vpsHost: vpsIp,\n identity: state.identity,\n });\n const localPort = await tunnels.forward(h.sandboxId, port);\n return { url: `http://127.0.0.1:${String(localPort)}` };\n },\n\n async startInBoxPortless(h, opts): Promise<void> {\n // Bring up a `portless` proxy *inside the VPS* mirroring the host's\n // mode so `<boxName>.localhost:<P>` resolves to the same content on\n // both sides. The `portless` CLI is baked into the base snapshot by\n // install-box.sh:316. Idempotent — `portless proxy start` exits 0 if a\n // proxy is already running on the port. Best-effort: a failure here\n // just means the in-box symmetric URL won't resolve; the host URL\n // still works.\n //\n // Run as root (vscode has NOPASSWD sudo): portless's :443/:80 TLS\n // proxy self-elevates to root anyway, and its state lands in\n // /root/.portless. A subsequent `portless alias` from vscode would\n // write to /home/vscode/.portless and the proxy wouldn't see it —\n // the two state dirs are disjoint. Using sudo for both keeps them\n // pointed at the same `/root/.portless`.\n const tlsFlag = opts.tls ? '' : '--no-tls';\n const startCmd = `sudo portless proxy start ${tlsFlag} -p ${String(opts.proxyPort)}`.replace(/\\s+/g, ' ');\n const aliasCmd = `sudo portless alias ${shellQuote(opts.boxName)} ${String(opts.webPort)}`;\n await this.exec(h, `${startCmd}; ${aliasCmd}`);\n },\n\n async attachArgv(h): Promise<string[]> {\n const { target } = await ensureLiveTarget(h.sandboxId);\n // Reuse the ControlMaster via `-S <sock>` — no new auth handshake, no\n // SSH-token mint pressure (unlike Daytona). Callers append `-t '<cmd>'`\n // or similar in the `buildAttach` helper.\n return [\n 'ssh',\n ...sshOptArgs(target),\n `${target.user}@${target.host}`,\n ];\n },\n\n async createSnapshot(h, name): Promise<void> {\n const id = Number.parseInt(h.sandboxId, 10);\n const c = client();\n const { image } = await withHetznerRetry(\n { method: 'createImage', retryOnAmbiguous: false, attemptTimeoutMs: 120_000 },\n () =>\n c.createImage(id, {\n type: 'snapshot',\n description: name,\n labels: { 'agentbox.role': 'ckpt', 'agentbox.box': h.sandboxId },\n }),\n );\n await pollUntil(\n `image ${String(image.id)} availability`,\n async () => {\n const img = await c.getImage(image.id);\n return img?.status === 'available' ? img : null;\n },\n { deadlineMs: SNAPSHOT_DEADLINE_MS, intervalMs: 3_000, maxIntervalMs: 10_000 },\n );\n },\n\n async deleteSnapshot(name): Promise<void> {\n const c = client();\n const img = await findImageByDescription(c, name);\n if (!img) return;\n try {\n await c.deleteImage(img.id);\n } catch (err) {\n if (err instanceof HetznerApiError && (err.statusCode === 404 || err.code === 'not_found')) return;\n throw err;\n }\n },\n};\n\n/** Exposed for the CLI's `firewall sync` / `show` subcommands. */\nexport { tunnels as _hetznerTunnels };\n\n","/**\n * Cloud-init `#cloud-config` user-data generators.\n *\n * Two flavors:\n * - **prepare** (`generatePrepareCloudInit`): for the temporary VPS that\n * `prepareHetzner()` boots to bake the base snapshot. Just injects a\n * `root` SSH key — the install script does everything else over ssh.\n * - **box** (`generateBoxCloudInit`): for per-box VPSes provisioned from\n * the base snapshot. Injects the per-box ssh pubkey for `vscode`,\n * optionally writes `/etc/hosts` aliases and `box.env`.\n *\n * The cloud-init format is documented at https://cloudinit.readthedocs.io.\n * We emit it as a small hand-rolled YAML doc — using a full yaml lib would\n * be overkill for the handful of fields we touch (and pulls in the same\n * `yaml` dep `sandbox-cloud` already has, but we keep this package's dep\n * surface minimal).\n */\n\nexport interface PrepareCloudInitOptions {\n /** ed25519/rsa public key string (one line, OpenSSH format). */\n sshPubkey: string;\n}\n\n/**\n * Cloud-init for the temporary prepare VPS. We log in as `root` here (it's a\n * throwaway VPS that lives ~10–15 min) and run `install-box.sh` over ssh.\n * The install script then creates the `vscode` user, installs everything,\n * and writes the sshd hardening drop-in that disables root login — which\n * doesn't take effect until we reload sshd at the end of the script (the\n * orchestrator finishes its scp/ssh dance before that point).\n */\nexport function generatePrepareCloudInit(opts: PrepareCloudInitOptions): string {\n const pubkey = opts.sshPubkey.trim();\n return [\n '#cloud-config',\n '# AgentBox temporary prepare VPS — used by `agentbox prepare --provider hetzner`',\n '# to bake the base snapshot. SSH key is single-use and discarded on VPS destroy.',\n 'disable_root: false',\n 'ssh_pwauth: false',\n // Hetzner's Ubuntu 24.04 stock image enforces a first-login password\n // change for root. With key-based auth that path can't run (no TTY),\n // so sshd refuses with \"Password change required but no TTY available.\"\n // Telling cloud-init to NOT expire passwords + clearing root's expiry\n // via `passwd -d` removes the gate. Belt-and-braces: the chpasswd block\n // covers cloud-init's own users-and-groups run; the runcmd covers the\n // case where the image's pre-baked expiry survives cloud-init.\n 'chpasswd:',\n ' expire: false',\n 'users:',\n ' - name: root',\n ' lock_passwd: false',\n ' ssh_authorized_keys:',\n ` - ${yamlScalar(pubkey)}`,\n 'runcmd:',\n ' - [ passwd, -d, root ]',\n ' - [ chage, -E, \"-1\", -I, \"-1\", -M, \"99999\", root ]',\n ' - [ bash, -lc, \"echo agentbox-prepare-ready\" ]',\n '',\n ].join('\\n');\n}\n\nexport interface BoxCloudInitOptions {\n /** ed25519/rsa public key string (one line, OpenSSH format). */\n sshPubkey: string;\n /**\n * Box name. Used to write a `<boxName>.localhost → 127.0.0.1` /etc/hosts\n * entry so non-browser in-box clients (curl, fetch in Node) can hit the\n * symmetric Portless URL the host browser sees.\n */\n boxName: string;\n /**\n * Lines for `/etc/agentbox/box.env` — set as `KEY=VALUE` pairs. Cloud-init\n * `write_files` writes them as-is; no shell escaping is applied because\n * the file is sourced via `set -a; . /etc/agentbox/box.env; set +a`.\n */\n boxEnv?: Record<string, string>;\n}\n\n/**\n * Cloud-init for per-box VPSes provisioned from the base snapshot. The\n * base snapshot already has the `vscode` user, sshd hardening, agentbox-ctl,\n * etc. — this just injects the per-box key and any per-box config.\n */\nexport function generateBoxCloudInit(opts: BoxCloudInitOptions): string {\n const pubkey = opts.sshPubkey.trim();\n const lines: string[] = [\n '#cloud-config',\n `# AgentBox per-box VPS — box '${opts.boxName}'`,\n 'disable_root: true',\n 'ssh_pwauth: false',\n // Same first-login expiry guard as the prepare cloud-init — keeps\n // Hetzner's Ubuntu hardening from blocking our key-based vscode login.\n 'chpasswd:',\n ' expire: false',\n 'users:',\n ' - name: vscode',\n ' lock_passwd: false',\n ' sudo: ALL=(ALL) NOPASSWD:ALL',\n ' ssh_authorized_keys:',\n ` - ${yamlScalar(pubkey)}`,\n ];\n\n const writeFiles: string[] = [\n ' path: /etc/hosts',\n ' append: true',\n ` content: \"127.0.0.1 ${opts.boxName}.localhost\\\\n\"`,\n ];\n lines.push('write_files:');\n lines.push(' - ' + writeFiles[0]);\n for (let i = 1; i < writeFiles.length; i++) {\n lines.push(' ' + writeFiles[i]);\n }\n\n if (opts.boxEnv && Object.keys(opts.boxEnv).length > 0) {\n const envContent = Object.entries(opts.boxEnv)\n .map(([k, v]) => `${k}=${v}`)\n .join('\\\\n') + '\\\\n';\n lines.push(' - path: /etc/agentbox/box.env');\n lines.push(' permissions: \"0644\"');\n lines.push(` content: \"${envContent}\"`);\n }\n\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Quote a string as a YAML scalar. ssh pubkeys may contain spaces and `+`\n * characters; the safe move is to wrap in double quotes and escape `\"` /\n * `\\` if they appear (they don't for ed25519/rsa). Returns the value\n * already-quoted (so the caller embeds it after the `- ` prefix).\n */\nfunction yamlScalar(value: string): string {\n // No `\"` or `\\` in a valid OpenSSH pubkey, so a bare double-quote wrap is\n // safe; we still guard so a future caller doesn't surprise us.\n if (/[\"\\\\]/.test(value)) {\n return JSON.stringify(value);\n }\n return `\"${value}\"`;\n}\n","/**\n * Polling helpers — wait for a Hetzner Cloud resource to reach a desired\n * state. Reused across the prepare flow + Phase 4 backend lifecycle.\n *\n * Bounded by `deadlineMs`; intervals grow modestly (1s → 2s → 4s, capped)\n * so a fast-completing action is observed quickly without hammering the\n * API for slow ones.\n */\n\nexport interface PollOptions {\n deadlineMs?: number;\n /** Starting interval; doubles per attempt up to `maxIntervalMs`. */\n intervalMs?: number;\n maxIntervalMs?: number;\n /** Optional logger for each poll attempt — useful to stream progress. */\n onPoll?: (line: string) => void;\n}\n\n/**\n * Poll `check()` until it returns a non-null value or `deadlineMs` elapses.\n * Throws on timeout with the supplied `label` in the message.\n */\nexport async function pollUntil<T>(\n label: string,\n check: () => Promise<T | null | undefined>,\n opts: PollOptions = {},\n): Promise<T> {\n const deadline = Date.now() + (opts.deadlineMs ?? 5 * 60_000);\n const max = opts.maxIntervalMs ?? 10_000;\n let interval = opts.intervalMs ?? 1_000;\n let attempt = 0;\n while (true) {\n attempt += 1;\n const out = await check();\n if (out !== null && out !== undefined) return out;\n if (Date.now() >= deadline) {\n throw new Error(`hetzner: timed out waiting for ${label} after ${String(attempt)} attempts`);\n }\n opts.onPoll?.(`${label}: not ready yet (attempt ${String(attempt)}); polling again in ${String(interval)}ms`);\n await new Promise((r) => setTimeout(r, interval));\n interval = Math.min(interval * 2, max);\n }\n}\n","/**\n * `agentbox prepare --provider hetzner` — bake the per-org Hetzner base\n * snapshot. Mirrors the user request: \"for `agentbox prepare` (and first\n * time hetzner is used) start a VM and set it up and snapshot as base image\n * (there's no way to start a VPS from an existing dockerfile).\"\n *\n * Flow:\n * 1. Mint an ephemeral SSH keypair under\n * ~/.agentbox/hetzner/prepare-<ts>/.\n * 2. Detect the host's egress IP and create a firewall locked to it,\n * named `agentbox-prepare-<ts>`.\n * 3. Create a temp VPS (Ubuntu 24.04, `cx22` default) with cloud-init\n * injecting the pubkey for `root`.\n * 4. Poll until cloud-init + sshd come up.\n * 5. scp the runtime assets (install script + agentbox-ctl + helpers +\n * baked config files) into /tmp.\n * 6. Run `bash /tmp/agentbox-install.sh` over ssh; stream stdout to the\n * prepare log via the `onLog` callback.\n * 7. `create_image` snapshot of the VPS; poll until `available`.\n * 8. Delete the VPS + firewall.\n * 9. Persist the snapshot id into `~/.agentbox/hetzner-prepared.json`.\n *\n * Failure-mode discipline: each major step is wrapped in try/catch so the\n * temp VPS + firewall are *always* cleaned up on failure (the user must\n * never end up with a forgotten €4/mo VPS due to a prepare error).\n *\n * The user requested noisy logging — every BEGIN/END marker from the\n * install script is forwarded verbatim into the prepare log, plus our own\n * step boundaries from `progress()`.\n */\n\nimport { join } from 'node:path';\nimport type { Provider } from '@agentbox/core';\nimport { UserFacingError } from '@agentbox/core';\nimport { computeContextSha256, readCliStamp } from '@agentbox/sandbox-core';\nimport {\n stageClaudeStaticForUpload,\n stageCodexStaticForUpload,\n stageOpencodeStaticForUpload,\n type StageResult,\n} from '@agentbox/sandbox-cloud';\nimport { ensureHetznerCredentials } from './credentials.js';\nimport { detectEgressIp } from './egress-ip.js';\nimport {\n createPerBoxFirewall,\n deletePerBoxFirewall,\n normalizeSourceCidr,\n} from './firewall.js';\nimport { makeHetznerClient } from './client.js';\nimport { generatePrepareCloudInit } from './cloud-init.js';\nimport {\n preparedStatePath,\n readPreparedState,\n writePreparedState,\n} from './prepared-state.js';\nimport { pollUntil } from './poll.js';\nimport {\n findStagedCliRuntimeRoot,\n resolveRuntimeAssets,\n type ResolvedAsset,\n} from './runtime-assets.js';\nimport { mintPrepareKey } from './ssh-key.js';\nimport {\n scpUpload,\n sshExec,\n waitForSsh,\n type SshTargetArgs,\n} from './ssh-cli.js';\n\nexport interface PrepareHetznerOptions {\n name?: string;\n hostWorkspace?: string;\n /** Force re-bake even when `~/.agentbox/hetzner-prepared.json` has a usable base. */\n force?: boolean;\n /** Hetzner location (defaults to `nbg1`). */\n location?: string;\n /** Server type (defaults to `cx22` — 2 vCPU / 4 GB / 40 GB / ~€4/mo). */\n serverType?: string;\n /**\n * Override the firewall source CIDR. Defaults to auto-detected egress IP\n * via `detectEgressIp()` (with `/32` appended). Pass `'0.0.0.0/0'` for\n * the explicit-open opt-in. Passing a bare IP appends `/32` automatically.\n */\n firewallSource?: string;\n /** CLI runtime tree (set by the CLI to its dist neighbor). */\n cliRuntimeRoot?: string;\n /** Repo root for the dev fallback (defaults to `process.cwd()` walk). */\n repoRoot?: string;\n onLog?: (line: string) => void;\n}\n\nexport interface PrepareHetznerResult {\n snapshotName?: string;\n /** Hetzner image id (numeric) — also recorded in hetzner-prepared.json. */\n imageId?: number;\n}\n\n// `cx22` was deprecated by Hetzner in early 2026; `cx23` is the drop-in\n// replacement with the same 2 vCPU / 4 GB / 40 GB shape on x86. Users can\n// still override via `prepareHetzner({serverType: ...})`.\nconst TEMP_SERVER_TYPE_DEFAULT = 'cx23';\nconst TEMP_SERVER_LOCATION_DEFAULT = 'nbg1';\nconst PREPARE_SSH_DEADLINE_MS = 5 * 60_000;\nconst INSTALL_SCRIPT_TIMEOUT_MS = 30 * 60_000;\nconst SNAPSHOT_DEADLINE_MS = 20 * 60_000;\n\n/**\n * Bake the per-org Hetzner base snapshot. Resolves only after the image is\n * `available` and the temp VPS + firewall are gone. Persists `{base.imageId,\n * base.description, base.createdAt, base.installScriptSha256}` into\n * `~/.agentbox/hetzner-prepared.json`.\n */\nexport async function prepareHetzner(\n opts: PrepareHetznerOptions = {},\n): Promise<PrepareHetznerResult> {\n await ensureHetznerCredentials();\n const client = makeHetznerClient();\n const log = opts.onLog ?? (() => {});\n const progress = (step: string) => log(`prepare-hetzner: ${step}`);\n\n // Skip-fast: if a base snapshot is already recorded *and* its image is\n // still on Hetzner *and* the build-context fingerprint hasn't changed *and*\n // --force was not passed, return the existing record.\n const existingState = readPreparedState();\n // Prefer an explicit override; otherwise auto-detect the published-CLI\n // staged runtime tree by inspecting where this module was loaded from.\n const assets = resolveRuntimeAssets({\n cliRuntimeRoot: opts.cliRuntimeRoot ?? findStagedCliRuntimeRoot(),\n repoRoot: opts.repoRoot,\n });\n // Fingerprint = hash of every asset we scp into the prepare VPS. Keyed on\n // logical name (stable across staged-vs-monorepo layouts) so two CLIs with\n // the same staged tree produce the same hash.\n const contextSha = await computeContextSha256(\n assets.map((a) => ({ rel: a.name, abs: a.localPath })),\n );\n\n if (!opts.force && existingState.base) {\n const remote = await client\n .getImage(existingState.base.imageId)\n .catch(() => null);\n if (remote && existingState.base.contextSha256 === contextSha) {\n progress(\n `base snapshot ${String(existingState.base.imageId)} already exists (fingerprint ${contextSha.slice(0, 12)} matches); skipping rebuild (pass --force to override)`,\n );\n return {\n snapshotName: existingState.base.description,\n imageId: existingState.base.imageId,\n };\n }\n if (!remote) {\n progress(`recorded base snapshot ${String(existingState.base.imageId)} is gone on Hetzner; rebuilding`);\n } else {\n progress(\n `build context changed (was ${existingState.base.contextSha256?.slice(0, 12) ?? '<none>'}, now ${contextSha.slice(0, 12)}); rebuilding base snapshot`,\n );\n }\n }\n\n // 1. Mint ephemeral key + detect egress IP in parallel.\n progress('minting ephemeral ssh key');\n const key = await mintPrepareKey();\n let firewallId: number | null = null;\n let serverId: number | null = null;\n try {\n progress('detecting host egress IP');\n const source = opts.firewallSource\n ? normalizeSourceCidr(opts.firewallSource)\n : `${await detectEgressIp({ onLog: log })}/32`;\n\n // 2. Create per-prepare firewall.\n const stamp = Date.now().toString(36);\n const firewallName = `agentbox-prepare-${stamp}`;\n progress(`creating firewall ${firewallName} (source ${source})`);\n const firewall = await createPerBoxFirewall(client, {\n name: firewallName,\n sourceCidr: source,\n labels: { 'agentbox.role': 'prepare' },\n });\n firewallId = firewall.id;\n\n // 3. Create temp VPS.\n const serverName = `agentbox-prepare-${stamp}`;\n const cloudInit = generatePrepareCloudInit({ sshPubkey: key.publicKey });\n progress(`creating temp VPS ${serverName} (${opts.serverType ?? TEMP_SERVER_TYPE_DEFAULT} / ${opts.location ?? TEMP_SERVER_LOCATION_DEFAULT})`);\n const created = await client.createServer({\n name: serverName,\n server_type: opts.serverType ?? TEMP_SERVER_TYPE_DEFAULT,\n image: 'ubuntu-24.04',\n location: opts.location ?? TEMP_SERVER_LOCATION_DEFAULT,\n user_data: cloudInit,\n firewalls: [{ firewall: firewall.id }],\n labels: { 'agentbox.managed': 'true', 'agentbox.role': 'prepare' },\n start_after_create: true,\n });\n serverId = created.server.id;\n const ip = created.server.public_net.ipv4?.ip;\n if (!ip) {\n throw new Error('hetzner: temp VPS came up without an IPv4 address');\n }\n\n // 4. Wait for sshd.\n progress(`waiting for ssh on ${ip} (deadline ${String(PREPARE_SSH_DEADLINE_MS / 1000)}s)`);\n const sshTarget: SshTargetArgs = {\n host: ip,\n user: 'root',\n identity: key.privatePath,\n knownHosts: join(key.dir, 'known_hosts'),\n };\n const up = await waitForSsh(sshTarget, PREPARE_SSH_DEADLINE_MS);\n if (!up) {\n throw new Error(`hetzner: ssh on ${ip} did not come up within ${String(PREPARE_SSH_DEADLINE_MS / 1000)}s`);\n }\n progress('ssh up — scp\\'ing runtime assets');\n\n // 5. scp every asset into /tmp/ **sequentially**. Parallel uploads\n // through 10 fresh ssh connections trip sshd's MaxStartups (10:30:100\n // default) on a freshly-booted VPS — surviving connections look fine\n // but some randomly write 0 bytes to the destination. The sequential\n // form is plenty fast (each file is small, total ~1MB).\n for (const asset of assets) {\n const remote = `/tmp/${asset.remoteBasename}`;\n log(`prepare-hetzner: scp ${asset.name} -> ${remote}`);\n await scpUpload(sshTarget, asset.localPath, remote);\n if (asset.remoteMode !== undefined) {\n const modeOctal = asset.remoteMode.toString(8);\n await sshExec(sshTarget, `chmod ${modeOctal} ${remote}`);\n }\n }\n\n // 6. Run the install script. We trace via `bash -x` and tee the full\n // output to /var/log/agentbox/install.log on the VPS so the trace\n // survives into the snapshot — handy when diagnosing a step that ran\n // (or didn't) deep inside the install. Stream stdout/stderr through\n // `onLog` so `prepare.log` shows the BEGIN/END markers in real time.\n // `set -o pipefail` so the pipe's exit code is bash's, not tee's.\n progress('running install-box.sh on temp VPS (this takes ~5-15 min)');\n const installRes = await sshExec(\n sshTarget,\n `sudo mkdir -p /var/log/agentbox && set -o pipefail && bash -x /tmp/agentbox-install.sh 2>&1 | sudo tee /var/log/agentbox/install.log`,\n {\n timeoutMs: INSTALL_SCRIPT_TIMEOUT_MS,\n onLine: (line) => log(`[install] ${line}`),\n },\n );\n if (installRes.exitCode !== 0) {\n throw new Error(\n `install-box.sh failed on temp VPS (exit ${String(installRes.exitCode)})\\n` +\n `Last stderr: ${installRes.stderr.slice(-500) || '(empty)'}\\n` +\n `The full trace was preserved at /var/log/agentbox/install.log inside any box made from the resulting snapshot.`,\n );\n }\n progress('install script complete');\n\n // 6b. Stage host agent static config (~/.claude plugins/skills/settings/\n // _claude.json, ~/.codex config + prompts, ~/.local/share/opencode), scp\n // each tarball, extract into /home/vscode/ as the `vscode` user. Mirrors\n // the Daytona bake step (`Image.addLocalFile` + `Image.runCommands`),\n // adapted for our ssh+scp model. Without this, the in-box claude/codex/\n // opencode boot with no plugins, no skills, no settings, and prompt the\n // user to log in fresh on every box.\n progress('staging host agent static config');\n const stagings: Array<{ kind: 'claude' | 'codex' | 'opencode'; tar: StageResult; dest: string }> = [];\n try {\n const claudeTar = await stageClaudeStaticForUpload({ hostWorkspace: opts.hostWorkspace });\n for (const w of claudeTar.warnings) log(`prepare-hetzner: ${w}`);\n if (claudeTar.tarballPath) stagings.push({ kind: 'claude', tar: claudeTar, dest: '/home/vscode/.claude' });\n else await claudeTar.cleanup();\n\n const codexTar = await stageCodexStaticForUpload();\n for (const w of codexTar.warnings) log(`prepare-hetzner: ${w}`);\n if (codexTar.tarballPath) stagings.push({ kind: 'codex', tar: codexTar, dest: '/home/vscode/.codex' });\n else await codexTar.cleanup();\n\n const opencodeTar = await stageOpencodeStaticForUpload();\n for (const w of opencodeTar.warnings) log(`prepare-hetzner: ${w}`);\n if (opencodeTar.tarballPath) stagings.push({ kind: 'opencode', tar: opencodeTar, dest: '/home/vscode/.local/share/opencode' });\n else await opencodeTar.cleanup();\n\n for (const s of stagings) {\n const remote = `/tmp/agentbox-${s.kind}-static.tar.gz`;\n log(`prepare-hetzner: scp ${s.kind} static (${s.tar.tarballPath}) -> ${remote}`);\n await scpUpload(sshTarget, s.tar.tarballPath as string, remote);\n // Extract as vscode so the files land owned by uid 1000. The dir\n // already exists (created by the install script's credential-pivot\n // step) — extract into it, don't replace it.\n const extractCmd =\n `sudo -u vscode mkdir -p ${s.dest} && ` +\n `sudo -u vscode tar -xzf ${remote} -C ${s.dest} --no-same-permissions --no-same-owner -m && ` +\n `rm -f ${remote}`;\n const r = await sshExec(sshTarget, extractCmd, { onLine: (line) => log(`[stage:${s.kind}] ${line}`) });\n if (r.exitCode !== 0) {\n throw new Error(\n `prepare-hetzner: ${s.kind} static extract failed (exit ${String(r.exitCode)}): ${r.stderr.slice(-300)}`,\n );\n }\n progress(`baked ${s.kind} static config into snapshot`);\n }\n } finally {\n for (const s of stagings) await s.tar.cleanup();\n }\n\n // 7. Snapshot.\n const description = opts.name ?? `agentbox-base-${stamp}`;\n progress(`creating snapshot '${description}' from VPS ${String(serverId)}`);\n const snap = await client.createImage(serverId, {\n type: 'snapshot',\n description,\n labels: { 'agentbox.role': 'base', 'agentbox.schema': '1' },\n });\n progress(`snapshot create requested (image id ${String(snap.image.id)}); polling until available`);\n const ready = await pollUntil(\n `image ${String(snap.image.id)} availability`,\n async () => {\n const img = await client.getImage(snap.image.id);\n if (!img) return null;\n if (img.status === 'available') return img;\n return null;\n },\n { deadlineMs: SNAPSHOT_DEADLINE_MS, intervalMs: 3_000, maxIntervalMs: 10_000, onPoll: (l) => log(`prepare-hetzner: ${l}`) },\n );\n\n // 8. Persist before tearing down — if the cleanup fails we still know\n // about the new snapshot.\n progress('persisting hetzner-prepared.json');\n const state = readPreparedState();\n const cliStamp = readCliStamp();\n state.base = {\n imageId: ready.id,\n description: ready.description,\n createdAt: new Date().toISOString(),\n contextSha256: contextSha,\n cliVersion: cliStamp.cliVersion,\n cliCommit: cliStamp.cliCommit,\n };\n writePreparedState(state);\n log(`prepare-hetzner: wrote ${preparedStatePath()}`);\n\n // 9. Cleanup: delete server first (cleanly detaches from firewall),\n // then the firewall.\n progress(`deleting temp VPS ${String(serverId)}`);\n await client.deleteServer(serverId);\n serverId = null;\n progress(`deleting per-prepare firewall ${String(firewallId)}`);\n await deletePerBoxFirewall(client, firewallId);\n firewallId = null;\n\n progress(`prepare complete — base snapshot ${String(ready.id)} (${ready.description})`);\n return { snapshotName: ready.description, imageId: ready.id };\n } catch (err) {\n // Failure cleanup — best-effort. Always try to delete the VPS first\n // (it costs €4/mo if left running). Surface the original error in any\n // case.\n if (serverId !== null) {\n log(`prepare-hetzner: cleanup — deleting temp VPS ${String(serverId)} after failure`);\n try {\n await client.deleteServer(serverId);\n } catch (cleanupErr) {\n log(\n `prepare-hetzner: WARN — failed to delete temp VPS ${String(serverId)}; check the Hetzner dashboard manually. ${\n cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)\n }`,\n );\n }\n }\n if (firewallId !== null) {\n log(`prepare-hetzner: cleanup — deleting per-prepare firewall ${String(firewallId)} after failure`);\n try {\n await deletePerBoxFirewall(client, firewallId);\n } catch (cleanupErr) {\n log(\n `prepare-hetzner: WARN — failed to delete firewall ${String(firewallId)}; check the Hetzner dashboard manually. ${\n cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)\n }`,\n );\n }\n }\n throw err;\n } finally {\n await key.cleanup();\n }\n}\n\n/**\n * Provider-level binding used by the CLI's `prepare` command. Matches the\n * shape of `daytonaProvider.prepare`.\n */\nexport const prepareHetznerProvider: NonNullable<Provider['prepare']> = (req) =>\n prepareHetzner({\n name: req.name,\n hostWorkspace: req.hostWorkspace ?? process.cwd(),\n force: req.force,\n onLog: req.onLog,\n });\n\n/**\n * First-use gate. If no base snapshot is recorded in\n * `~/.agentbox/hetzner-prepared.json`, throws an actionable error pointing\n * at `agentbox prepare --provider hetzner`.\n *\n * This is called by `backend.provision()` (lazily, from Phase 4 onward) so\n * `agentbox prepare --provider hetzner` itself can run without tripping\n * the gate.\n *\n * Phase 4 will widen this to also re-check the image is still on Hetzner\n * (404 → retrigger prepare prompt). For now it just gates on the local\n * record so the build is honest about the failure mode.\n */\nexport async function ensureHetznerBaseSnapshot(): Promise<void> {\n const state = readPreparedState();\n if (state.base !== undefined) return;\n throw new UserFacingError(\n 'no Hetzner base snapshot found.\\n' +\n 'Run `agentbox prepare --provider hetzner` first (Hetzner cannot build images from a Dockerfile,\\n' +\n 'so the base snapshot is a one-time prerequisite for cloud boxes on this backend).',\n );\n}\n\nexport type { ResolvedAsset };\n","/**\n * Per-box (and per-prepare-run) SSH key minting.\n *\n * AgentBox mints a fresh ed25519 keypair per box at provision time. The\n * private key never leaves the host; the public key is shipped to the VPS\n * via cloud-init `users:` (NOT the Hetzner SSH-keys-import API, which\n * would make the same pubkey available to attach to other VPSes the user\n * provisions — see the plan's §\"Key & key-lifecycle hygiene\").\n *\n * Storage layout (per the plan):\n * ~/.agentbox/boxes/<box-id>/ssh/\n * id_ed25519 (private, 0600)\n * id_ed25519.pub (public, 0644)\n * known_hosts (per-box, populated post-first-connect)\n * control.sock (ssh ControlMaster socket — created at runtime)\n *\n * For the temp prepare VPS we use a parallel path:\n * ~/.agentbox/hetzner/prepare-<timestamp>/\n * deleted after the snapshot completes.\n */\n\nimport { mkdir, readFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { execa } from 'execa';\n\nexport interface MintedSshKey {\n /** Directory holding the key files. */\n dir: string;\n /** Absolute path to the private key. */\n privatePath: string;\n /** Absolute path to the public key. */\n publicPath: string;\n /** Public key contents (one OpenSSH-format line). */\n publicKey: string;\n}\n\n/**\n * Mint a fresh ed25519 keypair into `targetDir/id_ed25519` (+ `.pub`). The\n * directory is created if missing. Throws if the private key already exists\n * — callers handle reuse explicitly (we don't silently overwrite).\n *\n * `comment` is embedded in the public key (the `agentbox/<box-id>` tag) so\n * the key is identifiable in `~/.ssh/authorized_keys` on a forensic look.\n */\nexport async function mintSshKey(targetDir: string, comment: string): Promise<MintedSshKey> {\n const dir = resolve(targetDir);\n const priv = join(dir, 'id_ed25519');\n const pub = `${priv}.pub`;\n await mkdir(dir, { recursive: true, mode: 0o700 });\n\n // `ssh-keygen -N ''` for no passphrase; `-q` to suppress the random art.\n // Caller is responsible for ensuring the dir is fresh — if `priv` already\n // exists, ssh-keygen would prompt to overwrite (and we don't pipe stdin so\n // it would hang). `mintPrepareKey` creates a fresh dir per call; the\n // per-box minter in backend.ts uses a fresh stamp directory too.\n await execa(\n 'ssh-keygen',\n ['-t', 'ed25519', '-N', '', '-C', comment, '-f', priv, '-q'],\n { stdio: 'pipe' },\n );\n\n const publicKey = (await readFile(pub, 'utf8')).trim();\n return { dir, privatePath: priv, publicPath: pub, publicKey };\n}\n\n/**\n * Mint a temporary keypair for the prepare orchestrator. Returns the same\n * shape as `mintSshKey` plus a `cleanup()` that rm -rf's the directory.\n * The caller is expected to call `cleanup()` in a `finally` block.\n */\nexport async function mintPrepareKey(): Promise<MintedSshKey & { cleanup: () => Promise<void> }> {\n const root = resolve(homedirOrCwd(), '.agentbox', 'hetzner', `prepare-${Date.now().toString(36)}`);\n const key = await mintSshKey(root, `agentbox-prepare-${new Date().toISOString().replace(/[:.]/g, '-')}`);\n return {\n ...key,\n cleanup: async () => {\n try {\n const { rm } = await import('node:fs/promises');\n await rm(dirname(key.privatePath), { recursive: true, force: true });\n } catch {\n // best-effort\n }\n },\n };\n}\n\nfunction homedirOrCwd(): string {\n try {\n // Lazy require so this module is import-safe even if `os` is shimmed\n // away in some weird bundle environment.\n return process.env.HOME ?? process.cwd();\n } catch {\n return process.cwd();\n }\n}\n","/**\n * Thin wrappers around the system `ssh` / `scp` binaries used during\n * `prepareHetzner()` (and Phase 4's SshTunnelManager).\n *\n * These helpers compose flags that suppress the usual interactive prompts\n * (`StrictHostKeyChecking=accept-new`, `UserKnownHostsFile=<per-box file>`)\n * and bind to the per-key/per-box paths.\n *\n * We shell out to the system OpenSSH rather than a JS SSH library because:\n * - The provider matrix already depends on system `ssh` for `agentbox\n * shell` against Docker boxes (`docker exec` doesn't go through ssh,\n * but `agentbox open`'s sshfs flow does).\n * - OpenSSH's ControlMaster + dynamic port forwarding is exactly the\n * primitive Phase 4 needs, and is hard to replicate in pure JS.\n * - No native-dep crutch and no surprise binary sizes.\n */\n\nimport { execa, type ResultPromise } from 'execa';\n\nexport interface SshTargetArgs {\n /** VPS IP (or DNS name) — passed to ssh as user@host. */\n host: string;\n /** Remote user. Hetzner stock images come up with `root`; baked snapshots use `vscode`. */\n user: string;\n /** Absolute path to the per-box/per-prepare private key. */\n identity: string;\n /** Absolute path to the per-box known_hosts file. */\n knownHosts: string;\n /** Optional ControlMaster socket (set during Phase 4 SshTunnelManager). */\n controlPath?: string;\n /** Extra `-o key=value` settings (e.g. ConnectTimeout for prepare polling). */\n options?: Record<string, string>;\n}\n\n/** Compose the `-o … -i … -o UserKnownHostsFile=…` flags for ssh/scp. */\nexport function sshOptArgs(target: SshTargetArgs): string[] {\n const out: string[] = [\n '-i', target.identity,\n '-o', 'StrictHostKeyChecking=accept-new',\n '-o', `UserKnownHostsFile=${target.knownHosts}`,\n '-o', 'GlobalKnownHostsFile=/dev/null',\n '-o', 'BatchMode=yes',\n '-o', 'LogLevel=ERROR',\n ];\n if (target.controlPath) {\n out.push('-o', `ControlPath=${target.controlPath}`);\n }\n for (const [k, v] of Object.entries(target.options ?? {})) {\n out.push('-o', `${k}=${v}`);\n }\n return out;\n}\n\nexport interface SshExecOptions {\n /** Stream stdout/stderr line-by-line into this callback. */\n onLine?: (line: string) => void;\n /** Pipe extra env into the remote shell. */\n env?: Record<string, string>;\n /** Per-command wall-clock cap (ms). */\n timeoutMs?: number;\n}\n\nexport interface SshExecResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Run a one-shot command on the target VPS over ssh. Returns the exit code\n * + captured stdout/stderr; non-zero exits do NOT throw — callers decide\n * what to do with them.\n */\nexport async function sshExec(\n target: SshTargetArgs,\n remoteCmd: string,\n opts: SshExecOptions = {},\n): Promise<SshExecResult> {\n const argv = [\n ...sshOptArgs(target),\n `${target.user}@${target.host}`,\n remoteCmd,\n ];\n const child = execa('ssh', argv, {\n reject: false,\n timeout: opts.timeoutMs,\n env: { ...process.env, ...opts.env },\n stdio: opts.onLine ? ['ignore', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],\n }) as ResultPromise;\n\n if (opts.onLine) {\n const handle = (chunk: Buffer | string) => {\n const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');\n for (const line of text.split(/\\r?\\n/)) {\n if (line.length > 0) opts.onLine?.(line);\n }\n };\n child.stdout?.on('data', handle);\n child.stderr?.on('data', handle);\n }\n\n const res = await child;\n return {\n exitCode: typeof res.exitCode === 'number' ? res.exitCode : 1,\n stdout: typeof res.stdout === 'string' ? res.stdout : '',\n stderr: typeof res.stderr === 'string' ? res.stderr : '',\n };\n}\n\n/** Copy a local file to the target VPS via `scp`. Throws on non-zero exit. */\nexport async function scpUpload(\n target: SshTargetArgs,\n localPath: string,\n remotePath: string,\n opts: SshExecOptions = {},\n): Promise<void> {\n const argv = [\n ...sshOptArgs(target),\n localPath,\n `${target.user}@${target.host}:${remotePath}`,\n ];\n const res = await execa('scp', argv, {\n reject: false,\n timeout: opts.timeoutMs,\n });\n if (res.exitCode !== 0) {\n throw new Error(\n `scp upload failed (exit ${String(res.exitCode)}): ${localPath} → ${remotePath}\\n${res.stderr ?? ''}`,\n );\n }\n}\n\n/** Copy a remote file to the host via `scp`. Throws on non-zero exit. */\nexport async function scpDownload(\n target: SshTargetArgs,\n remotePath: string,\n localPath: string,\n opts: SshExecOptions = {},\n): Promise<void> {\n const argv = [\n ...sshOptArgs(target),\n `${target.user}@${target.host}:${remotePath}`,\n localPath,\n ];\n const res = await execa('scp', argv, {\n reject: false,\n timeout: opts.timeoutMs,\n });\n if (res.exitCode !== 0) {\n throw new Error(\n `scp download failed (exit ${String(res.exitCode)}): ${remotePath} → ${localPath}\\n${res.stderr ?? ''}`,\n );\n }\n}\n\n/**\n * Poll the target until ssh succeeds (or `deadlineMs` elapses). Used by the\n * prepare orchestrator after `createServer` to wait for cloud-init to bring\n * sshd up. Returns true on success, false on timeout — callers throw with\n * appropriate context.\n */\nexport async function waitForSsh(\n target: SshTargetArgs,\n deadlineMs: number,\n intervalMs = 5_000,\n): Promise<boolean> {\n const stop = Date.now() + deadlineMs;\n while (Date.now() < stop) {\n const res = await sshExec(\n { ...target, options: { ...target.options, ConnectTimeout: '5' } },\n 'true',\n { timeoutMs: 10_000 },\n );\n if (res.exitCode === 0) return true;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n return false;\n}\n","/**\n * `SshTunnelManager` — one persistent `ssh` ControlMaster per box, plus\n * dynamic `-L` port forwards minted on demand.\n *\n * This is the load-bearing piece that makes the Hetzner provider's preview-URL\n * + exec/file I/O paths work without paying the SSH handshake on every call.\n * Design rationale lives in `~/.claude/plans/how-to-safely-create-parallel-pebble.md`\n * §\"SSH tunnel design (host side)\" and is recapped here.\n *\n * Layout:\n *\n * ~/.agentbox/boxes/<box-id>/ssh/\n * id_ed25519 per-box private key (0600)\n * id_ed25519.pub per-box public key (0644)\n * known_hosts per-box host-key pinning\n * control.sock ssh ControlMaster socket (created at runtime, removed by `-O exit`)\n *\n * Lifecycle:\n *\n * - open() → spawn `ssh -fNT -M -S control.sock -i id_ed25519 vscode@vps` once per box.\n * - forward() → `ssh -O forward -L 127.0.0.1:<localPort>:127.0.0.1:<remotePort> -S control.sock dummy`.\n * Picks a free local port from the ephemeral range.\n * Idempotent per (boxId, remotePort) — returns the cached\n * localPort on a repeated call.\n * - unforward() → `ssh -O cancel -L 127.0.0.1:<localPort>:127.0.0.1:<remotePort> -S control.sock dummy`.\n * - close() → `ssh -O exit -S control.sock dummy`; removes the socket.\n *\n * Thread-safety: a single AgentBox process owns one manager per CLI\n * invocation. Concurrent `forward()` calls for the same (boxId, remotePort)\n * race-resolve to the same localPort because the cache is checked first and\n * `ssh -O forward` is idempotent on the SSH side.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir, rm } from 'node:fs/promises';\nimport { createServer } from 'node:net';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { execa } from 'execa';\n\nconst HOST = '127.0.0.1';\nconst EPHEMERAL_MIN = 49152;\nconst EPHEMERAL_MAX = 65535;\n\nexport interface SshTunnelOpenOptions {\n boxId: string;\n /** VPS IP/host. */\n vpsHost: string;\n /** Remote user — `vscode` for boxes baked from `install-box.sh`. */\n vpsUser?: string;\n /** Absolute path to the private key. */\n identity: string;\n /** Override `~/.agentbox/boxes/<box-id>/ssh/` (tests inject this). */\n boxSshDir?: string;\n /** Override the connect-timeout (default 10s — fast-fail on a dropped firewall rule). */\n connectTimeoutSeconds?: number;\n}\n\nexport interface PortForward {\n localPort: number;\n remotePort: number;\n}\n\ninterface BoxTunnel {\n controlPath: string;\n vpsHost: string;\n vpsUser: string;\n identity: string;\n boxSshDir: string;\n // remotePort → localPort. Used so repeated `forward()` calls for the same\n // remote port return the same local port without re-asking sshd.\n forwards: Map<number, number>;\n}\n\nexport class SshTunnelManager {\n private boxes = new Map<string, BoxTunnel>();\n\n /**\n * Open the ControlMaster for `boxId`. Idempotent: if a master is already\n * up for this box (socket exists + responsive), no-op. Otherwise spawn a\n * fresh `ssh -fNT -M` and wait for the socket to appear.\n */\n async open(opts: SshTunnelOpenOptions): Promise<void> {\n const boxSshDir = opts.boxSshDir ?? defaultBoxSshDir(opts.boxId);\n await mkdir(boxSshDir, { recursive: true, mode: 0o700 });\n const controlPath = join(boxSshDir, 'control.sock');\n const knownHosts = join(boxSshDir, 'known_hosts');\n const user = opts.vpsUser ?? 'vscode';\n const tunnel: BoxTunnel = {\n controlPath,\n vpsHost: opts.vpsHost,\n vpsUser: user,\n identity: opts.identity,\n boxSshDir,\n forwards: new Map(),\n };\n\n // Reuse an existing live master if the socket already responds.\n if (existsSync(controlPath) && (await this.isAlive(controlPath))) {\n this.boxes.set(opts.boxId, tunnel);\n return;\n }\n // Stale socket from a prior crashed process — remove before re-opening.\n if (existsSync(controlPath)) {\n await rm(controlPath, { force: true });\n }\n\n const connectTimeout = opts.connectTimeoutSeconds ?? 10;\n const argv = [\n '-fNT',\n '-M',\n '-S', controlPath,\n '-i', opts.identity,\n '-o', 'StrictHostKeyChecking=accept-new',\n '-o', `UserKnownHostsFile=${knownHosts}`,\n '-o', 'GlobalKnownHostsFile=/dev/null',\n '-o', 'BatchMode=yes',\n '-o', 'LogLevel=ERROR',\n '-o', 'ExitOnForwardFailure=yes',\n '-o', 'ServerAliveInterval=30',\n '-o', 'ServerAliveCountMax=3',\n '-o', `ConnectTimeout=${String(connectTimeout)}`,\n `${user}@${opts.vpsHost}`,\n ];\n const res = await execa('ssh', argv, { reject: false });\n if (res.exitCode !== 0 || !existsSync(controlPath)) {\n throw new Error(\n `ssh ControlMaster failed for ${opts.boxId} (exit ${String(res.exitCode)}): ${res.stderr || res.stdout || '(no output)'}`,\n );\n }\n this.boxes.set(opts.boxId, tunnel);\n }\n\n /**\n * Mint (or fetch the cached) `127.0.0.1:<localPort> → vps:127.0.0.1:<remotePort>`\n * forward. Returns the local port. Idempotent per (boxId, remotePort).\n *\n * The cached entry is only returned when the underlying ControlMaster is\n * still alive — without that check we'd happily hand back a localPort that\n * stopped listening when the master died (e.g. transient network blip,\n * host sleep/wake). When the master is dead we drop ALL cached forwards\n * for this box (they all share one tunnel) and re-mint from scratch.\n */\n async forward(boxId: string, remotePort: number): Promise<number> {\n const tunnel = this.getTunnelOrThrow(boxId);\n const cached = tunnel.forwards.get(remotePort);\n if (cached !== undefined && (await this.isAlive(tunnel.controlPath))) {\n return cached;\n }\n if (cached !== undefined) {\n // Master died — every cached local port stopped listening. Drop them\n // all; callers that still hold a stale `localPort` will get a fresh\n // one on their next forward() call.\n tunnel.forwards.clear();\n }\n const localPort = await pickFreePort();\n const argv = [\n '-O', 'forward',\n '-L', `${HOST}:${String(localPort)}:${HOST}:${String(remotePort)}`,\n '-S', tunnel.controlPath,\n 'dummy', // the target host is ignored when -O is used, but argv needs one\n ];\n const res = await execa('ssh', argv, { reject: false });\n if (res.exitCode !== 0) {\n throw new Error(\n `ssh -O forward failed for ${boxId} (exit ${String(res.exitCode)}): ${res.stderr || res.stdout || '(no output)'}`,\n );\n }\n tunnel.forwards.set(remotePort, localPort);\n return localPort;\n }\n\n /**\n * Tear down a dead ControlMaster + every cached forward for this box,\n * then re-open from scratch. Idempotent — if the master is already alive\n * the master open() is a no-op, but the cached forwards still get\n * cleared so the next forward() call re-mints them. Returns when the\n * master is open and the box's forwards map is empty (ready for fresh\n * forward() calls).\n *\n * Use case: the cloud-poller observes ECONNREFUSED on the local port and\n * asks the backend to refresh the preview URL — that path calls into\n * here so the master + forward both come back fresh.\n */\n async refresh(opts: SshTunnelOpenOptions): Promise<void> {\n const existing = this.boxes.get(opts.boxId);\n if (existing) {\n const alive = await this.isAlive(existing.controlPath);\n if (!alive && existsSync(existing.controlPath)) {\n // Stale socket from a dead master — best-effort cleanup, then reopen.\n try {\n await execa(\n 'ssh',\n ['-O', 'exit', '-S', existing.controlPath, 'dummy'],\n { reject: false },\n );\n } catch {\n // ignore — `-O exit` on a dead master can fail\n }\n await rm(existing.controlPath, { force: true });\n }\n // Either way drop the cached forwards: even if the master happened to\n // be alive, the caller asked us to refresh because *something*\n // upstream (the local port) wasn't responding.\n existing.forwards.clear();\n }\n await this.open(opts);\n }\n\n /** Tear down a single forward. Idempotent — unknown ports are no-ops. */\n async unforward(boxId: string, remotePort: number): Promise<void> {\n const tunnel = this.getTunnelOrThrow(boxId);\n const localPort = tunnel.forwards.get(remotePort);\n if (localPort === undefined) return;\n const argv = [\n '-O', 'cancel',\n '-L', `${HOST}:${String(localPort)}:${HOST}:${String(remotePort)}`,\n '-S', tunnel.controlPath,\n 'dummy',\n ];\n await execa('ssh', argv, { reject: false });\n tunnel.forwards.delete(remotePort);\n }\n\n /**\n * Close the ControlMaster (and all its forwards). Idempotent — if no\n * master is recorded, no-op. Removes the socket file.\n */\n async close(boxId: string): Promise<void> {\n const tunnel = this.boxes.get(boxId);\n if (!tunnel) return;\n if (existsSync(tunnel.controlPath)) {\n await execa('ssh', ['-O', 'exit', '-S', tunnel.controlPath, 'dummy'], { reject: false });\n await rm(tunnel.controlPath, { force: true });\n }\n this.boxes.delete(boxId);\n }\n\n /** Tear down every open box. */\n async closeAll(): Promise<void> {\n const ids = Array.from(this.boxes.keys());\n await Promise.all(ids.map((id) => this.close(id)));\n }\n\n /** Path to the ControlMaster socket for `boxId`, if open. */\n controlPath(boxId: string): string | undefined {\n return this.boxes.get(boxId)?.controlPath;\n }\n\n /** True if a ControlMaster is registered for `boxId` (regardless of liveness). */\n has(boxId: string): boolean {\n return this.boxes.has(boxId);\n }\n\n /** Per-box ssh dir (tests use this to verify the layout). */\n boxSshDir(boxId: string): string | undefined {\n return this.boxes.get(boxId)?.boxSshDir;\n }\n\n /** Re-open the manager record for an existing-on-disk control socket. */\n registerExisting(boxId: string, opts: SshTunnelOpenOptions): void {\n const boxSshDir = opts.boxSshDir ?? defaultBoxSshDir(opts.boxId);\n this.boxes.set(boxId, {\n controlPath: join(boxSshDir, 'control.sock'),\n vpsHost: opts.vpsHost,\n vpsUser: opts.vpsUser ?? 'vscode',\n identity: opts.identity,\n boxSshDir,\n forwards: new Map(),\n });\n }\n\n private async isAlive(controlPath: string): Promise<boolean> {\n const res = await execa('ssh', ['-O', 'check', '-S', controlPath, 'dummy'], { reject: false });\n return res.exitCode === 0;\n }\n\n private getTunnelOrThrow(boxId: string): BoxTunnel {\n const t = this.boxes.get(boxId);\n if (!t) throw new Error(`no SSH ControlMaster registered for box ${boxId}; call open() first`);\n return t;\n }\n}\n\n/** Default per-box ssh dir: `~/.agentbox/boxes/<box-id>/ssh/`. */\nexport function defaultBoxSshDir(boxId: string): string {\n return resolve(homedir(), '.agentbox', 'boxes', boxId, 'ssh');\n}\n\n/**\n * Ask the kernel for a free port in the ephemeral range. We bind a fresh\n * server on `127.0.0.1:0`, capture the port the kernel assigned, and close\n * — there's a tiny race where the port could be claimed before `ssh -O\n * forward` lands, but in practice ssh's idempotent socket re-creation\n * doesn't conflict with the captive process.\n */\nasync function pickFreePort(): Promise<number> {\n return new Promise((resolveOk, reject) => {\n const srv = createServer();\n srv.unref();\n srv.on('error', reject);\n srv.listen(0, HOST, () => {\n const addr = srv.address();\n if (!addr || typeof addr === 'string') {\n srv.close();\n reject(new Error('could not get a free local port'));\n return;\n }\n const port = addr.port;\n srv.close(() => {\n if (port < EPHEMERAL_MIN || port > EPHEMERAL_MAX) {\n // Some systems give back a low port — that's fine, just allow it.\n resolveOk(port);\n } else {\n resolveOk(port);\n }\n });\n });\n });\n}\n\nexport const _internalForTests = {\n pickFreePort,\n defaultBoxSshDir,\n EPHEMERAL_MIN,\n EPHEMERAL_MAX,\n // Tests use the export below; this object documents the surface.\n exports: {} as Record<string, never>,\n};\n// Avoid unused-vars in case the test wants to add helpers later — see backend.ts pattern.\nvoid dirname;\n"],"mappings":"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","names":["existsSync","rm","mkdir","join","execa","execa","mkdir","dirname","join","resolve","rm","client","SNAPSHOT_DEADLINE_MS","existsSync"]}
|