@madarco/agentbox 0.13.0 → 0.15.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 +125 -0
- package/README.md +11 -8
- package/dist/{_cloud-attach-HJC672UR.js → _cloud-attach-R6TRWG5L.js} +4 -4
- package/dist/{chunk-QYRK5H6Q.js → chunk-43Q5GWP6.js} +108 -56
- package/dist/chunk-43Q5GWP6.js.map +1 -0
- package/dist/{chunk-ECLLV5JH.js → chunk-72CJTXN6.js} +156 -5
- package/dist/chunk-72CJTXN6.js.map +1 -0
- package/dist/{chunk-R5XIDQFR.js → chunk-BKU34KYY.js} +170 -6
- package/dist/chunk-BKU34KYY.js.map +1 -0
- package/dist/{chunk-4NQXNQ53.js → chunk-E7CHS7ZR.js} +168 -58
- package/dist/chunk-E7CHS7ZR.js.map +1 -0
- package/dist/chunk-MCOU6CZS.js +346 -0
- package/dist/chunk-MCOU6CZS.js.map +1 -0
- package/dist/{chunk-B4QG2MCW.js → chunk-MLMFNN4T.js} +762 -483
- package/dist/chunk-MLMFNN4T.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-SNTHHWKY.js → chunk-XKH7NTT7.js} +80 -22
- package/dist/chunk-XKH7NTT7.js.map +1 -0
- package/dist/{dist-7KVUIKJX.js → dist-AGTIA7AD.js} +37 -226
- package/dist/dist-AGTIA7AD.js.map +1 -0
- package/dist/{dist-OPIBZ7XM.js → dist-FIFEFKJ7.js} +14 -69
- package/dist/dist-FIFEFKJ7.js.map +1 -0
- package/dist/dist-JZ3XO6EB.js +662 -0
- package/dist/dist-JZ3XO6EB.js.map +1 -0
- package/dist/{dist-OG6NW6SM.js → dist-OGJGZETZ.js} +5 -3
- package/dist/{dist-JAN5VABY.js → dist-S4XR4ACV.js} +25 -177
- package/dist/dist-S4XR4ACV.js.map +1 -0
- package/dist/index.js +2229 -1314
- package/dist/index.js.map +1 -1
- package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js → prepared-state-MQHD3M5F-Q27AZU53.js} +2 -2
- package/package.json +6 -4
- package/runtime/docker/Dockerfile.box +21 -26
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +67 -1
- package/runtime/docker/packages/ctl/dist/bin.cjs +361 -43
- 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 +263 -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 +24158 -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 +67 -1
- package/runtime/hetzner/agentbox-vnc-start +17 -6
- package/runtime/hetzner/claude-managed-settings.json +2 -1
- package/runtime/hetzner/ctl.cjs +361 -43
- package/runtime/relay/bin.cjs +380 -233
- package/runtime/vercel/agentbox-setup-skill.md +67 -1
- package/runtime/vercel/agentbox-vnc-start +17 -6
- package/runtime/vercel/claude-managed-settings.json +2 -1
- package/runtime/vercel/ctl.cjs +361 -43
- package/share/agentbox-setup/SKILL.md +67 -1
- package/share/host-skills/agentbox-info/SKILL.md +47 -35
- 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-R6TRWG5L.js.map} +0 -0
- /package/dist/{dist-OG6NW6SM.js.map → dist-OGJGZETZ.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/env-loader.ts","../../../packages/sandbox-hetzner/src/client.ts","../../../packages/sandbox-hetzner/src/credentials.ts","../../../packages/sandbox-hetzner/src/egress-ip.ts","../../../packages/sandbox-hetzner/src/retry.ts","../../../packages/sandbox-hetzner/src/firewall.ts","../../../packages/sandbox-hetzner/src/runtime-assets.ts","../../../packages/sandbox-hetzner/src/prepared-state.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { resolve } from 'node:path';\n\n/**\n * Hetzner env auto-loader — mirrors `ensureDaytonaEnvLoaded()`. The Hetzner\n * REST client reads `HCLOUD_TOKEN` from `process.env`. We pull it in from\n * `~/.agentbox/secrets.env` so the client Just Works after the user runs\n * `agentbox hetzner login` once.\n *\n * Lookup order (first wins; process.env is never overwritten):\n * 1. `process.env` (already set in the shell).\n * 2. `~/.agentbox/secrets.env` — written by `agentbox hetzner login`.\n *\n * Project-level `.env` / `.env.local` are intentionally NOT consulted: those\n * files belong to the app code being developed, and a `HCLOUD_TOKEN` there\n * is typically meant for in-box infrastructure work, not for the host CLI to\n * harvest and provision VPSes with.\n *\n * Only Hetzner-prefixed keys are imported. Idempotent + side-effect-free\n * after the first call.\n */\nconst HETZNER_KEYS = ['HCLOUD_TOKEN', 'HCLOUD_ENDPOINT'] as const;\n\nlet loaded = false;\n\nexport function ensureHetznerEnvLoaded(): void {\n if (loaded) return;\n loaded = true;\n importHetznerFromFile(resolve(homedir(), '.agentbox', 'secrets.env'));\n}\n\nfunction importHetznerFromFile(path: string): void {\n if (!existsSync(path)) return;\n let body: string;\n try {\n body = readFileSync(path, 'utf8');\n } catch {\n return;\n }\n const parsed = parseEnvFile(body);\n for (const key of HETZNER_KEYS) {\n if (process.env[key] !== undefined) continue;\n const value = parsed[key];\n if (typeof value === 'string') {\n process.env[key] = value;\n }\n }\n}\n\n/**\n * Minimal `.env` parser: handles `KEY=value`, `KEY=\"value with spaces\"`,\n * `KEY='value with $special chars'`, `export KEY=value`, blank lines, and\n * `#` comments. Same shape as the daytona env-loader's parser — kept local\n * here rather than imported across packages to avoid the cycle (daytona\n * doesn't import from hetzner and shouldn't start now).\n */\nexport function parseEnvFile(body: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const rawLine of body.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (line.length === 0 || line.startsWith('#')) continue;\n const stripped = line.startsWith('export ') ? line.slice('export '.length) : line;\n const eq = stripped.indexOf('=');\n if (eq <= 0) continue;\n const key = stripped.slice(0, eq).trim();\n let value = stripped.slice(eq + 1).trim();\n if (\n value.length >= 2 &&\n ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\")))\n ) {\n value = value.slice(1, -1);\n }\n out[key] = value;\n }\n return out;\n}\n","/**\n * Hetzner Cloud REST API client — hand-rolled fetch wrapper.\n *\n * Why not an SDK: the Hetzner SDK options are limited (no official JS SDK\n * with strict types at the time of writing), and the subset of the API we\n * need is small (servers, images, firewalls, plus a handful of read-only\n * lookups). A hand-rolled client gives us strict typing of just the fields\n * we touch, no heavy dep tree, and full control over the retry wrapper.\n *\n * Auth: bearer token in `HCLOUD_TOKEN` env. The env-loader pulls it from\n * `~/.agentbox/secrets.env` so the user only sets it once via\n * `agentbox hetzner login`.\n *\n * Errors: REST responses get unwrapped into typed `HetznerApiError`s that\n * carry the response `status` + the API's `error.code` / `error.message`.\n * Network failures bubble up as raw `Error`s with a `code` property\n * (ECONNRESET, ETIMEDOUT, …) — the retry wrapper classifies both shapes.\n */\n\nimport { ensureHetznerEnvLoaded } from './env-loader.js';\n\nexport const DEFAULT_HCLOUD_ENDPOINT = 'https://api.hetzner.cloud/v1';\n\n/**\n * Coarse Hetzner Cloud Server lifecycle states we care about. Hetzner has a\n * dozen finer-grained ones (`initializing`, `migrating`, `rebuilding`, …);\n * we map them in `backend.ts` to the four-value `CloudState` everyone else\n * consumes. Listed here so the client return types stay narrow.\n */\nexport type HetznerServerStatus =\n | 'running'\n | 'initializing'\n | 'starting'\n | 'stopping'\n | 'off'\n | 'deleting'\n | 'migrating'\n | 'rebuilding'\n | 'unknown';\n\nexport interface HetznerServer {\n id: number;\n name: string;\n status: HetznerServerStatus;\n created: string;\n public_net: {\n ipv4: { ip: string; blocked: boolean } | null;\n ipv6: { ip: string; blocked: boolean } | null;\n };\n server_type: { name: string; cores: number; memory: number; disk: number };\n image: { id: number; name?: string; description?: string; type: string } | null;\n labels: Record<string, string>;\n}\n\nexport interface HetznerAction {\n id: number;\n command: string;\n status: 'running' | 'success' | 'error';\n progress: number;\n error?: { code: string; message: string };\n}\n\nexport interface HetznerImage {\n id: number;\n type: 'system' | 'snapshot' | 'backup' | 'app';\n status: 'available' | 'creating' | 'unavailable';\n name?: string;\n description: string;\n image_size?: number;\n disk_size: number;\n created: string;\n labels: Record<string, string>;\n bound_to?: number;\n}\n\nexport interface HetznerFirewall {\n id: number;\n name: string;\n rules: HetznerFirewallRule[];\n applied_to: Array<{ type: 'server'; server: { id: number } }>;\n}\n\nexport interface HetznerFirewallRule {\n direction: 'in' | 'out';\n protocol: 'tcp' | 'udp' | 'icmp' | 'esp' | 'gre';\n port?: string;\n source_ips?: string[];\n destination_ips?: string[];\n description?: string;\n}\n\nexport interface HetznerSshKey {\n id: number;\n name: string;\n fingerprint: string;\n public_key: string;\n labels: Record<string, string>;\n}\n\nexport interface CreateServerRequest {\n name: string;\n server_type: string;\n image: string | number;\n location?: string;\n datacenter?: string;\n user_data?: string;\n ssh_keys?: Array<string | number>;\n firewalls?: Array<{ firewall: number }>;\n labels?: Record<string, string>;\n start_after_create?: boolean;\n public_net?: {\n enable_ipv4?: boolean;\n enable_ipv6?: boolean;\n };\n}\n\nexport interface CreateFirewallRequest {\n name: string;\n rules: HetznerFirewallRule[];\n labels?: Record<string, string>;\n apply_to?: Array<{ type: 'server'; server: { id: number } }>;\n}\n\n/**\n * Strongly-typed Hetzner API error. The Hetzner API consistently returns\n * `{ error: { code, message, details? } }` for 4xx/5xx (https://docs.hetzner.cloud/#errors).\n * We unwrap that into this class so callers can do `instanceof\n * HetznerApiError` and inspect `.code` / `.statusCode` without parsing the\n * body again.\n */\nexport class HetznerApiError extends Error {\n readonly statusCode: number;\n readonly code: string;\n readonly details?: unknown;\n constructor(statusCode: number, code: string, message: string, details?: unknown) {\n super(`hetzner ${String(statusCode)} ${code}: ${message}`);\n this.name = 'HetznerApiError';\n this.statusCode = statusCode;\n this.code = code;\n this.details = details;\n }\n}\n\n/**\n * Subset of the Hetzner Cloud API the agentbox provider talks to. Methods\n * map 1:1 to REST endpoints; each operation is small + idempotent-where-the-\n * API-is-idempotent. The retry wrapper around the provider methods handles\n * transient 5xx / connection failures.\n */\nexport interface HetznerClient {\n /** GET /servers/{id}. Returns null on 404 so callers don't have to try/catch. */\n getServer(id: number): Promise<HetznerServer | null>;\n /** POST /servers. Returns the created server + the create action handle. */\n createServer(req: CreateServerRequest): Promise<{ server: HetznerServer; action: HetznerAction }>;\n /** GET /servers (with optional label selector). */\n listServers(opts?: { label_selector?: string }): Promise<HetznerServer[]>;\n /** DELETE /servers/{id}. Returns the action handle. Idempotent on 404. */\n deleteServer(id: number): Promise<HetznerAction | null>;\n /** POST /servers/{id}/actions/poweron. */\n powerOn(id: number): Promise<HetznerAction>;\n /** POST /servers/{id}/actions/poweroff. */\n powerOff(id: number): Promise<HetznerAction>;\n /** POST /servers/{id}/actions/shutdown — graceful, sends ACPI. */\n shutdown(id: number): Promise<HetznerAction>;\n /** POST /servers/{id}/actions/create_image — snapshot of the live disk. */\n createImage(\n id: number,\n body: { type: 'snapshot' | 'backup'; description?: string; labels?: Record<string, string> },\n ): Promise<{ image: HetznerImage; action: HetznerAction }>;\n /** GET /images/{id}. Returns null on 404. */\n getImage(id: number): Promise<HetznerImage | null>;\n /** GET /images (filterable). */\n listImages(opts?: {\n type?: 'system' | 'snapshot' | 'backup' | 'app';\n label_selector?: string;\n name?: string;\n }): Promise<HetznerImage[]>;\n /** DELETE /images/{id}. Idempotent on 404. */\n deleteImage(id: number): Promise<void>;\n /** POST /firewalls. */\n createFirewall(req: CreateFirewallRequest): Promise<HetznerFirewall>;\n /** POST /firewalls/{id}/actions/set_rules. Replaces the entire rule set. */\n setFirewallRules(id: number, rules: HetznerFirewallRule[]): Promise<HetznerAction[]>;\n /** GET /firewalls/{id}. Returns null on 404. */\n getFirewall(id: number): Promise<HetznerFirewall | null>;\n /** DELETE /firewalls/{id}. Idempotent on 404. */\n deleteFirewall(id: number): Promise<void>;\n /**\n * GET /locations — used by `agentbox hetzner login` to validate the token\n * with a cheap unauthenticated-shape call (the endpoint requires a valid\n * token but returns a small, stable response).\n */\n listLocations(): Promise<Array<{ id: number; name: string; city: string; country: string }>>;\n}\n\ninterface MakeClientOptions {\n /** Override the bearer token (else read from `HCLOUD_TOKEN`). */\n token?: string;\n /** Override the API base URL (else read from `HCLOUD_ENDPOINT` or use the default). */\n endpoint?: string;\n /** Per-request fetch impl (tests inject this). */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Build a Hetzner Cloud client bound to the current `HCLOUD_TOKEN`. The token\n * is resolved at construction time, so re-running `agentbox hetzner login` in\n * the middle of a long-lived process won't pick up the new token without a\n * fresh `makeHetznerClient()` call (we accept this — the CLI re-imports the\n * provider on each invocation).\n */\nexport function makeHetznerClient(opts: MakeClientOptions = {}): HetznerClient {\n ensureHetznerEnvLoaded();\n const rawToken = opts.token ?? process.env.HCLOUD_TOKEN;\n if (!rawToken || rawToken.trim().length === 0) {\n throw new Error(\n 'Hetzner credentials not configured: HCLOUD_TOKEN is empty.\\n' +\n 'Run `agentbox hetzner login` interactively, or set HCLOUD_TOKEN in the environment.',\n );\n }\n // Bind to a const so the type narrows for the closures below — without\n // this the `req()` closure sees the original `string | undefined` shape.\n const token: string = rawToken.trim();\n const endpoint = (opts.endpoint ?? process.env.HCLOUD_ENDPOINT ?? DEFAULT_HCLOUD_ENDPOINT).replace(/\\/$/, '');\n const fetchImpl = opts.fetchImpl ?? fetch;\n\n async function req<T>(\n method: 'GET' | 'POST' | 'PUT' | 'DELETE',\n path: string,\n body?: unknown,\n ): Promise<T | null> {\n const url = `${endpoint}${path}`;\n const init: RequestInit = {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n };\n const res = await fetchImpl(url, init);\n if (res.status === 204) return null;\n if (res.status === 404) return null;\n if (!res.ok) {\n let parsed: { error?: { code?: string; message?: string; details?: unknown } } = {};\n try {\n parsed = (await res.json()) as typeof parsed;\n } catch {\n // body wasn't json\n }\n const code = parsed.error?.code ?? `http_${String(res.status)}`;\n const msg = parsed.error?.message ?? res.statusText ?? 'unknown error';\n throw new HetznerApiError(res.status, code, msg, parsed.error?.details);\n }\n const text = await res.text();\n if (text.length === 0) return null;\n return JSON.parse(text) as T;\n }\n\n async function reqExpect<T>(\n method: 'GET' | 'POST' | 'PUT' | 'DELETE',\n path: string,\n body?: unknown,\n ): Promise<T> {\n const out = await req<T>(method, path, body);\n if (out === null) {\n throw new HetznerApiError(0, 'empty_response', `expected a body from ${method} ${path}`);\n }\n return out;\n }\n\n return {\n async getServer(id) {\n const r = await req<{ server: HetznerServer }>('GET', `/servers/${String(id)}`);\n return r?.server ?? null;\n },\n async createServer(reqBody) {\n const r = await reqExpect<{ server: HetznerServer; action: HetznerAction }>(\n 'POST',\n '/servers',\n reqBody,\n );\n return { server: r.server, action: r.action };\n },\n async listServers(opts) {\n const params = new URLSearchParams();\n if (opts?.label_selector) params.set('label_selector', opts.label_selector);\n params.set('per_page', '50');\n const all: HetznerServer[] = [];\n let pageNum = 1;\n while (true) {\n params.set('page', String(pageNum));\n const r = await reqExpect<{\n servers: HetznerServer[];\n meta?: { pagination?: { next_page?: number | null } };\n }>('GET', `/servers?${params.toString()}`);\n all.push(...r.servers);\n const next = r.meta?.pagination?.next_page;\n if (typeof next !== 'number') break;\n pageNum = next;\n }\n return all;\n },\n async deleteServer(id) {\n const r = await req<{ action: HetznerAction }>('DELETE', `/servers/${String(id)}`);\n return r?.action ?? null;\n },\n async powerOn(id) {\n const r = await reqExpect<{ action: HetznerAction }>(\n 'POST',\n `/servers/${String(id)}/actions/poweron`,\n );\n return r.action;\n },\n async powerOff(id) {\n const r = await reqExpect<{ action: HetznerAction }>(\n 'POST',\n `/servers/${String(id)}/actions/poweroff`,\n );\n return r.action;\n },\n async shutdown(id) {\n const r = await reqExpect<{ action: HetznerAction }>(\n 'POST',\n `/servers/${String(id)}/actions/shutdown`,\n );\n return r.action;\n },\n async createImage(id, body) {\n const r = await reqExpect<{ image: HetznerImage; action: HetznerAction }>(\n 'POST',\n `/servers/${String(id)}/actions/create_image`,\n body,\n );\n return { image: r.image, action: r.action };\n },\n async getImage(id) {\n const r = await req<{ image: HetznerImage }>('GET', `/images/${String(id)}`);\n return r?.image ?? null;\n },\n async listImages(opts) {\n const params = new URLSearchParams();\n if (opts?.type) params.set('type', opts.type);\n if (opts?.label_selector) params.set('label_selector', opts.label_selector);\n if (opts?.name) params.set('name', opts.name);\n params.set('per_page', '50');\n const all: HetznerImage[] = [];\n let pageNum = 1;\n while (true) {\n params.set('page', String(pageNum));\n const r = await reqExpect<{\n images: HetznerImage[];\n meta?: { pagination?: { next_page?: number | null } };\n }>('GET', `/images?${params.toString()}`);\n all.push(...r.images);\n const next = r.meta?.pagination?.next_page;\n if (typeof next !== 'number') break;\n pageNum = next;\n }\n return all;\n },\n async deleteImage(id) {\n await req<unknown>('DELETE', `/images/${String(id)}`);\n },\n async createFirewall(reqBody) {\n const r = await reqExpect<{ firewall: HetznerFirewall }>('POST', '/firewalls', reqBody);\n return r.firewall;\n },\n async setFirewallRules(id, rules) {\n const r = await reqExpect<{ actions: HetznerAction[] }>(\n 'POST',\n `/firewalls/${String(id)}/actions/set_rules`,\n { rules },\n );\n return r.actions;\n },\n async getFirewall(id) {\n const r = await req<{ firewall: HetznerFirewall }>('GET', `/firewalls/${String(id)}`);\n return r?.firewall ?? null;\n },\n async deleteFirewall(id) {\n await req<unknown>('DELETE', `/firewalls/${String(id)}`);\n },\n async listLocations() {\n const r = await reqExpect<{\n locations: Array<{ id: number; name: string; city: string; country: string }>;\n }>('GET', '/locations');\n return r.locations;\n },\n };\n}\n","import { spawnSync } from 'node:child_process';\nimport { hostOpenCommand } from '@agentbox/sandbox-core';\nimport {\n chmodSync,\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n writeFileSync,\n} from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, resolve } from 'node:path';\nimport { confirm, isCancel, intro, log, note, outro, password, spinner } from '@clack/prompts';\nimport { makeHetznerClient } from './client.js';\nimport { ensureHetznerEnvLoaded } from './env-loader.js';\n\nconst DASHBOARD_KEYS_URL = 'https://console.hetzner.cloud/projects';\n\n/**\n * Keys we manage in `~/.agentbox/secrets.env`. When the user reconfigures\n * we strip prior values before appending so the file never accumulates\n * duplicates. `HCLOUD_ENDPOINT` is honored but we don't prompt for it\n * (default endpoint covers 100% of users).\n */\nconst MANAGED_KEYS = ['HCLOUD_TOKEN', 'HCLOUD_ENDPOINT'] as const;\ntype ManagedKey = (typeof MANAGED_KEYS)[number];\n\nexport interface EnsureHetznerCredentialsOptions {\n /** Re-prompt even when valid credentials are already present (used by `agentbox hetzner login`). */\n force?: boolean;\n}\n\n/**\n * First-run interactive setup for Hetzner credentials. Walks the user\n * through creating a project API token, pasting it, validating, and\n * persisting to `~/.agentbox/secrets.env`.\n *\n * No-op when credentials are already configured (env var or our secrets\n * file). Silent no-op when stdin isn't a TTY so scripted/CI callers get\n * the API \"401 unauthorized\" error instead of a hung prompt.\n *\n * Mirrors `ensureDaytonaCredentials()` in shape so the registry's first-\n * run gate stays uniform across providers.\n */\nexport async function ensureHetznerCredentials(\n opts: EnsureHetznerCredentialsOptions = {},\n): Promise<void> {\n ensureHetznerEnvLoaded();\n\n if (!opts.force && hasUsableCredentials()) return;\n if (!process.stdin.isTTY) return;\n\n intro('Hetzner Cloud setup');\n note(\n `AgentBox needs a Hetzner Cloud API token (project-scoped) to provision VPSes.\\n\\n` +\n `1. Open ${DASHBOARD_KEYS_URL}\\n` +\n `2. Pick a project (or create one).\\n` +\n `3. Security → API Tokens → Generate API Token (Read + Write).`,\n 'API token required',\n );\n\n const open = await confirm({\n message: `Open ${DASHBOARD_KEYS_URL} in your browser?`,\n initialValue: true,\n });\n if (isCancel(open)) {\n log.warn('Hetzner setup cancelled — re-run `agentbox hetzner login` when ready.');\n return;\n }\n if (open) openDashboard();\n\n // One retry on auth failure (typos / expired token are the common case).\n for (let attempt = 0; attempt < 2; attempt++) {\n const creds = await promptForCredentials();\n if (creds === null) return;\n\n const result = await validateCredentials(creds);\n if (result.ok) {\n persistCredentials(creds);\n log.success(`Hetzner credentials saved to ${secretsPath()}`);\n outro('Setup complete.');\n return;\n }\n if (result.kind === 'auth' && attempt === 0) {\n log.error(`That token was rejected by Hetzner: ${result.message}`);\n log.info('Try again, or press Ctrl-C to cancel.');\n continue;\n }\n if (result.kind === 'network') {\n log.warn(`Could not reach Hetzner to validate (${result.message}) — saving anyway.`);\n persistCredentials(creds);\n log.success(`Hetzner credentials saved to ${secretsPath()}`);\n outro('Setup complete (unvalidated).');\n return;\n }\n throw new Error(`Hetzner credentials rejected: ${result.message}`);\n }\n}\n\nfunction hasUsableCredentials(): boolean {\n return typeof process.env.HCLOUD_TOKEN === 'string' && process.env.HCLOUD_TOKEN.length > 0;\n}\n\ninterface Credentials {\n token: string;\n endpoint?: string;\n}\n\nasync function promptForCredentials(): Promise<Credentials | null> {\n const token = await password({\n message: 'Paste your Hetzner Cloud API token',\n validate(v) {\n if (!v || v.trim().length === 0) return 'Cannot be empty';\n return undefined;\n },\n });\n if (isCancel(token)) {\n log.warn('Hetzner setup cancelled.');\n return null;\n }\n return { token: token.trim() };\n}\n\ntype ValidationResult =\n | { ok: true }\n | { ok: false; kind: 'auth'; message: string }\n | { ok: false; kind: 'network'; message: string };\n\nasync function validateCredentials(creds: Credentials): Promise<ValidationResult> {\n const s = spinner();\n s.start('Validating credentials with Hetzner');\n\n try {\n const client = makeHetznerClient({ token: creds.token, endpoint: creds.endpoint });\n // `listLocations()` is a cheap, deterministic call that exercises auth +\n // basic API reachability without provisioning anything.\n await client.listLocations();\n s.stop('Hetzner credentials accepted');\n return { ok: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n s.stop('Hetzner credentials check failed');\n if (/401|403|unauthor|forbidden|invalid|token/i.test(message)) {\n return { ok: false, kind: 'auth', message };\n }\n return { ok: false, kind: 'network', message };\n }\n}\n\nfunction persistCredentials(creds: Credentials): void {\n process.env.HCLOUD_TOKEN = creds.token;\n if (creds.endpoint) process.env.HCLOUD_ENDPOINT = creds.endpoint;\n const path = secretsPath();\n mkdirSync(dirname(path), { recursive: true });\n\n let existing = '';\n if (existsSync(path)) {\n try {\n existing = readFileSync(path, 'utf8');\n } catch {\n existing = '';\n }\n }\n\n const kept = existing\n .split(/\\r?\\n/)\n .filter((line) => {\n const stripped = line.startsWith('export ') ? line.slice('export '.length) : line;\n const eq = stripped.indexOf('=');\n if (eq <= 0) return true;\n const key = stripped.slice(0, eq).trim();\n return !(MANAGED_KEYS as readonly string[]).includes(key);\n })\n .join('\\n')\n .replace(/\\s+$/u, '');\n\n const lines: string[] = [`HCLOUD_TOKEN=${creds.token}`];\n if (creds.endpoint) lines.push(`HCLOUD_ENDPOINT=${creds.endpoint}`);\n\n const body = (kept ? `${kept}\\n` : '') + lines.join('\\n') + '\\n';\n\n const tmp = `${path}.tmp`;\n writeFileSync(tmp, body, { mode: 0o600 });\n try {\n chmodSync(tmp, 0o600);\n } catch {\n // chmod best-effort; writeFileSync mode already covers most filesystems.\n }\n renameSync(tmp, path);\n try {\n chmodSync(path, 0o600);\n } catch {\n // ignore — already attempted above.\n }\n}\n\nfunction openDashboard(): void {\n try {\n const r = spawnSync(hostOpenCommand(), [DASHBOARD_KEYS_URL], { stdio: 'ignore' });\n if (r.status !== 0) {\n log.warn(`Could not auto-open the browser — visit ${DASHBOARD_KEYS_URL} manually.`);\n }\n } catch {\n log.warn(`Could not auto-open the browser — visit ${DASHBOARD_KEYS_URL} manually.`);\n }\n}\n\nexport function secretsPath(): string {\n return resolve(homedir(), '.agentbox', 'secrets.env');\n}\n\nexport interface HetznerCredStatus {\n token?: string;\n endpoint?: string;\n source: 'env' | 'secrets.env' | 'none';\n}\n\nexport function readHetznerCredStatus(): HetznerCredStatus {\n const shellHadToken = !!process.env.HCLOUD_TOKEN;\n ensureHetznerEnvLoaded();\n const token = process.env.HCLOUD_TOKEN;\n const endpoint = process.env.HCLOUD_ENDPOINT;\n if (!token) return { source: 'none' };\n return {\n token,\n endpoint,\n source: shellHadToken ? 'env' : 'secrets.env',\n };\n}\n\nexport function maskKey(value: string): string {\n if (value.length <= 8) return '*'.repeat(value.length);\n return `${value.slice(0, 4)}…${'*'.repeat(8)}${value.slice(-4)}`;\n}\n\n/** Snapshot of the managed env keys (used by tests around `applyToEnv`). */\nexport function snapshotManagedEnv(): Record<ManagedKey, string | undefined> {\n const out = {} as Record<ManagedKey, string | undefined>;\n for (const k of MANAGED_KEYS) out[k] = process.env[k];\n return out;\n}\n\nexport function restoreManagedEnv(snap: Record<ManagedKey, string | undefined>): void {\n for (const k of MANAGED_KEYS) {\n if (snap[k] === undefined) delete process.env[k];\n else process.env[k] = snap[k];\n }\n}\n","/**\n * Host egress-IP detection for the Hetzner firewall lock-down. Probes three\n * independent providers in sequence; first 3s success wins. Fails loud\n * (throws) if all three fail — we do **not** silently fall back to\n * `0.0.0.0/0`, because that would defeat the safe-by-default firewall.\n *\n * The user can always override the auto-detect via\n * `--firewall-source <cidr>` (or `--firewall-source 0.0.0.0/0` for the\n * explicit dynamic-IP opt-in).\n */\n\nconst PROBES = [\n 'https://api.ipify.org',\n 'https://ifconfig.io/ip',\n 'https://icanhazip.com',\n] as const;\n\nconst TIMEOUT_MS = 3_000;\n\nconst IPV4_RE = /^(?:\\d{1,3}\\.){3}\\d{1,3}$/;\nconst IPV6_RE = /^[0-9a-fA-F:]+$/;\n\nexport interface DetectEgressIpOptions {\n /** Override the probe list (tests inject this). */\n probes?: readonly string[];\n /** Per-probe timeout in ms (default 3_000). */\n timeoutMs?: number;\n /** Override `fetch` (tests inject this). */\n fetchImpl?: typeof fetch;\n /** Best-effort logger for probe attempts. */\n onLog?: (line: string) => void;\n}\n\n/**\n * Detect the host's egress IP. Returns the bare IP string (no `/32`); the\n * caller composes the CIDR.\n *\n * Throws when no probe responded. The error message lists each probe that\n * was tried so the user can see whether their network is blocking a\n * specific provider.\n */\nexport async function detectEgressIp(opts: DetectEgressIpOptions = {}): Promise<string> {\n const probes = opts.probes ?? PROBES;\n const timeout = opts.timeoutMs ?? TIMEOUT_MS;\n const fetchImpl = opts.fetchImpl ?? fetch;\n const errors: string[] = [];\n\n for (const url of probes) {\n try {\n const ip = await raceTimeout(probe(url, fetchImpl), timeout);\n if (ip) {\n opts.onLog?.(`egress-ip: detected ${ip} via ${url}`);\n return ip;\n }\n errors.push(`${url}: empty/invalid response`);\n } catch (err) {\n errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n throw new Error(\n `could not auto-detect the host's egress IP — all ${String(probes.length)} probes failed:\\n` +\n errors.map((e) => ` - ${e}`).join('\\n') +\n `\\nOverride with --firewall-source <cidr> (e.g. --firewall-source 0.0.0.0/0 for the explicit-open opt-in).`,\n );\n}\n\nasync function probe(url: string, fetchImpl: typeof fetch): Promise<string | null> {\n const res = await fetchImpl(url, { method: 'GET' });\n if (!res.ok) return null;\n const body = (await res.text()).trim();\n if (IPV4_RE.test(body)) {\n // Cheap sanity: each octet in 0–255.\n const parts = body.split('.').map((p) => Number.parseInt(p, 10));\n if (parts.every((p) => p >= 0 && p <= 255)) return body;\n return null;\n }\n // We do not currently use IPv6 for firewall rules (Hetzner accepts them\n // but the rest of the provider talks IPv4), but accept the probe answer\n // so a v6-only network surfaces an actionable error rather than a silent\n // empty result. Composing the CIDR is the caller's job.\n if (IPV6_RE.test(body) && body.includes(':')) return body;\n return null;\n}\n\nasync function raceTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n p,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new Error(`probe timed out after ${String(ms)}ms`)), ms);\n }),\n ]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n}\n","/**\n * Bounded retry wrapper for Hetzner Cloud API calls — mirrors\n * `withDaytonaRetry` in shape and intent. Hetzner is generally well-behaved\n * but the public API does rate-limit (429) and occasionally returns 502/504\n * during regional incidents; without bounded retries those propagate as\n * wedges in the calling lifecycle code.\n *\n * Non-idempotent ops (`provision`, `createImage`) pass\n * `retryOnAmbiguous: false` so a 504 after the request reached the origin\n * doesn't create a duplicate billable resource.\n */\n\nimport { HetznerApiError } from './client.js';\n\nexport interface WithRetryOptions {\n /** Method name, used in retry log lines. */\n method: string;\n /** Per-attempt timeout (ms). Default 30_000. */\n attemptTimeoutMs?: number;\n /** Backoff before attempts 2, 3, … (ms). Default [1000, 2000, 4000]. */\n backoffMs?: readonly number[];\n /**\n * Whether to retry on errors where we can't be sure the server applied\n * the request — connection failures, per-attempt timeouts, and 5xx\n * responses. Set false for non-idempotent operations (e.g. `provision`,\n * `createImage`) where a retry could create a duplicate resource.\n */\n retryOnAmbiguous: boolean;\n /** Override the default `process.stderr` retry sink (used by tests). */\n onRetry?: (line: string) => void;\n}\n\nconst DEFAULT_BACKOFF: readonly number[] = [1000, 2000, 4000];\nconst DEFAULT_ATTEMPT_TIMEOUT_MS = 30_000;\n\nclass AttemptTimeoutError extends Error {\n constructor(method: string, ms: number) {\n super(`hetzner ${method}: per-attempt timeout after ${String(ms)}ms`);\n this.name = 'AttemptTimeoutError';\n }\n}\n\nexport function isAttemptTimeout(err: unknown): err is AttemptTimeoutError {\n return err instanceof AttemptTimeoutError;\n}\n\n/**\n * Classify an error as retriable or not. `allowAmbiguous` gates the cases\n * where the server may or may not have applied the request — the caller\n * decides based on idempotency.\n */\nexport function isRetriable(err: unknown, allowAmbiguous: boolean): boolean {\n if (err instanceof HetznerApiError) {\n // Rate limit: always back off — the server told us to.\n if (err.statusCode === 429 || err.code === 'rate_limit_exceeded') return true;\n // 5xx: ambiguous (the API may or may not have applied the change).\n if (err.statusCode >= 500 && err.statusCode <= 599) return allowAmbiguous;\n // Hetzner conflict / locked errors: the API tells us to wait — same as\n // rate-limit semantically. `conflict` is what `delete_server` returns\n // when another action (e.g. our own poweroff) is still in flight.\n if (err.code === 'locked' || err.code === 'conflict') return true;\n // Everything else is a permanent client error (auth, validation, not_found).\n return false;\n }\n\n if (err instanceof AttemptTimeoutError) return allowAmbiguous;\n\n // Raw fetch / undici errors. The Node fetch impl wraps low-level errors in\n // `{ cause }`; we check both shapes for portability.\n if (err && typeof err === 'object') {\n const candidates: unknown[] = [err, (err as { cause?: unknown }).cause];\n for (const c of candidates) {\n if (!c || typeof c !== 'object') continue;\n const code = (c as { code?: unknown }).code;\n if (\n code === 'ECONNRESET' ||\n code === 'ETIMEDOUT' ||\n code === 'ECONNABORTED' ||\n code === 'EAI_AGAIN' ||\n code === 'ECONNREFUSED' ||\n code === 'ENOTFOUND' ||\n code === 'UND_ERR_SOCKET' ||\n code === 'UND_ERR_CONNECT_TIMEOUT'\n ) {\n return allowAmbiguous;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Run `fn`, retrying on transient failures with capped exponential backoff.\n * Each attempt is bounded by `attemptTimeoutMs` via Promise.race; total\n * wall-clock = sum(backoffMs) + maxAttempts * attemptTimeoutMs.\n */\nexport async function withHetznerRetry<T>(\n opts: WithRetryOptions,\n fn: () => Promise<T>,\n): Promise<T> {\n const backoff = opts.backoffMs ?? DEFAULT_BACKOFF;\n const maxAttempts = backoff.length + 1;\n const timeoutMs = opts.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;\n const log = opts.onRetry ?? defaultRetryLog;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await raceTimeout(fn(), timeoutMs, opts.method);\n } catch (err) {\n const last = attempt === maxAttempts;\n if (last || !isRetriable(err, opts.retryOnAmbiguous)) throw err;\n const delay = backoff[attempt - 1] ?? backoff[backoff.length - 1] ?? 4000;\n log(\n `hetzner ${opts.method}: attempt ${String(attempt)} failed (${errorSummary(err)}); retrying in ${String(delay)}ms`,\n );\n await sleep(delay);\n }\n }\n throw new Error(`withHetznerRetry: exhausted attempts for ${opts.method}`);\n}\n\nfunction defaultRetryLog(line: string): void {\n process.stderr.write(`\\n[hetzner-retry] ${line}\\n`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function raceTimeout<T>(p: Promise<T>, ms: number, method: string): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n p,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new AttemptTimeoutError(method, ms)), ms);\n }),\n ]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n}\n\nfunction errorSummary(err: unknown): string {\n if (err instanceof HetznerApiError) {\n return `HetznerApiError ${String(err.statusCode)} ${err.code}: ${truncate(err.message)}`;\n }\n if (err instanceof Error) {\n const code = (err as { code?: unknown }).code;\n return code !== undefined\n ? `${err.name}(${String(code)}): ${truncate(err.message)}`\n : `${err.name}: ${truncate(err.message)}`;\n }\n return truncate(String(err));\n}\n\nfunction truncate(s: string, max = 160): string {\n return s.length > max ? `${s.slice(0, max)}…` : s;\n}\n","/**\n * Hetzner Cloud Firewall provisioning + drift sync.\n *\n * Defense-in-depth model (recapped from\n * ~/.claude/plans/how-to-safely-create-parallel-pebble.md §\"The safety model\"):\n *\n * 1. In-VPS services bind to loopback (the load-bearing layer).\n * 2. Hetzner Cloud Firewall locks SSH to the host's egress IP — applied\n * here at provision time, before the VPS first boots. Everything else\n * is denied inbound; outbound is unrestricted.\n * 3. sshd hardening (PasswordAuthentication no, AllowUsers vscode, …)\n * written by cloud-init at first boot.\n *\n * Layer 2 is what this module provisions. The firewall is per-box (1:1 with\n * the VPS) so an egress-IP-drift on one box doesn't affect siblings, and a\n * destroy cleanly removes everything we created.\n */\n\nimport { HetznerApiError, type HetznerClient, type HetznerFirewall, type HetznerFirewallRule } from './client.js';\nimport { withHetznerRetry } from './retry.js';\n\n/**\n * Build the SSH-only inbound rule for a given source CIDR. Outbound is\n * left unrestricted (empty rules array = \"no inbound besides this one\").\n */\nexport function sshOnlyInboundRule(sourceCidr: string): HetznerFirewallRule[] {\n return [\n {\n direction: 'in',\n protocol: 'tcp',\n port: '22',\n source_ips: [sourceCidr],\n description: 'agentbox: SSH from host egress IP only',\n },\n ];\n}\n\nexport interface CreateFirewallOptions {\n /** Human-readable name persisted with the firewall (visible in the Hetzner dashboard). */\n name: string;\n /** Source CIDR (e.g. `1.2.3.4/32`). The caller is responsible for normalizing the suffix. */\n sourceCidr: string;\n /** Labels merged onto the firewall (we always add `agentbox.managed=true`). */\n labels?: Record<string, string>;\n}\n\n/**\n * Provision a fresh per-box firewall locked to the given source CIDR.\n * Returns the created `HetznerFirewall` so the caller can persist\n * `firewallId` on the box record.\n */\nexport async function createPerBoxFirewall(\n client: HetznerClient,\n opts: CreateFirewallOptions,\n): Promise<HetznerFirewall> {\n return withHetznerRetry(\n { method: 'createFirewall', retryOnAmbiguous: false, attemptTimeoutMs: 60_000 },\n () =>\n client.createFirewall({\n name: opts.name,\n rules: sshOnlyInboundRule(opts.sourceCidr),\n labels: {\n 'agentbox.managed': 'true',\n 'agentbox.role': 'box',\n ...opts.labels,\n },\n }),\n );\n}\n\n/**\n * Re-detect the egress IP and replace the firewall's rule set with the new\n * source. Used by `agentbox hetzner firewall sync <box>` after the host\n * laptop moves networks. Cheap operation — no VPS restart involved.\n *\n * Idempotent on the API: setting the same rules again is a no-op from the\n * user's point of view (the API still returns an action handle, but it\n * resolves instantly).\n */\nexport async function syncFirewallSource(\n client: HetznerClient,\n firewallId: number,\n sourceCidr: string,\n): Promise<void> {\n await withHetznerRetry(\n { method: 'setFirewallRules', retryOnAmbiguous: true, attemptTimeoutMs: 60_000 },\n () => client.setFirewallRules(firewallId, sshOnlyInboundRule(sourceCidr)),\n );\n}\n\n/**\n * Delete a per-box firewall. Idempotent on 404 (the API surfaces it as a\n * `not_found` error which the retry classifier won't retry; we swallow it\n * here so destroy paths don't need a special-case).\n *\n * Hetzner returns 409 `conflict` if the firewall is still attached to a\n * server when we try to delete it — `deleteServer()` returns as soon as the\n * delete action is *enqueued*, not after the server's firewall attachment\n * is torn down, so a quick subsequent `deleteFirewall()` will collide.\n * We poll for a short window (default 60s, intervals doubled to 8s) to\n * cover the typical 5–15s detach lag before giving up.\n */\nexport async function deletePerBoxFirewall(\n client: HetznerClient,\n firewallId: number,\n opts: { detachWaitMs?: number } = {},\n): Promise<void> {\n const deadline = Date.now() + (opts.detachWaitMs ?? 60_000);\n let interval = 1_000;\n while (true) {\n try {\n await withHetznerRetry(\n { method: 'deleteFirewall', retryOnAmbiguous: true, attemptTimeoutMs: 30_000 },\n () => client.deleteFirewall(firewallId),\n );\n return;\n } catch (err) {\n if (err instanceof HetznerApiError && (err.statusCode === 404 || err.code === 'not_found')) {\n return;\n }\n const stillAttached =\n err instanceof HetznerApiError &&\n (err.statusCode === 409 ||\n err.code === 'conflict' ||\n err.code === 'resource_in_use');\n if (stillAttached && Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, interval));\n interval = Math.min(interval * 2, 8_000);\n continue;\n }\n throw err;\n }\n }\n}\n\n/**\n * Normalize a source spec into a CIDR. Accepts:\n * - bare IPv4 → appends `/32`\n * - bare IPv6 → appends `/128`\n * - already-CIDR (anything with `/`) → returned as-is\n *\n * Whitespace is trimmed. Does **not** validate the address itself — that's\n * either the API's job (it'll reject bad CIDRs with a clear `validation`\n * error) or `detectEgressIp`'s job (it only returns valid IPv4/IPv6).\n */\nexport function normalizeSourceCidr(raw: string): string {\n const trimmed = raw.trim();\n if (trimmed.includes('/')) return trimmed;\n if (trimmed.includes(':')) return `${trimmed}/128`;\n return `${trimmed}/32`;\n}\n","/**\n * Resolver for the on-disk files we need to ship into a fresh VPS during\n * `prepareHetzner()` — same shape as the docker provider's runtime/docker\n * staging, but lighter (no Dockerfile build, just a flat tarball of files\n * to scp into /tmp).\n *\n * Lookup order for each file:\n * 1. The CLI's staged runtime tree: `<cliRoot>/runtime/hetzner/...`\n * (populated by `apps/cli/scripts/stage-runtime.mjs`).\n * 2. The monorepo source tree (dev fallback): the file's canonical\n * package-relative path under `packages/`.\n *\n * Failure mode: any missing file throws a clear error naming the lookup\n * paths so a partial dev rebuild is obvious to debug.\n */\n\nimport { existsSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst SELF = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Locate the staged `runtime/hetzner/` tree. Two candidates:\n *\n * 1. Bundled CLI: the hetzner module is inlined into apps/cli's dist/, so\n * `dirname(import.meta.url)` is `<cliRoot>/dist`; the staged runtime\n * sits at `<cliRoot>/runtime/hetzner`.\n * 2. Workspace dev: this module's dist is at\n * `packages/sandbox-hetzner/dist/`. There's no staged runtime there;\n * callers fall through to the monorepo source paths in\n * `candidatesFor()`. Returns undefined in that case.\n *\n * The CLI doesn't have to pass `cliRuntimeRoot` explicitly — this helper\n * picks it up by inspecting where the module was loaded from.\n */\nexport function findStagedCliRuntimeRoot(): string | undefined {\n const candidates = [\n resolve(SELF, '..', 'runtime'), // <cliRoot>/dist/.. → <cliRoot> then /runtime\n resolve(SELF, '..', '..', 'runtime'), // chunk-NNNN.js at <cliRoot>/dist/<sub>/.. → <cliRoot>/runtime\n ];\n for (const c of candidates) {\n if (existsSync(resolve(c, 'hetzner', 'scripts', 'install-box.sh'))) return c;\n }\n return undefined;\n}\n\n/**\n * Each runtime asset has a stable, well-known destination basename in\n * `/tmp` on the prepare VPS and is resolved from one of N candidate\n * source paths on the host.\n */\nexport interface RuntimeAsset {\n /** Logical name (used in error messages + log lines). */\n name: string;\n /** Basename on the prepare VPS (under /tmp/). */\n remoteBasename: string;\n /** Optional file mode at scp-time. */\n remoteMode?: number;\n}\n\nexport const RUNTIME_ASSETS: readonly RuntimeAsset[] = [\n { name: 'install-box.sh', remoteBasename: 'agentbox-install.sh', remoteMode: 0o755 },\n { name: 'agentbox-ctl', remoteBasename: 'agentbox-ctl', remoteMode: 0o755 },\n { name: 'agentbox-vnc-start', remoteBasename: 'agentbox-vnc-start', remoteMode: 0o755 },\n { name: 'agentbox-dockerd-start', remoteBasename: 'agentbox-dockerd-start', remoteMode: 0o755 },\n { name: 'agentbox-checkpoint-cleanup', remoteBasename: 'agentbox-checkpoint-cleanup', remoteMode: 0o755 },\n { name: 'agentbox-open', remoteBasename: 'agentbox-open', remoteMode: 0o755 },\n { name: 'gh-shim', remoteBasename: 'agentbox-gh-shim', remoteMode: 0o755 },\n { name: 'git-shim', remoteBasename: 'agentbox-git-shim', remoteMode: 0o755 },\n { name: 'custom-system-CLAUDE.md', remoteBasename: 'agentbox-custom-CLAUDE.md', remoteMode: 0o644 },\n { name: 'claude-managed-settings.json', remoteBasename: 'agentbox-managed-settings.json', remoteMode: 0o644 },\n { name: 'agentbox-codex-hooks.json', remoteBasename: 'agentbox-codex-hooks.json', remoteMode: 0o644 },\n { name: 'agentbox-setup-skill.md', remoteBasename: 'agentbox-setup-skill.md', remoteMode: 0o644 },\n] as const;\n\nexport interface ResolvedAsset extends RuntimeAsset {\n localPath: string;\n}\n\n/**\n * Build the candidate search paths for a given asset. Tries CLI runtime\n * first, then the monorepo source tree. The first one that exists wins.\n *\n * `cliRuntimeRoot` is provided by the caller because we don't know how the\n * embedding CLI lays out its dist (the @madarco/agentbox CLI puts dist + a\n * sibling runtime/ next to it; tests pass a tmp dir). Use the helper\n * `findCliRuntimeRoot()` below from the calling context that has the right\n * anchor (typically `import.meta.url` of an apps/cli module).\n */\nexport function candidatesFor(\n name: string,\n opts: { cliRuntimeRoot?: string; repoRoot?: string } = {},\n): string[] {\n const cliRoot = opts.cliRuntimeRoot;\n const monorepo = opts.repoRoot ?? guessRepoRoot();\n\n // Map logical → relative paths (relative to either anchor).\n const monorepoRelative: Record<string, string[]> = {\n 'install-box.sh': ['packages/sandbox-hetzner/scripts/install-box.sh'],\n 'agentbox-ctl': ['packages/ctl/dist/bin.cjs'],\n 'agentbox-vnc-start': ['packages/sandbox-docker/scripts/agentbox-vnc-start'],\n 'agentbox-dockerd-start': ['packages/sandbox-docker/scripts/agentbox-dockerd-start'],\n 'agentbox-checkpoint-cleanup': ['packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup'],\n 'agentbox-open': ['packages/sandbox-docker/scripts/agentbox-open'],\n 'gh-shim': ['packages/sandbox-docker/scripts/gh-shim'],\n 'git-shim': ['packages/sandbox-docker/scripts/git-shim'],\n 'custom-system-CLAUDE.md': ['packages/sandbox-hetzner/scripts/custom-system-CLAUDE.md'],\n 'claude-managed-settings.json': ['packages/sandbox-docker/scripts/claude-managed-settings.json'],\n 'agentbox-codex-hooks.json': ['packages/sandbox-docker/scripts/agentbox-codex-hooks.json'],\n 'agentbox-setup-skill.md': ['apps/cli/share/agentbox-setup/SKILL.md'],\n };\n\n // CLI-runtime-tree relative paths (mirrors the staging layout).\n const cliRelative: Record<string, string[]> = {\n 'install-box.sh': ['hetzner/scripts/install-box.sh'],\n 'agentbox-ctl': ['hetzner/ctl.cjs'],\n 'agentbox-vnc-start': ['hetzner/agentbox-vnc-start', 'docker/packages/sandbox-docker/scripts/agentbox-vnc-start'],\n 'agentbox-dockerd-start': ['hetzner/agentbox-dockerd-start', 'docker/packages/sandbox-docker/scripts/agentbox-dockerd-start'],\n 'agentbox-checkpoint-cleanup': ['hetzner/agentbox-checkpoint-cleanup', 'docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup'],\n 'agentbox-open': ['hetzner/agentbox-open', 'docker/packages/sandbox-docker/scripts/agentbox-open'],\n 'gh-shim': ['hetzner/gh-shim', 'docker/packages/sandbox-docker/scripts/gh-shim'],\n 'git-shim': ['hetzner/git-shim', 'docker/packages/sandbox-docker/scripts/git-shim'],\n 'custom-system-CLAUDE.md': ['hetzner/custom-system-CLAUDE.md'],\n 'claude-managed-settings.json': ['hetzner/claude-managed-settings.json', 'docker/packages/sandbox-docker/scripts/claude-managed-settings.json'],\n 'agentbox-codex-hooks.json': ['hetzner/agentbox-codex-hooks.json', 'docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json'],\n 'agentbox-setup-skill.md': ['hetzner/agentbox-setup-skill.md', 'docker/apps/cli/share/agentbox-setup/SKILL.md'],\n };\n\n const out: string[] = [];\n if (cliRoot) {\n for (const rel of cliRelative[name] ?? []) out.push(resolve(cliRoot, rel));\n }\n for (const rel of monorepoRelative[name] ?? []) out.push(resolve(monorepo, rel));\n return out;\n}\n\n/**\n * Resolve every runtime asset to its absolute on-host path. Throws an\n * actionable error if any asset can't be found (lists every path tried).\n */\nexport function resolveRuntimeAssets(opts: {\n cliRuntimeRoot?: string;\n repoRoot?: string;\n} = {}): ResolvedAsset[] {\n const out: ResolvedAsset[] = [];\n const missing: Array<{ name: string; tried: string[] }> = [];\n for (const asset of RUNTIME_ASSETS) {\n const cands = candidatesFor(asset.name, opts);\n const hit = cands.find((p) => existsSync(p));\n if (!hit) {\n missing.push({ name: asset.name, tried: cands });\n continue;\n }\n out.push({ ...asset, localPath: hit });\n }\n if (missing.length > 0) {\n const lines = missing.flatMap((m) => [` - ${m.name}: tried`, ...m.tried.map((p) => ` ${p}`)]);\n throw new Error(\n `hetzner: could not resolve runtime assets — these files are needed to install on the prepare VPS:\\n` +\n lines.join('\\n') +\n `\\n\\nIf you are running from the monorepo, ensure \\`pnpm -w build\\` has run so packages/ctl/dist/bin.cjs exists. ` +\n `If you are running from a published CLI bundle, the runtime/hetzner tree should be staged automatically.`,\n );\n }\n return out;\n}\n\n/** Best-effort: walk up from this file looking for `pnpm-workspace.yaml`. */\nfunction guessRepoRoot(): string {\n let cur = SELF;\n for (let i = 0; i < 8; i++) {\n if (existsSync(resolve(cur, 'pnpm-workspace.yaml'))) return cur;\n const parent = dirname(cur);\n if (parent === cur) break;\n cur = parent;\n }\n return SELF; // fall through to itself; resolution will fail with a clear error\n}\n","/**\n * Persisted record of what `agentbox prepare --provider hetzner` has built.\n * Lives at `~/.agentbox/hetzner-prepared.json` so the auto-prepare gate\n * (`ensureHetznerBaseSnapshot()`) and runtime image resolution can see it.\n *\n * Only the shared `base` snapshot is recorded here — built once per Hetzner\n * project / API token: Ubuntu + deps + agentbox-ctl + agents + agent-browser,\n * baked from `install-box.sh`.\n *\n * The per-project snapshot tier is NOT a separate registry: it's the existing\n * `agentbox checkpoint create --set-default` + `box.defaultCheckpointHetzner`\n * flow (see `docs/cloud-create-flow.md` §\"base vs project snapshot\"), and\n * auto-capture at the end of setup is driven by the `/agentbox-setup` skill\n * (`agentbox-ctl checkpoint --set-default`), cross-provider. So there's no\n * `projects[<hash>]` map here.\n *\n * Schema versioned so future shape changes can migrate.\n */\n\nimport { computeContextSha256, preparedStatePathFor, readPreparedStateRaw, writePreparedStateRaw } from '@agentbox/sandbox-core';\nimport { findStagedCliRuntimeRoot, resolveRuntimeAssets } from './runtime-assets.js';\n\n/**\n * Schema history:\n * 1 — `base.imageId`, `base.description`, `base.createdAt`,\n * `base.installScriptSha256?`\n * 2 — `base.installScriptSha256` → `base.contextSha256` (now covers every\n * asset we scp'd in, not just the install script); `base.cliVersion`\n * and `base.cliCommit?` added so we can warn when an old snapshot\n * predates the running CLI.\n *\n * Read-time migration is lossy in one direction: a schema-1 file is lifted\n * to schema 2 by *renaming* `installScriptSha256` to `contextSha256`. The\n * hash doesn't change but the meaning narrows (install script only → full\n * asset list), so the next `agentbox prepare --provider hetzner` run will\n * recompute and overwrite. A legacy `projects` key (an early, never-wired\n * per-project tier) is simply ignored — removing the field doesn't break\n * reads, so no schema bump is needed.\n */\nconst SCHEMA = 2 as const;\n\nexport interface PreparedBaseSnapshot {\n /** Hetzner image id (numeric — opaque, but stable across `getImage` calls). */\n imageId: number;\n /** User-facing description (matches the firewall-dashboard rows). */\n description: string;\n /** ISO timestamp of bake-completion. */\n createdAt: string;\n /**\n * Deterministic SHA-256 of the build context (every file scp'd into the\n * prepare VPS). Rebuild when it changes.\n */\n contextSha256?: string;\n /** CLI version that produced this snapshot (informational). */\n cliVersion?: string;\n /** Git short SHA of the CLI build (informational). */\n cliCommit?: string;\n}\n\nexport interface PreparedHetznerState {\n schema: typeof SCHEMA;\n /** The shared base snapshot. Absent until first `agentbox prepare`. */\n base?: PreparedBaseSnapshot;\n}\n\ninterface LegacyV1Base {\n imageId: number;\n description: string;\n createdAt: string;\n installScriptSha256?: string;\n}\n\ninterface LegacyV1State {\n schema: 1;\n base?: LegacyV1Base;\n}\n\nexport function preparedStatePath(): string {\n return preparedStatePathFor('hetzner');\n}\n\nexport function readPreparedState(): PreparedHetznerState {\n const raw = readPreparedStateRaw('hetzner');\n if (raw === null || typeof raw !== 'object') return { schema: SCHEMA };\n const parsed = raw as Partial<PreparedHetznerState> | LegacyV1State;\n if ((parsed as { schema?: unknown }).schema === 1) {\n const v1 = parsed as LegacyV1State;\n return migrateFromV1(v1);\n }\n if (parsed.schema !== SCHEMA) {\n // Unknown schema: don't crash, just refuse to read — the file will be\n // overwritten on the next successful prepare.\n return { schema: SCHEMA };\n }\n return {\n schema: SCHEMA,\n base: parsed.base,\n };\n}\n\nfunction migrateFromV1(v1: LegacyV1State): PreparedHetznerState {\n // The v1 `installScriptSha256` covered only `install-box.sh`, not the full\n // asset list a v2 `contextSha256` represents. Lifting it forward as a\n // placeholder fingerprint means the next prepare run will mismatch and\n // rebuild — exactly what we want, since the broader hash semantics also\n // changed.\n const base: PreparedBaseSnapshot | undefined = v1.base\n ? {\n imageId: v1.base.imageId,\n description: v1.base.description,\n createdAt: v1.base.createdAt,\n contextSha256: v1.base.installScriptSha256,\n }\n : undefined;\n return {\n schema: SCHEMA,\n base,\n };\n}\n\nexport function writePreparedState(state: PreparedHetznerState): void {\n writePreparedStateRaw('hetzner', state);\n}\n\n/**\n * Convenience helper: update one field of the state without forcing callers\n * to read/merge/write themselves.\n */\nexport function updatePreparedState(mutate: (s: PreparedHetznerState) => void): void {\n const s = readPreparedState();\n mutate(s);\n writePreparedState(s);\n}\n\n/**\n * Compute the CURRENT build-context fingerprint for the hetzner base snapshot\n * (the SHA over every file `prepare` would scp into the prepare VPS).\n * Side-effect-free — never builds. Returns `undefined` when the runtime\n * assets can't be resolved (dev tree without `pnpm -w build`) so the CLI\n * can degrade to \"can't tell, don't nag\".\n *\n * Used by `evaluateBaseFreshness` to compare against the stored value in\n * `hetzner-prepared.json.base.contextSha256`. Must produce a byte-identical\n * hash to the one `prepare` writes — both go through the same\n * `resolveRuntimeAssets` + `computeContextSha256` chain.\n */\nexport async function currentHetznerBaseFingerprintLive(): Promise<string | undefined> {\n try {\n const assets = resolveRuntimeAssets({ cliRuntimeRoot: findStagedCliRuntimeRoot() });\n return await computeContextSha256(\n assets.map((a) => ({ rel: a.name, abs: a.localPath })),\n );\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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","names":["existsSync","readFileSync","homedir","resolve","dirname","opts","existsSync","readFileSync","resolve","homedir","log","raceTimeout","dirname"]}
|
|
@@ -9,17 +9,24 @@ import {
|
|
|
9
9
|
CREDENTIALS_BACKUP_FILE,
|
|
10
10
|
OPENCODE_CREDENTIALS_BACKUP_FILE,
|
|
11
11
|
OPENCODE_FORWARDED_ENV_KEYS,
|
|
12
|
+
SHARED_CLAUDE_VOLUME,
|
|
13
|
+
SHARED_CODEX_VOLUME,
|
|
14
|
+
SHARED_OPENCODE_VOLUME,
|
|
12
15
|
buildHostEnvFindArgs,
|
|
13
16
|
buildHostSyncManifest,
|
|
14
17
|
buildTmuxConfigShellSnippet,
|
|
15
18
|
computeSyncDelta,
|
|
16
19
|
ensureRelay,
|
|
20
|
+
extractCodexCredentials,
|
|
21
|
+
extractOpencodeCredentials,
|
|
17
22
|
forgetBoxFromRelay,
|
|
18
23
|
generateBoxId,
|
|
19
24
|
generateRelayToken,
|
|
20
25
|
generateVncPassword,
|
|
21
26
|
hashProjectPath,
|
|
27
|
+
hostClaudeBackupExpired,
|
|
22
28
|
isRealAgentCredential,
|
|
29
|
+
loadEffectiveConfig,
|
|
23
30
|
portlessAlias,
|
|
24
31
|
portlessGetUrl,
|
|
25
32
|
portlessUnalias,
|
|
@@ -27,15 +34,18 @@ import {
|
|
|
27
34
|
registerBoxWithRelay,
|
|
28
35
|
sanitizeMnemonic,
|
|
29
36
|
stageClaudeCredentialsForUpload,
|
|
37
|
+
stageClaudeJsonOnlyForUpload,
|
|
30
38
|
stageClaudeStaticForUpload,
|
|
31
39
|
stageCodexCredentialsForUpload,
|
|
32
40
|
stageCodexStaticForUpload,
|
|
33
41
|
stageDynamicSyncTarball,
|
|
34
42
|
stageOpencodeCredentialsForUpload,
|
|
35
43
|
stageOpencodeStateForUpload,
|
|
36
|
-
stageOpencodeStaticForUpload
|
|
37
|
-
|
|
44
|
+
stageOpencodeStaticForUpload,
|
|
45
|
+
syncClaudeCredentials
|
|
46
|
+
} from "./chunk-MLMFNN4T.js";
|
|
38
47
|
import {
|
|
48
|
+
DEFAULT_BOX_IMAGE,
|
|
39
49
|
allocateProjectIndex,
|
|
40
50
|
detectGitRepos,
|
|
41
51
|
readCliStamp,
|
|
@@ -43,7 +53,7 @@ import {
|
|
|
43
53
|
readState,
|
|
44
54
|
recordBox,
|
|
45
55
|
removeBoxRecord
|
|
46
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-XKH7NTT7.js";
|
|
47
57
|
|
|
48
58
|
// ../../packages/sandbox-cloud/dist/index.js
|
|
49
59
|
import { basename as basename2 } from "path";
|
|
@@ -113,11 +123,12 @@ var SEED_MARKER = ".agentbox-seeded-at";
|
|
|
113
123
|
async function ensureAgentVolumesForCloud(backend, opts = {}) {
|
|
114
124
|
const log = opts.onLog ?? (() => {
|
|
115
125
|
});
|
|
126
|
+
const allAgents = AGENT_SPECS.map((s) => s.kind);
|
|
116
127
|
if (typeof backend.ensureVolume !== "function") {
|
|
117
128
|
log(
|
|
118
|
-
`cloud backend '${backend.name}' has no volume primitive \u2014 agent credentials
|
|
129
|
+
`cloud backend '${backend.name}' has no volume primitive \u2014 agent credentials seeded per-create only`
|
|
119
130
|
);
|
|
120
|
-
return { mounts: [], env: buildForwardedEnv(
|
|
131
|
+
return { mounts: [], env: buildForwardedEnv(allAgents), agents: allAgents };
|
|
121
132
|
}
|
|
122
133
|
let volumeId;
|
|
123
134
|
try {
|
|
@@ -133,8 +144,7 @@ async function ensureAgentVolumesForCloud(backend, opts = {}) {
|
|
|
133
144
|
mountPath: spec.credentialsMountPath,
|
|
134
145
|
subpath: spec.credentialsSubpath
|
|
135
146
|
}));
|
|
136
|
-
|
|
137
|
-
return { mounts, env: buildForwardedEnv(agents), agents };
|
|
147
|
+
return { mounts, env: buildForwardedEnv(allAgents), agents: allAgents };
|
|
138
148
|
}
|
|
139
149
|
function buildForwardedEnv(agents) {
|
|
140
150
|
const env = {};
|
|
@@ -153,14 +163,51 @@ function buildForwardedEnv(agents) {
|
|
|
153
163
|
return env;
|
|
154
164
|
}
|
|
155
165
|
async function seedAgentVolumesIfFresh(backend, handle, opts = {}) {
|
|
166
|
+
const log = opts.onLog ?? (() => {
|
|
167
|
+
});
|
|
156
168
|
const wanted = new Set(opts.agents ?? AGENT_SPECS.map((s) => s.kind));
|
|
157
169
|
const specs = AGENT_SPECS.filter((s) => wanted.has(s.kind));
|
|
158
|
-
await Promise.
|
|
170
|
+
await Promise.allSettled(
|
|
171
|
+
specs.map(
|
|
172
|
+
(spec) => seedCredentialsOne(backend, handle, spec, opts).catch((err) => {
|
|
173
|
+
log(
|
|
174
|
+
`${spec.kind}: credentials seed failed (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
175
|
+
);
|
|
176
|
+
})
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
async function refreshAgentCredentialsBackup(opts = {}) {
|
|
181
|
+
const log = opts.onLog ?? (() => {
|
|
182
|
+
});
|
|
183
|
+
if (!await hostClaudeBackupExpired()) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
log("claude: host credentials backup expired \u2014 refreshing from docker shared volume");
|
|
187
|
+
const image = DEFAULT_BOX_IMAGE;
|
|
188
|
+
try {
|
|
189
|
+
const r = await syncClaudeCredentials({ volume: SHARED_CLAUDE_VOLUME }, { image, isolate: false });
|
|
190
|
+
if (r.direction === "extracted") {
|
|
191
|
+
log("claude: refreshed host credentials backup from docker shared volume");
|
|
192
|
+
} else if (r.direction === "noop") {
|
|
193
|
+
log("claude: no docker shared volume to refresh from (continuing with existing backup)");
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await extractCodexCredentials(SHARED_CODEX_VOLUME, image);
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
await extractOpencodeCredentials(SHARED_OPENCODE_VOLUME, image);
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
159
205
|
}
|
|
160
206
|
async function seedCredentialsOne(backend, handle, spec, opts) {
|
|
161
207
|
const log = opts.onLog ?? (() => {
|
|
162
208
|
});
|
|
163
|
-
|
|
209
|
+
const hasVolume = typeof backend.ensureVolume === "function";
|
|
210
|
+
if (hasVolume && !opts.force) {
|
|
164
211
|
const probe = await backend.exec(
|
|
165
212
|
handle,
|
|
166
213
|
`test -f ${spec.credentialsMountPath}/${SEED_MARKER}`
|
|
@@ -186,27 +233,44 @@ async function seedCredentialsOne(backend, handle, spec, opts) {
|
|
|
186
233
|
}
|
|
187
234
|
const sizeKB = (tarSize / 1024).toFixed(1);
|
|
188
235
|
log(`${spec.kind}: uploading ${sizeKB} KB credentials tarball`);
|
|
189
|
-
process.stderr.write(`[agent-creds] ${spec.kind}: uploading ${sizeKB} KB...
|
|
190
|
-
`);
|
|
191
236
|
const t0 = Date.now();
|
|
192
237
|
const remoteTar = `/tmp/agentbox-${spec.kind}-creds.tar.gz`;
|
|
193
|
-
|
|
238
|
+
try {
|
|
239
|
+
await backend.uploadFile(handle, staged.tarballPath, remoteTar);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const msg = `${spec.kind}: credentials upload failed (${err instanceof Error ? err.message : String(err)}); agent falls back to interactive login`;
|
|
242
|
+
log(msg);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
194
245
|
const upDt = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
log(`${spec.kind}: upload done in ${upDt}s`);
|
|
247
|
+
const extractCmd = hasVolume ? (
|
|
248
|
+
// Daytona volumes are S3-backed FUSE and reject chmod/utime. The
|
|
249
|
+
// credentials payload is one small file, so we extract straight into
|
|
250
|
+
// the mount with `cp` (not tar — tar would chmod the parent dir during
|
|
251
|
+
// delayed-set-stat and abort with EPERM). Two-step: tar into a local-fs
|
|
252
|
+
// staging dir, then cp the file across. Marker tracks idempotency.
|
|
253
|
+
(() => {
|
|
254
|
+
const stageDir = `/tmp/agentbox-creds-stage-${spec.kind}`;
|
|
255
|
+
return `set -e; rm -rf ${stageDir}; mkdir -p ${stageDir}; tar -xzf ${remoteTar} -C ${stageDir}; cp -r ${stageDir}/. ${spec.credentialsMountPath}/; rm -rf ${stageDir}; date -u +%FT%TZ > ${spec.credentialsMountPath}/${SEED_MARKER}; rm -f ${remoteTar}`;
|
|
256
|
+
})()
|
|
257
|
+
) : (
|
|
258
|
+
// Ephemeral FS: extract straight into the box-baked `~/.agentbox-creds/
|
|
259
|
+
// <agent>/` dir. `sudo -u vscode` ensures the on-disk file ends up
|
|
260
|
+
// vscode-owned regardless of which user `backend.exec` runs as
|
|
261
|
+
// (e2b: vscode, vercel: vscode, hetzner: vscode via ssh — sudo
|
|
262
|
+
// works on all three because vscode has passwordless sudo). The
|
|
263
|
+
// `--no-same-permissions --no-same-owner -m` flags mirror what
|
|
264
|
+
// vercel/hetzner did in their old custom pushers.
|
|
265
|
+
`set -e; sudo -u vscode mkdir -p ${spec.credentialsMountPath}; sudo -u vscode tar -xzf ${remoteTar} -C ${spec.credentialsMountPath} --no-same-permissions --no-same-owner -m; rm -f ${remoteTar}`
|
|
266
|
+
);
|
|
199
267
|
const extract = await backend.exec(handle, extractCmd);
|
|
200
268
|
if (extract.exitCode !== 0) {
|
|
201
269
|
const msg = `${spec.kind}: credentials extract failed (exit ${String(extract.exitCode)}); agent falls back to interactive login. stdout: ${extract.stdout.slice(-200)} stderr: ${extract.stderr.slice(-200)}`;
|
|
202
270
|
log(msg);
|
|
203
|
-
process.stderr.write(`[agent-creds] ${msg}
|
|
204
|
-
`);
|
|
205
271
|
return;
|
|
206
272
|
}
|
|
207
|
-
log(`${spec.kind}: credentials seeded
|
|
208
|
-
process.stderr.write(`[agent-creds] ${spec.kind}: credentials seeded
|
|
209
|
-
`);
|
|
273
|
+
log(`${spec.kind}: credentials seeded`);
|
|
210
274
|
} finally {
|
|
211
275
|
await staged.cleanup();
|
|
212
276
|
}
|
|
@@ -349,6 +413,38 @@ async function seedDynamicConfig(backend, handle, opts) {
|
|
|
349
413
|
);
|
|
350
414
|
}
|
|
351
415
|
}
|
|
416
|
+
var REMOTE_TAR2 = "/tmp/agentbox-claude-json.tar.gz";
|
|
417
|
+
var BOX_CLAUDE_DIR = "/home/vscode/.claude";
|
|
418
|
+
async function seedClaudeJsonAtCreate(backend, handle, opts = {}) {
|
|
419
|
+
const log = opts.onLog ?? (() => {
|
|
420
|
+
});
|
|
421
|
+
let staged = null;
|
|
422
|
+
try {
|
|
423
|
+
staged = await stageClaudeJsonOnlyForUpload({ hostWorkspace: opts.hostWorkspace });
|
|
424
|
+
if (staged.tarballPath === null) {
|
|
425
|
+
log("claude: no _claude.json overlay (host has no claude config)");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
await backend.uploadFile(handle, staged.tarballPath, REMOTE_TAR2);
|
|
429
|
+
const extract = await backend.exec(
|
|
430
|
+
handle,
|
|
431
|
+
`set -e; mkdir -p ${BOX_CLAUDE_DIR}; tar -xzf ${REMOTE_TAR2} -C ${BOX_CLAUDE_DIR}; rm -f ${REMOTE_TAR2}`
|
|
432
|
+
);
|
|
433
|
+
if (extract.exitCode !== 0) {
|
|
434
|
+
log(
|
|
435
|
+
`claude: _claude.json overlay extract failed (exit ${String(extract.exitCode)}); stderr: ${extract.stderr.slice(-200)}`
|
|
436
|
+
);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
log("claude: _claude.json overlay seeded");
|
|
440
|
+
} catch (err) {
|
|
441
|
+
log(
|
|
442
|
+
`claude: _claude.json overlay failed (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
443
|
+
);
|
|
444
|
+
} finally {
|
|
445
|
+
if (staged) await staged.cleanup();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
352
448
|
function quoteShellArgv(argv) {
|
|
353
449
|
return argv.map(quoteShellArg).join(" ");
|
|
354
450
|
}
|
|
@@ -591,7 +687,8 @@ async function uploadOneEntry(args) {
|
|
|
591
687
|
const isDir = entry.kind === "dir";
|
|
592
688
|
const parentDir = isDir ? boxDest : dirnameUnix(boxDest);
|
|
593
689
|
const localTar = join4(args.stageDir, `carry-${String(args.index)}.tar`);
|
|
594
|
-
const
|
|
690
|
+
const excludeArgs = isDir ? (entry.exclude ?? []).map((p) => `--exclude=${p}`) : [];
|
|
691
|
+
const tarArgs = isDir ? ["-C", entry.absSrc, "-cf", localTar, ...excludeArgs, "."] : ["-C", dirnameUnix(entry.absSrc), "-cf", localTar, basenameUnix(entry.absSrc)];
|
|
595
692
|
const packed = await execa3("tar", tarArgs, { reject: false });
|
|
596
693
|
if (packed.exitCode !== 0) {
|
|
597
694
|
throw new Error(`tar pack failed: ${String(packed.stderr).slice(0, 300)}`);
|
|
@@ -621,7 +718,8 @@ async function uploadOneEntry(args) {
|
|
|
621
718
|
}
|
|
622
719
|
parts.push(`rm -f ${remoteTar}`);
|
|
623
720
|
const cmd = parts.join(" && ");
|
|
624
|
-
const
|
|
721
|
+
const wantsRoot = args.backend.name === "vercel" || args.backend.name === "e2b";
|
|
722
|
+
const execOpts = wantsRoot ? { user: "root" } : void 0;
|
|
625
723
|
const res = await args.backend.exec(args.handle, cmd, execOpts);
|
|
626
724
|
if (res.exitCode !== 0) {
|
|
627
725
|
throw new Error(
|
|
@@ -674,7 +772,7 @@ function isPlainObject(v) {
|
|
|
674
772
|
}
|
|
675
773
|
var REMOTE_UP_TAR = "/tmp/agentbox-cp-up.tar.gz";
|
|
676
774
|
var REMOTE_DOWN_TAR = "/tmp/agentbox-cp-down.tar.gz";
|
|
677
|
-
async function uploadToCloudBox(backend, handle, hostSrc, boxDst) {
|
|
775
|
+
async function uploadToCloudBox(backend, handle, hostSrc, boxDst, exclude) {
|
|
678
776
|
const srcAbs = hostResolve(hostSrc);
|
|
679
777
|
if (!existsSync(srcAbs)) throw new Error(`source not found: ${hostSrc}`);
|
|
680
778
|
const srcBasename = hostBasename(srcAbs);
|
|
@@ -692,7 +790,8 @@ async function uploadToCloudBox(backend, handle, hostSrc, boxDst) {
|
|
|
692
790
|
const stage = await mkdtemp4(hostJoin(tmpdir4(), "agentbox-cp-up-"));
|
|
693
791
|
const localTar = hostJoin(stage, "payload.tar.gz");
|
|
694
792
|
try {
|
|
695
|
-
|
|
793
|
+
const excludeArgs = (exclude ?? []).map((p) => `--exclude=${p}`);
|
|
794
|
+
await execa4("tar", ["-C", srcParent, "-czf", localTar, ...excludeArgs, srcBasename], {
|
|
696
795
|
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
697
796
|
});
|
|
698
797
|
await backend.uploadFile(handle, localTar, REMOTE_UP_TAR);
|
|
@@ -746,7 +845,7 @@ async function pullCloudDirContents(backend, handle, boxSrcDir, hostDstDir) {
|
|
|
746
845
|
}
|
|
747
846
|
return { finalPath: dstAbs };
|
|
748
847
|
}
|
|
749
|
-
async function downloadFromCloudBox(backend, handle, boxSrc, hostDst) {
|
|
848
|
+
async function downloadFromCloudBox(backend, handle, boxSrc, hostDst, exclude) {
|
|
750
849
|
const srcBasename = posix.basename(boxSrc);
|
|
751
850
|
const srcParent = posix.dirname(boxSrc);
|
|
752
851
|
const dstAbs = hostResolve(hostDst);
|
|
@@ -765,10 +864,11 @@ async function downloadFromCloudBox(backend, handle, boxSrc, hostDst) {
|
|
|
765
864
|
const stage = await mkdtemp4(hostJoin(tmpdir4(), "agentbox-cp-down-"));
|
|
766
865
|
const localTar = hostJoin(stage, "payload.tar.gz");
|
|
767
866
|
try {
|
|
867
|
+
const excludeArgs = (exclude ?? []).map((p) => `--exclude=${quoteShellArg(p)}`).join(" ");
|
|
768
868
|
const packScript = [
|
|
769
869
|
`set -euo pipefail`,
|
|
770
870
|
`cd ${quoteShellArg(srcParent)}`,
|
|
771
|
-
`tar -czf ${quoteShellArg(REMOTE_DOWN_TAR)} ${quoteShellArg(srcBasename)}`
|
|
871
|
+
`tar -czf ${quoteShellArg(REMOTE_DOWN_TAR)} ${excludeArgs} ${quoteShellArg(srcBasename)}`
|
|
772
872
|
].join("\n");
|
|
773
873
|
const r = await backend.exec(handle, bashScript(packScript));
|
|
774
874
|
if (r.exitCode !== 0) {
|
|
@@ -847,14 +947,15 @@ async function launchCloudVncDaemon(args) {
|
|
|
847
947
|
`mkdir -p /var/log/agentbox 2>/dev/null || true`,
|
|
848
948
|
`nohup /usr/local/bin/agentbox-vnc-start >> /var/log/agentbox/vnc-start.log 2>&1 &`,
|
|
849
949
|
`disown`,
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
|
|
950
|
+
// Probe for websockify to bind 6080. ~15s ceiling: E2B's Python venv
|
|
951
|
+
// startup (websockify is a pure-python proxy launched from a venv) takes
|
|
952
|
+
// ~7-9s before the socket binds, so a 5s ceiling false-negatives every
|
|
953
|
+
// create. Docker/hetzner/daytona/vercel come up well inside this window.
|
|
954
|
+
`for _ in $(seq 1 150); do`,
|
|
854
955
|
` if (echo > /dev/tcp/127.0.0.1/6080) 2>/dev/null; then echo ready; exit 0; fi`,
|
|
855
956
|
` sleep 0.1`,
|
|
856
957
|
`done`,
|
|
857
|
-
`echo "websockify did not bind 6080 within
|
|
958
|
+
`echo "websockify did not bind 6080 within 15s" >&2`,
|
|
858
959
|
`exit 1`
|
|
859
960
|
].join("\n");
|
|
860
961
|
const r = await args.backend.exec(args.handle, bashScript(script));
|
|
@@ -1295,6 +1396,14 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1295
1396
|
}
|
|
1296
1397
|
};
|
|
1297
1398
|
await recordBox(next);
|
|
1399
|
+
if (opts.launchDockerd !== false) {
|
|
1400
|
+
try {
|
|
1401
|
+
const dockerd = await launchCloudDockerdDaemon({ backend, handle: h, timeoutMs: 6e4 });
|
|
1402
|
+
if (!dockerd.up) {
|
|
1403
|
+
}
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1298
1407
|
await launchCloudCtlDaemon({
|
|
1299
1408
|
backend,
|
|
1300
1409
|
handle: h,
|
|
@@ -1305,14 +1414,6 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1305
1414
|
bridgeToken: box.cloud?.bridgeToken,
|
|
1306
1415
|
webProxyPort: backend.webProxyPort
|
|
1307
1416
|
});
|
|
1308
|
-
if (opts.launchDockerd !== false) {
|
|
1309
|
-
try {
|
|
1310
|
-
const dockerd = await launchCloudDockerdDaemon({ backend, handle: h, timeoutMs: 6e4 });
|
|
1311
|
-
if (!dockerd.up) {
|
|
1312
|
-
}
|
|
1313
|
-
} catch {
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
1417
|
if (box.vncEnabled && box.vncPassword) {
|
|
1317
1418
|
try {
|
|
1318
1419
|
await launchCloudVncDaemon({ backend, handle: h, vncPassword: box.vncPassword });
|
|
@@ -1331,7 +1432,8 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1331
1432
|
previewToken: relayPreview.token,
|
|
1332
1433
|
bridgeToken: box.cloud.bridgeToken,
|
|
1333
1434
|
createdAt: box.createdAt,
|
|
1334
|
-
projectIndex: box.projectIndex
|
|
1435
|
+
projectIndex: box.projectIndex,
|
|
1436
|
+
autoApproveHostActions: box.autoApproveHostActions
|
|
1335
1437
|
});
|
|
1336
1438
|
} catch {
|
|
1337
1439
|
}
|
|
@@ -1450,6 +1552,7 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1450
1552
|
onLog: log
|
|
1451
1553
|
});
|
|
1452
1554
|
}
|
|
1555
|
+
await refreshAgentCredentialsBackup({ onLog: log });
|
|
1453
1556
|
if (agentVolumes.agents.length > 0) {
|
|
1454
1557
|
await seedAgentVolumesIfFresh(backend, handle, {
|
|
1455
1558
|
agents: agentVolumes.agents,
|
|
@@ -1458,6 +1561,10 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1458
1561
|
});
|
|
1459
1562
|
}
|
|
1460
1563
|
await seedOpencodeModelState(backend, handle, { onLog: log });
|
|
1564
|
+
await seedClaudeJsonAtCreate(backend, handle, {
|
|
1565
|
+
hostWorkspace: req.workspacePath,
|
|
1566
|
+
onLog: log
|
|
1567
|
+
});
|
|
1461
1568
|
await seedDynamicConfig(backend, handle, { workspacePath: req.workspacePath, onLog: log });
|
|
1462
1569
|
await seedGitIdentity(backend, handle, { hostRepo: req.workspacePath, onLog: log });
|
|
1463
1570
|
if (req.envFilesToImport && req.envFilesToImport.length > 0) {
|
|
@@ -1486,17 +1593,6 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1486
1593
|
carrySummary = { count: result.applied.length, entries: result.applied };
|
|
1487
1594
|
}
|
|
1488
1595
|
}
|
|
1489
|
-
log("launching agentbox-ctl daemon");
|
|
1490
|
-
await launchCloudCtlDaemon({
|
|
1491
|
-
backend,
|
|
1492
|
-
handle,
|
|
1493
|
-
boxId: id,
|
|
1494
|
-
boxName: name,
|
|
1495
|
-
relayUrl: `http://127.0.0.1:${String(8788)}`,
|
|
1496
|
-
relayToken,
|
|
1497
|
-
bridgeToken,
|
|
1498
|
-
webProxyPort: backend.webProxyPort
|
|
1499
|
-
});
|
|
1500
1596
|
if (opts.launchDockerd !== false) {
|
|
1501
1597
|
log("launching in-box dockerd");
|
|
1502
1598
|
try {
|
|
@@ -1509,6 +1605,17 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1509
1605
|
);
|
|
1510
1606
|
}
|
|
1511
1607
|
}
|
|
1608
|
+
log("launching agentbox-ctl daemon");
|
|
1609
|
+
await launchCloudCtlDaemon({
|
|
1610
|
+
backend,
|
|
1611
|
+
handle,
|
|
1612
|
+
boxId: id,
|
|
1613
|
+
boxName: name,
|
|
1614
|
+
relayUrl: `http://127.0.0.1:${String(8788)}`,
|
|
1615
|
+
relayToken,
|
|
1616
|
+
bridgeToken,
|
|
1617
|
+
webProxyPort: backend.webProxyPort
|
|
1618
|
+
});
|
|
1512
1619
|
const vncEnabled = req.vnc?.enabled !== false;
|
|
1513
1620
|
const vncPassword = vncEnabled ? generateVncPassword() : void 0;
|
|
1514
1621
|
if (vncEnabled && vncPassword) {
|
|
@@ -1584,6 +1691,7 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1584
1691
|
}
|
|
1585
1692
|
const state = await readState();
|
|
1586
1693
|
const projectIndex = req.projectRoot ? allocateProjectIndex(state, req.projectRoot) : void 0;
|
|
1694
|
+
const autoApproveHostActions = (await loadEffectiveConfig(req.projectRoot ?? req.workspacePath)).effective.box.autoApproveHostActions;
|
|
1587
1695
|
if (relayPreview) {
|
|
1588
1696
|
try {
|
|
1589
1697
|
await registerBoxWithRelay({
|
|
@@ -1596,7 +1704,8 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1596
1704
|
previewUrl: relayPreview.url,
|
|
1597
1705
|
previewToken: relayPreview.token,
|
|
1598
1706
|
bridgeToken,
|
|
1599
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1707
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1708
|
+
autoApproveHostActions
|
|
1600
1709
|
});
|
|
1601
1710
|
} catch (err) {
|
|
1602
1711
|
log(
|
|
@@ -1622,6 +1731,7 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1622
1731
|
relayToken,
|
|
1623
1732
|
withPlaywright: req.withPlaywright,
|
|
1624
1733
|
withEnv: req.withEnv,
|
|
1734
|
+
autoApproveHostActions: autoApproveHostActions ? true : void 0,
|
|
1625
1735
|
carry: carrySummary,
|
|
1626
1736
|
portlessAlias: portlessAliasName,
|
|
1627
1737
|
portlessUrl: portlessUrlResolved,
|
|
@@ -1772,11 +1882,11 @@ function createCloudProvider(backend, opts = {}) {
|
|
|
1772
1882
|
} : void 0;
|
|
1773
1883
|
return { argv: fullArgv, cleanup };
|
|
1774
1884
|
},
|
|
1775
|
-
async uploadPath(box, hostSrc, boxDst) {
|
|
1776
|
-
return uploadToCloudBox(backend, handleFor(box), hostSrc, boxDst);
|
|
1885
|
+
async uploadPath(box, hostSrc, boxDst, exclude) {
|
|
1886
|
+
return uploadToCloudBox(backend, handleFor(box), hostSrc, boxDst, exclude);
|
|
1777
1887
|
},
|
|
1778
|
-
async downloadPath(box, boxSrc, hostDst) {
|
|
1779
|
-
return downloadFromCloudBox(backend, handleFor(box), boxSrc, hostDst);
|
|
1888
|
+
async downloadPath(box, boxSrc, hostDst, exclude) {
|
|
1889
|
+
return downloadFromCloudBox(backend, handleFor(box), boxSrc, hostDst, exclude);
|
|
1780
1890
|
},
|
|
1781
1891
|
async downloadDirContents(box, boxSrc, hostDst) {
|
|
1782
1892
|
return pullCloudDirContents(backend, handleFor(box), boxSrc, hostDst);
|
|
@@ -1955,4 +2065,4 @@ export {
|
|
|
1955
2065
|
createCloudProvider,
|
|
1956
2066
|
renderInnerCommand
|
|
1957
2067
|
};
|
|
1958
|
-
//# sourceMappingURL=chunk-
|
|
2068
|
+
//# sourceMappingURL=chunk-E7CHS7ZR.js.map
|