@madarco/agentbox 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/_cloud-attach-DMVH6GWO.js +12 -0
  2. package/dist/chunk-7KOEFGN2.js +1162 -0
  3. package/dist/chunk-7KOEFGN2.js.map +1 -0
  4. package/dist/chunk-I24B6AXR.js +600 -0
  5. package/dist/chunk-I24B6AXR.js.map +1 -0
  6. package/dist/chunk-NAVL4R34.js +7546 -0
  7. package/dist/chunk-NAVL4R34.js.map +1 -0
  8. package/dist/chunk-NW5NYTQM.js +1366 -0
  9. package/dist/chunk-NW5NYTQM.js.map +1 -0
  10. package/dist/chunk-UK72UQ5U.js +237 -0
  11. package/dist/chunk-UK72UQ5U.js.map +1 -0
  12. package/dist/chunk-V5KZGB5V.js +722 -0
  13. package/dist/chunk-V5KZGB5V.js.map +1 -0
  14. package/dist/cloud-poller-ZIWSADJB-JXFRJUEM.js +10 -0
  15. package/dist/dist-ETCFRVPA.js +423 -0
  16. package/dist/dist-QZGJIBT5.js +1339 -0
  17. package/dist/dist-QZGJIBT5.js.map +1 -0
  18. package/dist/dist-R67WMLCF.js +183 -0
  19. package/dist/dist-R67WMLCF.js.map +1 -0
  20. package/dist/index.js +3998 -1569
  21. package/dist/index.js.map +1 -1
  22. package/package.json +8 -3
  23. package/runtime/docker/Dockerfile.box +98 -14
  24. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +15 -8
  25. package/runtime/docker/packages/ctl/dist/bin.cjs +10220 -773
  26. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +37 -0
  27. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +9 -9
  28. package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
  29. package/runtime/hetzner/agentbox-codex-hooks.json +37 -0
  30. package/runtime/hetzner/agentbox-dockerd-start +132 -0
  31. package/runtime/hetzner/agentbox-open +28 -0
  32. package/runtime/hetzner/agentbox-setup-skill.md +196 -0
  33. package/runtime/hetzner/agentbox-vnc-start +77 -0
  34. package/runtime/hetzner/claude-managed-settings.json +54 -0
  35. package/runtime/hetzner/ctl.cjs +22350 -0
  36. package/runtime/hetzner/custom-system-CLAUDE.md +27 -0
  37. package/runtime/hetzner/scripts/install-box.sh +365 -0
  38. package/runtime/relay/bin.cjs +9118 -809
  39. package/share/agentbox-setup/SKILL.md +15 -8
  40. package/dist/chunk-BBZMA2K6.js +0 -238
  41. package/dist/chunk-BBZMA2K6.js.map +0 -1
  42. package/dist/chunk-HHMWQNLF.js +0 -1709
  43. package/dist/chunk-HHMWQNLF.js.map +0 -1
  44. package/dist/chunk-HPZMD5DE.js +0 -106
  45. package/dist/chunk-HPZMD5DE.js.map +0 -1
  46. package/dist/chunk-HTTKML3C.js +0 -2655
  47. package/dist/chunk-HTTKML3C.js.map +0 -1
  48. package/dist/chunk-KJNZP6I3.js +0 -586
  49. package/dist/chunk-KJNZP6I3.js.map +0 -1
  50. package/dist/chunk-M7I247BK.js +0 -525
  51. package/dist/chunk-M7I247BK.js.map +0 -1
  52. package/dist/create-6PWXI6HO-OWAMHBAK.js +0 -15
  53. package/dist/lifecycle-EMXR46DI-DUVBXNTV.js +0 -38
  54. package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
  55. package/dist/stats-SZXOJE3D-N7OODCHW.js +0 -19
  56. package/dist/stats-SZXOJE3D-N7OODCHW.js.map +0 -1
  57. /package/dist/{create-6PWXI6HO-OWAMHBAK.js.map → _cloud-attach-DMVH6GWO.js.map} +0 -0
  58. /package/dist/{lifecycle-EMXR46DI-DUVBXNTV.js.map → cloud-poller-ZIWSADJB-JXFRJUEM.js.map} +0 -0
  59. /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-ETCFRVPA.js.map} +0 -0
@@ -0,0 +1,1366 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CLAUDE_FORWARDED_ENV_KEYS,
4
+ CODEX_FORWARDED_ENV_KEYS,
5
+ OPENCODE_FORWARDED_ENV_KEYS,
6
+ allocateProjectIndex,
7
+ buildHostEnvFindArgs,
8
+ detectGitRepos,
9
+ ensureRelay,
10
+ forgetBoxFromRelay,
11
+ generateRelayToken,
12
+ generateVncPassword,
13
+ hashProjectPath,
14
+ portlessAlias,
15
+ portlessGetUrl,
16
+ portlessUnalias,
17
+ projectDirSegment,
18
+ readState,
19
+ recordBox,
20
+ registerBoxWithRelay,
21
+ removeBoxRecord,
22
+ sanitizeMnemonic,
23
+ stageClaudeCredentialsForUpload,
24
+ stageClaudeStaticForUpload,
25
+ stageCodexCredentialsForUpload,
26
+ stageCodexStaticForUpload,
27
+ stageOpencodeCredentialsForUpload,
28
+ stageOpencodeStaticForUpload
29
+ } from "./chunk-NAVL4R34.js";
30
+
31
+ // ../../packages/sandbox-cloud/dist/index.js
32
+ import { randomBytes } from "crypto";
33
+ import { basename as basename2 } from "path";
34
+ import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
35
+ import { homedir } from "os";
36
+ import { basename, join } from "path";
37
+ import { mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
38
+ import { tmpdir } from "os";
39
+ import { join as join2 } from "path";
40
+ import { execa } from "execa";
41
+ import { readFile as readFile2 } from "fs/promises";
42
+ import { join as join3 } from "path";
43
+ import { parse as parseYaml } from "yaml";
44
+ import { execa as execa2 } from "execa";
45
+ import { existsSync, mkdirSync, renameSync, statSync } from "fs";
46
+ import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
47
+ import { tmpdir as tmpdir2 } from "os";
48
+ import {
49
+ basename as hostBasename,
50
+ dirname as hostDirname,
51
+ join as hostJoin,
52
+ resolve as hostResolve
53
+ } from "path";
54
+ import { posix } from "path";
55
+ import { execa as execa3 } from "execa";
56
+ import { mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
57
+ import { tmpdir as tmpdir3 } from "os";
58
+ import { join as join4 } from "path";
59
+ var CREDENTIALS_VOLUME = "agentbox-credentials";
60
+ var AGENT_SPECS = [
61
+ {
62
+ kind: "claude",
63
+ staticMountPath: "/home/vscode/.claude",
64
+ credentialsMountPath: "/home/vscode/.agentbox-creds/claude",
65
+ credentialsSubpath: "claude/",
66
+ stageStatic: (opts) => stageClaudeStaticForUpload({ hostWorkspace: opts.hostWorkspace }),
67
+ stageCredentials: () => stageClaudeCredentialsForUpload()
68
+ },
69
+ {
70
+ kind: "codex",
71
+ staticMountPath: "/home/vscode/.codex",
72
+ credentialsMountPath: "/home/vscode/.agentbox-creds/codex",
73
+ credentialsSubpath: "codex/",
74
+ stageStatic: () => stageCodexStaticForUpload(),
75
+ stageCredentials: () => stageCodexCredentialsForUpload()
76
+ },
77
+ {
78
+ kind: "opencode",
79
+ staticMountPath: "/home/vscode/.local/share/opencode",
80
+ credentialsMountPath: "/home/vscode/.agentbox-creds/opencode",
81
+ credentialsSubpath: "opencode/",
82
+ stageStatic: () => stageOpencodeStaticForUpload(),
83
+ stageCredentials: () => stageOpencodeCredentialsForUpload()
84
+ }
85
+ ];
86
+ var SEED_MARKER = ".agentbox-seeded-at";
87
+ async function ensureAgentVolumesForCloud(backend, opts = {}) {
88
+ const log = opts.onLog ?? (() => {
89
+ });
90
+ if (typeof backend.ensureVolume !== "function") {
91
+ log(
92
+ `cloud backend '${backend.name}' has no volume primitive \u2014 agent credentials will not persist across boxes`
93
+ );
94
+ return { mounts: [], env: buildForwardedEnv([]), agents: [] };
95
+ }
96
+ let volumeId;
97
+ try {
98
+ const ensured = await backend.ensureVolume(CREDENTIALS_VOLUME);
99
+ volumeId = ensured.volumeId;
100
+ } catch (err) {
101
+ const msg = err instanceof Error ? err.message : String(err);
102
+ log(`ensureVolume(${CREDENTIALS_VOLUME}) failed (skipping credentials seed): ${msg}`);
103
+ return { mounts: [], env: buildForwardedEnv([]), agents: [] };
104
+ }
105
+ const mounts = AGENT_SPECS.map((spec) => ({
106
+ volumeId,
107
+ mountPath: spec.credentialsMountPath,
108
+ subpath: spec.credentialsSubpath
109
+ }));
110
+ const agents = AGENT_SPECS.map((s) => s.kind);
111
+ return { mounts, env: buildForwardedEnv(agents), agents };
112
+ }
113
+ function buildForwardedEnv(agents) {
114
+ const env = {};
115
+ if (agents.includes("opencode")) {
116
+ env["OPENCODE_CONFIG_DIR"] = "/home/vscode/.local/share/opencode/config";
117
+ }
118
+ const forwardedKeys = /* @__PURE__ */ new Set([
119
+ ...CLAUDE_FORWARDED_ENV_KEYS,
120
+ ...CODEX_FORWARDED_ENV_KEYS,
121
+ ...OPENCODE_FORWARDED_ENV_KEYS
122
+ ]);
123
+ for (const k of forwardedKeys) {
124
+ const v = process.env[k];
125
+ if (typeof v === "string" && v.length > 0) env[k] = v;
126
+ }
127
+ return env;
128
+ }
129
+ async function seedAgentVolumesIfFresh(backend, handle, opts = {}) {
130
+ const wanted = new Set(opts.agents ?? AGENT_SPECS.map((s) => s.kind));
131
+ const specs = AGENT_SPECS.filter((s) => wanted.has(s.kind));
132
+ await Promise.all(specs.map((spec) => seedCredentialsOne(backend, handle, spec, opts)));
133
+ }
134
+ async function seedCredentialsOne(backend, handle, spec, opts) {
135
+ const log = opts.onLog ?? (() => {
136
+ });
137
+ if (!opts.force) {
138
+ const probe = await backend.exec(
139
+ handle,
140
+ `test -f ${spec.credentialsMountPath}/${SEED_MARKER}`
141
+ );
142
+ if (probe.exitCode === 0) {
143
+ log(`${spec.kind}: credentials already seeded \u2014 mounting only`);
144
+ return;
145
+ }
146
+ }
147
+ log(`${spec.kind}: staging host credentials`);
148
+ const staged = await spec.stageCredentials();
149
+ for (const w of staged.warnings) log(w);
150
+ try {
151
+ if (staged.tarballPath === null) {
152
+ log(`${spec.kind}: no credentials to seed`);
153
+ return;
154
+ }
155
+ let tarSize = 0;
156
+ try {
157
+ const { statSync: statSync2 } = await import("fs");
158
+ tarSize = statSync2(staged.tarballPath).size;
159
+ } catch {
160
+ }
161
+ const sizeKB = (tarSize / 1024).toFixed(1);
162
+ log(`${spec.kind}: uploading ${sizeKB} KB credentials tarball`);
163
+ process.stderr.write(`[agent-creds] ${spec.kind}: uploading ${sizeKB} KB...
164
+ `);
165
+ const t0 = Date.now();
166
+ const remoteTar = `/tmp/agentbox-${spec.kind}-creds.tar.gz`;
167
+ await backend.uploadFile(handle, staged.tarballPath, remoteTar);
168
+ const upDt = ((Date.now() - t0) / 1e3).toFixed(1);
169
+ process.stderr.write(`[agent-creds] ${spec.kind}: upload done in ${upDt}s
170
+ `);
171
+ const stageDir = `/tmp/agentbox-creds-stage-${spec.kind}`;
172
+ const extractCmd = `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}`;
173
+ const extract = await backend.exec(handle, extractCmd);
174
+ if (extract.exitCode !== 0) {
175
+ 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)}`;
176
+ log(msg);
177
+ process.stderr.write(`[agent-creds] ${msg}
178
+ `);
179
+ return;
180
+ }
181
+ log(`${spec.kind}: credentials seeded \u2713`);
182
+ process.stderr.write(`[agent-creds] ${spec.kind}: credentials seeded
183
+ `);
184
+ } finally {
185
+ await staged.cleanup();
186
+ }
187
+ }
188
+ function agentSpecsForCloud() {
189
+ return AGENT_SPECS.map((s) => ({
190
+ kind: s.kind,
191
+ staticMountPath: s.staticMountPath,
192
+ credentialsMountPath: s.credentialsMountPath,
193
+ credentialsSubpath: s.credentialsSubpath
194
+ }));
195
+ }
196
+ var CLOUD_CHECKPOINTS_ROOT = join(homedir(), ".agentbox", "cloud-checkpoints");
197
+ var CLOUD_SNAPSHOT_NAME_PREFIX = "agentbox-ckpt-";
198
+ function cloudSnapshotName(projectRoot, name) {
199
+ const mnemonic = sanitizeMnemonic(basename(projectRoot));
200
+ return `${CLOUD_SNAPSHOT_NAME_PREFIX}${hashProjectPath(projectRoot)}_${mnemonic}-${name}`;
201
+ }
202
+ function backendDir(backend, projectRoot) {
203
+ return join(CLOUD_CHECKPOINTS_ROOT, backend, projectDirSegment(projectRoot));
204
+ }
205
+ function checkpointDir(backend, projectRoot, name) {
206
+ return join(backendDir(backend, projectRoot), name);
207
+ }
208
+ async function readManifest(dir) {
209
+ try {
210
+ const raw = await readFile(join(dir, "manifest.json"), "utf8");
211
+ const m = JSON.parse(raw);
212
+ if (m.schema !== 1) return null;
213
+ return m;
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+ async function listCloudCheckpoints(projectRoot, backend) {
219
+ const root = backendDir(backend, projectRoot);
220
+ let entries;
221
+ try {
222
+ entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
223
+ } catch {
224
+ return [];
225
+ }
226
+ const out = [];
227
+ for (const name of entries) {
228
+ const dir = join(root, name);
229
+ const manifest = await readManifest(dir);
230
+ if (manifest) out.push({ name, dir, manifest });
231
+ }
232
+ out.sort((a, b) => a.manifest.createdAt.localeCompare(b.manifest.createdAt));
233
+ return out;
234
+ }
235
+ async function resolveCloudCheckpoint(projectRoot, backend, ref) {
236
+ const dir = checkpointDir(backend, projectRoot, ref);
237
+ const manifest = await readManifest(dir);
238
+ if (!manifest) return null;
239
+ return { name: ref, dir, manifest };
240
+ }
241
+ async function writeCloudCheckpointManifest(projectRoot, backend, name, fields) {
242
+ const dir = checkpointDir(backend, projectRoot, name);
243
+ await mkdir(dir, { recursive: true });
244
+ const manifest = {
245
+ schema: 1,
246
+ name,
247
+ backend,
248
+ snapshotName: fields.snapshotName,
249
+ sourceBoxId: fields.sourceBoxId,
250
+ sourceBoxName: fields.sourceBoxName,
251
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
252
+ };
253
+ await writeFile(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
254
+ return { name, dir, manifest };
255
+ }
256
+ async function removeCloudCheckpointDir(projectRoot, backend, name) {
257
+ const dir = checkpointDir(backend, projectRoot, name);
258
+ const existed = await readManifest(dir) !== null;
259
+ if (!existed) return false;
260
+ await rm(dir, { recursive: true, force: true });
261
+ return true;
262
+ }
263
+ var WORKSPACE_DIR_DEFAULT = "/workspace";
264
+ var REMOTE_TAR_PATH = "/tmp/agentbox-envfiles.tar";
265
+ async function uploadEnvFiles(args) {
266
+ const log = args.onLog ?? (() => {
267
+ });
268
+ if (args.files.length === 0) return { copied: 0 };
269
+ const workspaceDir = args.workspaceDir ?? WORKSPACE_DIR_DEFAULT;
270
+ const found = await execa("find", buildHostEnvFindArgs(args.files).slice(1), {
271
+ cwd: args.workspacePath,
272
+ reject: false
273
+ });
274
+ if (found.exitCode !== 0) {
275
+ log(`env-file scan failed: ${String(found.stderr).slice(0, 300)}`);
276
+ return { copied: 0 };
277
+ }
278
+ const list = String(found.stdout).split("\0").filter((p) => p.length > 0);
279
+ if (list.length === 0) return { copied: 0 };
280
+ const stage = await mkdtemp(join2(tmpdir(), "agentbox-envfiles-"));
281
+ const localTar = join2(stage, "envfiles.tar");
282
+ try {
283
+ const packed = await execa(
284
+ "tar",
285
+ ["-C", args.workspacePath, "--null", "-T", "-", "-cf", localTar],
286
+ { input: list.join("\0"), reject: false }
287
+ );
288
+ if (packed.exitCode !== 0) {
289
+ log(`env-file tar pack failed: ${String(packed.stderr).slice(0, 300)}`);
290
+ return { copied: 0 };
291
+ }
292
+ await writeFile2(join2(stage, ".marker"), "").catch(() => {
293
+ });
294
+ await args.backend.uploadFile(args.handle, localTar, REMOTE_TAR_PATH);
295
+ const extract = await args.backend.exec(
296
+ args.handle,
297
+ `tar -xf ${REMOTE_TAR_PATH} -C ${workspaceDir} --no-same-permissions --no-same-owner -m && rm -f ${REMOTE_TAR_PATH}`
298
+ );
299
+ if (extract.exitCode !== 0) {
300
+ log(
301
+ `env-file extract failed (exit ${String(extract.exitCode)}); stdout: ${extract.stdout.slice(-200)} stderr: ${extract.stderr.slice(-200)}`
302
+ );
303
+ return { copied: 0 };
304
+ }
305
+ } finally {
306
+ await rm2(stage, { recursive: true, force: true });
307
+ }
308
+ return { copied: list.length };
309
+ }
310
+ async function readExposedServicePorts(workspacePath) {
311
+ let text;
312
+ try {
313
+ text = await readFile2(join3(workspacePath, "agentbox.yaml"), "utf8");
314
+ } catch {
315
+ return [];
316
+ }
317
+ let doc;
318
+ try {
319
+ doc = parseYaml(text);
320
+ } catch {
321
+ return [];
322
+ }
323
+ if (!isPlainObject(doc)) return [];
324
+ const services = doc["services"];
325
+ if (!isPlainObject(services)) return [];
326
+ const out = /* @__PURE__ */ new Set();
327
+ for (const value of Object.values(services)) {
328
+ if (!isPlainObject(value)) continue;
329
+ const expose = value["expose"];
330
+ if (!isPlainObject(expose)) continue;
331
+ const port = expose["port"];
332
+ if (typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536) {
333
+ out.add(port);
334
+ }
335
+ }
336
+ return [...out].sort((a, b) => a - b);
337
+ }
338
+ function isPlainObject(v) {
339
+ return typeof v === "object" && v !== null && !Array.isArray(v);
340
+ }
341
+ function quoteShellArgv(argv) {
342
+ return argv.map(quoteShellArg).join(" ");
343
+ }
344
+ function quoteShellArg(arg) {
345
+ if (arg.length === 0) return "''";
346
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(arg)) return arg;
347
+ return "'" + arg.replace(/'/g, "'\\''") + "'";
348
+ }
349
+ function bashScript(body) {
350
+ return `bash -c ${quoteShellArg(body)}`;
351
+ }
352
+ var REMOTE_UP_TAR = "/tmp/agentbox-cp-up.tar.gz";
353
+ var REMOTE_DOWN_TAR = "/tmp/agentbox-cp-down.tar.gz";
354
+ async function uploadToCloudBox(backend, handle, hostSrc, boxDst) {
355
+ const srcAbs = hostResolve(hostSrc);
356
+ if (!existsSync(srcAbs)) throw new Error(`source not found: ${hostSrc}`);
357
+ const srcBasename = hostBasename(srcAbs);
358
+ const srcParent = hostDirname(srcAbs);
359
+ let boxParent;
360
+ let finalName;
361
+ if (boxDst.endsWith("/")) {
362
+ boxParent = boxDst.replace(/\/+$/, "") || "/";
363
+ finalName = srcBasename;
364
+ } else {
365
+ boxParent = posix.dirname(boxDst);
366
+ finalName = posix.basename(boxDst);
367
+ }
368
+ const finalPath = boxParent === "/" ? `/${finalName}` : `${boxParent}/${finalName}`;
369
+ const stage = await mkdtemp2(hostJoin(tmpdir2(), "agentbox-cp-up-"));
370
+ const localTar = hostJoin(stage, "payload.tar.gz");
371
+ try {
372
+ await execa2("tar", ["-C", srcParent, "-czf", localTar, srcBasename], {
373
+ env: { ...process.env, COPYFILE_DISABLE: "1" }
374
+ });
375
+ await backend.uploadFile(handle, localTar, REMOTE_UP_TAR);
376
+ const initialPath = boxParent === "/" ? `/${srcBasename}` : `${boxParent}/${srcBasename}`;
377
+ const renameStep = finalName !== srcBasename ? `$SUDO cp -f ${quoteShellArg(initialPath)} ${quoteShellArg(finalPath)} && $SUDO rm -f ${quoteShellArg(initialPath)}` : ": # no rename";
378
+ const script = [
379
+ `set -euo pipefail`,
380
+ `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`,
381
+ `$SUDO mkdir -p ${quoteShellArg(boxParent)}`,
382
+ // --no-same-permissions / --no-same-owner / -m: Daytona's S3-backed
383
+ // FUSE volumes reject chmod/utime/chown; skipping them lets the extract
384
+ // complete on a mounted-volume destination. Harmless no-op on the
385
+ // sandbox's regular disk. Same flags as the credential-seed extract.
386
+ `$SUDO tar -xzf ${quoteShellArg(REMOTE_UP_TAR)} -C ${quoteShellArg(boxParent)} --no-same-permissions --no-same-owner -m`,
387
+ renameStep,
388
+ // chown only the landed path — anything we mkdir'd through stays at
389
+ // its existing ownership. Tolerate failure (chown bad on read-only mounts).
390
+ `$SUDO chown -R "$(id -un):$(id -gn)" ${quoteShellArg(finalPath)} || true`,
391
+ `rm -f ${quoteShellArg(REMOTE_UP_TAR)}`
392
+ ].join("\n");
393
+ const r = await backend.exec(handle, bashScript(script));
394
+ if (r.exitCode !== 0) {
395
+ throw new Error(`cloud upload extract failed: ${r.stderr || r.stdout}`);
396
+ }
397
+ } finally {
398
+ await rm3(stage, { recursive: true, force: true });
399
+ }
400
+ return { finalPath };
401
+ }
402
+ async function pullCloudDirContents(backend, handle, boxSrcDir, hostDstDir) {
403
+ const dstAbs = hostResolve(hostDstDir);
404
+ mkdirSync(dstAbs, { recursive: true });
405
+ const stage = await mkdtemp2(hostJoin(tmpdir2(), "agentbox-pull-"));
406
+ const localTar = hostJoin(stage, "payload.tar.gz");
407
+ try {
408
+ const packScript = [
409
+ `set -euo pipefail`,
410
+ `cd ${quoteShellArg(boxSrcDir)}`,
411
+ `tar -czf ${quoteShellArg(REMOTE_DOWN_TAR)} .`
412
+ ].join("\n");
413
+ const r = await backend.exec(handle, bashScript(packScript));
414
+ if (r.exitCode !== 0) {
415
+ throw new Error(`cloud workspace pack failed: ${r.stderr || r.stdout}`);
416
+ }
417
+ await backend.downloadFile(handle, REMOTE_DOWN_TAR, localTar);
418
+ await execa2("tar", ["-xzf", localTar, "-C", dstAbs]);
419
+ await backend.exec(handle, `rm -f ${quoteShellArg(REMOTE_DOWN_TAR)}`).catch(() => {
420
+ });
421
+ } finally {
422
+ await rm3(stage, { recursive: true, force: true });
423
+ }
424
+ return { finalPath: dstAbs };
425
+ }
426
+ async function downloadFromCloudBox(backend, handle, boxSrc, hostDst) {
427
+ const srcBasename = posix.basename(boxSrc);
428
+ const srcParent = posix.dirname(boxSrc);
429
+ const dstAbs = hostResolve(hostDst);
430
+ let hostParent;
431
+ let finalName;
432
+ const dstExists = existsSync(dstAbs);
433
+ if (hostDst.endsWith("/") || dstExists && statSync(dstAbs).isDirectory()) {
434
+ hostParent = dstAbs;
435
+ finalName = srcBasename;
436
+ } else {
437
+ hostParent = hostDirname(dstAbs);
438
+ finalName = hostBasename(dstAbs);
439
+ }
440
+ mkdirSync(hostParent, { recursive: true });
441
+ const finalPath = hostJoin(hostParent, finalName);
442
+ const stage = await mkdtemp2(hostJoin(tmpdir2(), "agentbox-cp-down-"));
443
+ const localTar = hostJoin(stage, "payload.tar.gz");
444
+ try {
445
+ const packScript = [
446
+ `set -euo pipefail`,
447
+ `cd ${quoteShellArg(srcParent)}`,
448
+ `tar -czf ${quoteShellArg(REMOTE_DOWN_TAR)} ${quoteShellArg(srcBasename)}`
449
+ ].join("\n");
450
+ const r = await backend.exec(handle, bashScript(packScript));
451
+ if (r.exitCode !== 0) {
452
+ throw new Error(`cloud download pack failed: ${r.stderr || r.stdout}`);
453
+ }
454
+ await backend.downloadFile(handle, REMOTE_DOWN_TAR, localTar);
455
+ await execa2("tar", ["-xzf", localTar, "-C", hostParent]);
456
+ if (finalName !== srcBasename) {
457
+ renameSync(hostJoin(hostParent, srcBasename), finalPath);
458
+ }
459
+ await backend.exec(handle, `rm -f ${quoteShellArg(REMOTE_DOWN_TAR)}`).catch(() => {
460
+ });
461
+ } finally {
462
+ await rm3(stage, { recursive: true, force: true });
463
+ }
464
+ return { finalPath };
465
+ }
466
+ async function launchCloudCtlDaemon(args) {
467
+ const env = [
468
+ `AGENTBOX_BOX_ID=${quoteShellArgv([args.boxId])}`,
469
+ `AGENTBOX_BOX_NAME=${quoteShellArgv([args.boxName])}`,
470
+ `AGENTBOX_BOX_KIND=cloud`
471
+ ];
472
+ if (args.relayUrl) env.push(`AGENTBOX_RELAY_URL=${quoteShellArgv([args.relayUrl])}`);
473
+ if (args.relayToken) env.push(`AGENTBOX_RELAY_TOKEN=${quoteShellArgv([args.relayToken])}`);
474
+ if (args.bridgeToken) env.push(`AGENTBOX_BRIDGE_TOKEN=${quoteShellArgv([args.bridgeToken])}`);
475
+ const script = [
476
+ `set -e`,
477
+ `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`,
478
+ `$SUDO mkdir -p /run/agentbox /var/log/agentbox`,
479
+ `$SUDO chown "$(id -un):$(id -gn)" /run/agentbox /var/log/agentbox`,
480
+ `export ${env.join(" ")}`,
481
+ `nohup /usr/local/bin/agentbox-ctl daemon >> /var/log/agentbox/ctl-daemon.log 2>&1 &`,
482
+ `disown`,
483
+ `echo started`
484
+ ].join("\n");
485
+ const r = await args.backend.exec(args.handle, bashScript(script));
486
+ if (r.exitCode !== 0) {
487
+ throw new Error(`agentbox-ctl daemon launch failed: ${r.stderr || r.stdout}`);
488
+ }
489
+ }
490
+ async function launchCloudDockerdDaemon(args) {
491
+ const timeoutMs = args.timeoutMs ?? 6e4;
492
+ const startScript = [
493
+ `set -euo pipefail`,
494
+ `mkdir -p /var/log/agentbox`,
495
+ `nohup sudo -n /usr/local/bin/agentbox-dockerd-start >> /var/log/agentbox/dockerd.log 2>&1 &`,
496
+ `echo "spawned dockerd"`
497
+ ].join("\n");
498
+ const launch = await args.backend.exec(args.handle, bashScript(startScript));
499
+ if (launch.exitCode !== 0) {
500
+ return {
501
+ up: false,
502
+ reason: `dockerd launch failed: ${launch.stderr || launch.stdout}`
503
+ };
504
+ }
505
+ const probeCmd = "[ -S /var/run/docker.sock ] && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1";
506
+ const deadline = Date.now() + timeoutMs;
507
+ while (Date.now() < deadline) {
508
+ const probe = await args.backend.exec(args.handle, probeCmd);
509
+ if (probe.exitCode === 0) return { up: true };
510
+ await new Promise((r) => setTimeout(r, 500));
511
+ }
512
+ return {
513
+ up: false,
514
+ reason: `dockerd did not become ready within ${String(timeoutMs)}ms`
515
+ };
516
+ }
517
+ async function launchCloudVncDaemon(args) {
518
+ const script = [
519
+ `set -e`,
520
+ `cd /home/vscode`,
521
+ `export AGENTBOX_VNC_PASSWORD=${quoteShellArg(args.vncPassword)}`,
522
+ `mkdir -p /var/log/agentbox 2>/dev/null || true`,
523
+ `nohup /usr/local/bin/agentbox-vnc-start >> /var/log/agentbox/vnc-start.log 2>&1 &`,
524
+ `disown`,
525
+ // Match the host-side probe in packages/sandbox-docker/src/vnc.ts: poll for
526
+ // ~5s, then give up. The script itself waits for Xvnc internally; this
527
+ // outer probe confirms websockify's public port came up too.
528
+ `for _ in $(seq 1 50); do`,
529
+ ` if (echo > /dev/tcp/127.0.0.1/6080) 2>/dev/null; then echo ready; exit 0; fi`,
530
+ ` sleep 0.1`,
531
+ `done`,
532
+ `echo "websockify did not bind 6080 within 5s" >&2`,
533
+ `exit 1`
534
+ ].join("\n");
535
+ const r = await args.backend.exec(args.handle, bashScript(script));
536
+ if (r.exitCode !== 0) {
537
+ throw new Error(`agentbox-vnc-start failed: ${r.stderr || r.stdout}`);
538
+ }
539
+ }
540
+ var WORKSPACE_DIR_DEFAULT2 = "/workspace";
541
+ async function seedCloudWorkspace(args) {
542
+ const workspaceDir = args.workspaceDir ?? WORKSPACE_DIR_DEFAULT2;
543
+ const log = args.onLog ?? (() => {
544
+ });
545
+ const repos = await detectGitRepos(args.workspacePath);
546
+ const root = repos.find((r) => r.kind === "root");
547
+ const nested = repos.filter((r) => r.kind === "nested");
548
+ if (root) {
549
+ log(
550
+ nested.length > 0 ? `seeding /workspace from git bundle (+${String(nested.length)} nested repo${nested.length === 1 ? "" : "s"})` : "seeding /workspace from git bundle"
551
+ );
552
+ await seedFromGitBundle({
553
+ backend: args.backend,
554
+ handle: args.handle,
555
+ hostRepo: root.hostMainRepo,
556
+ branch: args.branch,
557
+ workspaceDir
558
+ });
559
+ for (const r of nested) {
560
+ const sub = `${workspaceDir}/${r.relPathFromWorkspace}`;
561
+ log(`seeding nested repo ${r.relPathFromWorkspace} from git bundle`);
562
+ await seedFromGitBundle({
563
+ backend: args.backend,
564
+ handle: args.handle,
565
+ hostRepo: r.hostMainRepo,
566
+ branch: args.branch,
567
+ workspaceDir: sub
568
+ });
569
+ }
570
+ return { fromGit: true, branch: args.branch };
571
+ }
572
+ log("seeding /workspace from workspace tarball (no git detected)");
573
+ await seedFromTar({
574
+ backend: args.backend,
575
+ handle: args.handle,
576
+ hostDir: args.workspacePath,
577
+ workspaceDir
578
+ });
579
+ return { fromGit: false, branch: args.branch };
580
+ }
581
+ var STASH_CARRYOVER_REF = "refs/agentbox-carryover/stash";
582
+ var REMOTE_UNTRACKED_TAR = "/tmp/agentbox-carryover-untracked.tar.gz";
583
+ async function seedFromGitBundle(args) {
584
+ const stage = await mkdtemp3(join4(tmpdir3(), "agentbox-bundle-"));
585
+ const bundlePath = join4(stage, "workspace.bundle");
586
+ const untrackedTarPath = join4(stage, "untracked.tar.gz");
587
+ const stashSha = await safeStashCreate(args.hostRepo);
588
+ const untrackedSize = await maybeBuildUntrackedTar(args.hostRepo, untrackedTarPath);
589
+ let stashRefCreated = false;
590
+ try {
591
+ if (stashSha) {
592
+ const ref = await execa3(
593
+ "git",
594
+ ["-C", args.hostRepo, "update-ref", STASH_CARRYOVER_REF, stashSha],
595
+ { reject: false }
596
+ );
597
+ stashRefCreated = ref.exitCode === 0;
598
+ }
599
+ const depthRaw = process.env["AGENTBOX_BUNDLE_DEPTH"];
600
+ const depth = depthRaw ? Number.parseInt(depthRaw, 10) : NaN;
601
+ const bundleArgs = ["-C", args.hostRepo, "bundle", "create", bundlePath];
602
+ if (Number.isFinite(depth) && depth > 0) {
603
+ bundleArgs.push(`--depth=${String(depth)}`, "HEAD");
604
+ } else {
605
+ bundleArgs.push("--all");
606
+ }
607
+ if (stashRefCreated) bundleArgs.push(STASH_CARRYOVER_REF);
608
+ await execa3("git", bundleArgs);
609
+ if (stashRefCreated) {
610
+ await execa3("git", ["-C", args.hostRepo, "update-ref", "-d", STASH_CARRYOVER_REF], {
611
+ reject: false
612
+ });
613
+ stashRefCreated = false;
614
+ }
615
+ const remoteUrl = await readOriginUrl(args.hostRepo);
616
+ const remoteBundle = "/tmp/agentbox-workspace.bundle";
617
+ await args.backend.uploadFile(args.handle, bundlePath, remoteBundle);
618
+ if (untrackedSize > 0) {
619
+ await args.backend.uploadFile(args.handle, untrackedTarPath, REMOTE_UNTRACKED_TAR);
620
+ }
621
+ const setOrigin = remoteUrl ? `git -C ${quoteShellArgv([args.workspaceDir])} remote set-url origin ${quoteShellArgv([remoteUrl])}` : ": # no host origin to copy";
622
+ const SUDO = `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`;
623
+ const carryOverSteps = stashSha ? [
624
+ `if git -C ${quoteShellArgv([args.workspaceDir])} rev-parse --verify ${quoteShellArgv([`refs/remotes/origin/agentbox-carryover/stash`])} >/dev/null 2>&1; then git -C ${quoteShellArgv([args.workspaceDir])} stash apply ${quoteShellArgv([`refs/remotes/origin/agentbox-carryover/stash`])} || echo "agentbox: stash apply soft-failed; carry-over may be incomplete" >&2 ; git -C ${quoteShellArgv([args.workspaceDir])} update-ref -d ${quoteShellArgv([`refs/remotes/origin/agentbox-carryover/stash`])} || true ; fi`
625
+ ] : [];
626
+ if (untrackedSize > 0) {
627
+ carryOverSteps.push(
628
+ `if [ -f ${quoteShellArgv([REMOTE_UNTRACKED_TAR])} ]; then tar -C ${quoteShellArgv([args.workspaceDir])} -xzf ${quoteShellArgv([REMOTE_UNTRACKED_TAR])} && rm -f ${quoteShellArgv([REMOTE_UNTRACKED_TAR])} ; fi`
629
+ );
630
+ }
631
+ const script = [
632
+ `set -euo pipefail`,
633
+ // Move out of any cwd we might inherit from Daytona's executeCommand
634
+ // before we delete /workspace. The agentbox image bakes WORKDIR
635
+ // /workspace; if the shell's cwd is /workspace when we `rm -rf` it,
636
+ // the next process inherits a stale cwd FD and git-clone's child
637
+ // (index-pack) fails with "Unable to read current working directory".
638
+ `cd /tmp`,
639
+ SUDO,
640
+ // rm -rf only the directory we're about to clone into — for nested
641
+ // repos this is just `/workspace/<rel>`, so the root clone (already
642
+ // at `/workspace`) is preserved.
643
+ `$SUDO rm -rf ${quoteShellArgv([args.workspaceDir])}`,
644
+ `$SUDO mkdir -p ${quoteShellArgv([args.workspaceDir])}`,
645
+ `$SUDO chown "$(id -un):$(id -gn)" ${quoteShellArgv([args.workspaceDir])}`,
646
+ `git clone ${quoteShellArgv([remoteBundle, args.workspaceDir])}`,
647
+ setOrigin,
648
+ `git -C ${quoteShellArgv([args.workspaceDir])} fetch ${quoteShellArgv([remoteBundle])} --tags '+refs/heads/*:refs/remotes/bundle/*' || true`,
649
+ `git -C ${quoteShellArgv([args.workspaceDir])} checkout -B ${quoteShellArgv([args.branch])}`,
650
+ ...carryOverSteps,
651
+ `rm -f ${quoteShellArgv([remoteBundle])}`
652
+ ].join("\n");
653
+ const r = await args.backend.exec(args.handle, bashScript(script));
654
+ if (r.exitCode !== 0) {
655
+ throw new Error(`workspace seed (bundle) failed: ${r.stderr || r.stdout}`);
656
+ }
657
+ } finally {
658
+ if (stashRefCreated) {
659
+ await execa3("git", ["-C", args.hostRepo, "update-ref", "-d", STASH_CARRYOVER_REF], {
660
+ reject: false
661
+ });
662
+ }
663
+ await rm4(stage, { recursive: true, force: true });
664
+ }
665
+ }
666
+ async function safeStashCreate(hostRepo) {
667
+ const r = await execa3("git", ["-C", hostRepo, "stash", "create"], { reject: false });
668
+ if (r.exitCode !== 0) return null;
669
+ const sha = r.stdout.trim();
670
+ return sha.length > 0 ? sha : null;
671
+ }
672
+ async function maybeBuildUntrackedTar(hostRepo, outPath) {
673
+ const list = await execa3(
674
+ "git",
675
+ ["-C", hostRepo, "ls-files", "--others", "--exclude-standard", "-z"],
676
+ { reject: false }
677
+ );
678
+ if (list.exitCode !== 0 || list.stdout.length === 0) return 0;
679
+ const tar = await execa3(
680
+ "tar",
681
+ ["-C", hostRepo, "--null", "-T", "-", "-czf", outPath],
682
+ {
683
+ input: list.stdout,
684
+ env: { ...process.env, COPYFILE_DISABLE: "1" },
685
+ reject: false
686
+ }
687
+ );
688
+ if (tar.exitCode !== 0) return 0;
689
+ try {
690
+ const { stat } = await import("fs/promises");
691
+ const s = await stat(outPath);
692
+ return s.size;
693
+ } catch {
694
+ return 0;
695
+ }
696
+ }
697
+ async function readOriginUrl(hostRepo) {
698
+ const r = await execa3("git", ["-C", hostRepo, "remote", "get-url", "origin"], { reject: false });
699
+ if (r.exitCode !== 0) return null;
700
+ const out = (r.stdout ?? "").trim();
701
+ return out.length > 0 ? out : null;
702
+ }
703
+ async function seedFromTar(args) {
704
+ const stage = await mkdtemp3(join4(tmpdir3(), "agentbox-tar-"));
705
+ const tarPath = join4(stage, "workspace.tar.gz");
706
+ try {
707
+ await execa3("tar", ["-C", args.hostDir, "-czf", tarPath, "."]);
708
+ const remoteTar = "/tmp/agentbox-workspace.tar.gz";
709
+ await args.backend.uploadFile(args.handle, tarPath, remoteTar);
710
+ const SUDO = `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`;
711
+ const script = [
712
+ `set -euo pipefail`,
713
+ // Move out of any cwd we might inherit from Daytona's executeCommand
714
+ // before we delete /workspace. The agentbox image bakes WORKDIR
715
+ // /workspace; if the shell's cwd is /workspace when we `rm -rf` it,
716
+ // the next process inherits a stale cwd FD and git-clone's child
717
+ // (index-pack) fails with "Unable to read current working directory".
718
+ `cd /tmp`,
719
+ SUDO,
720
+ `$SUDO rm -rf ${quoteShellArgv([args.workspaceDir])}`,
721
+ `$SUDO mkdir -p ${quoteShellArgv([args.workspaceDir])}`,
722
+ `$SUDO chown "$(id -un):$(id -gn)" ${quoteShellArgv([args.workspaceDir])}`,
723
+ `tar -C ${quoteShellArgv([args.workspaceDir])} -xzf ${quoteShellArgv([remoteTar])}`,
724
+ `rm -f ${quoteShellArgv([remoteTar])}`
725
+ ].join("\n");
726
+ const r = await args.backend.exec(args.handle, bashScript(script));
727
+ if (r.exitCode !== 0) {
728
+ throw new Error(`workspace seed (tar) failed: ${r.stderr || r.stdout}`);
729
+ }
730
+ } finally {
731
+ await rm4(stage, { recursive: true, force: true });
732
+ }
733
+ }
734
+ var CLOUD_WORKSPACE_DIR = "/workspace";
735
+ var CLOUD_WEB_PROXY_PORT = 80;
736
+ var CLOUD_VNC_PORT = 6080;
737
+ var DEFAULT_SIGNED_URL_TTL_SECONDS = 3600;
738
+ var FALLBACK_IMAGE = "agentbox/box:dev";
739
+ var DEFAULT_PORTLESS_PROXY_PORT = 1355;
740
+ function parsePortlessUrl(url) {
741
+ try {
742
+ const u = new URL(url);
743
+ if (!u.hostname.endsWith(".localhost")) return void 0;
744
+ const tls = u.protocol === "https:";
745
+ const proxyPort = u.port ? Number.parseInt(u.port, 10) : tls ? 443 : 80;
746
+ if (!Number.isFinite(proxyPort)) return void 0;
747
+ return { proxyPort, tls };
748
+ } catch {
749
+ return void 0;
750
+ }
751
+ }
752
+ function parseLoopbackPort(url) {
753
+ try {
754
+ const u = new URL(url);
755
+ if (u.hostname !== "127.0.0.1" && u.hostname !== "localhost") return void 0;
756
+ const port = Number.parseInt(u.port, 10);
757
+ return Number.isFinite(port) ? port : void 0;
758
+ } catch {
759
+ return void 0;
760
+ }
761
+ }
762
+ async function bootstrapPortlessForCloudBox(backend, handle, args) {
763
+ const localPort = parseLoopbackPort(args.webPreviewUrl);
764
+ if (localPort === void 0) return void 0;
765
+ const ok = await portlessAlias(args.boxName, localPort);
766
+ if (!ok) {
767
+ args.onLog(
768
+ `portless: alias not registered (portless CLI missing or not running) \u2014 host URL stays http://127.0.0.1:${String(localPort)}`
769
+ );
770
+ return void 0;
771
+ }
772
+ const url = await portlessGetUrl(args.boxName);
773
+ args.onLog(`portless alias ${url} -> 127.0.0.1:${String(localPort)}`);
774
+ if (backend.startInBoxPortless) {
775
+ const mode = parsePortlessUrl(url) ?? { proxyPort: DEFAULT_PORTLESS_PROXY_PORT, tls: false };
776
+ try {
777
+ await backend.startInBoxPortless(handle, {
778
+ boxName: args.boxName,
779
+ proxyPort: mode.proxyPort,
780
+ tls: mode.tls,
781
+ webPort: args.webPort
782
+ });
783
+ args.onLog(
784
+ `portless: in-box mirror up on 127.0.0.1:${String(mode.proxyPort)} (${mode.tls ? "https" : "http"})`
785
+ );
786
+ } catch (err) {
787
+ args.onLog(
788
+ `portless: in-box mirror failed (continuing): ${err instanceof Error ? err.message : String(err)}`
789
+ );
790
+ }
791
+ }
792
+ return { alias: args.boxName, url };
793
+ }
794
+ function createCloudProvider(backend, opts = {}) {
795
+ const providerName = backend.name;
796
+ function handleFor(box) {
797
+ const sandboxId = box.cloud?.sandboxId;
798
+ if (!sandboxId) {
799
+ throw new Error(`cloud box ${box.name} has no sandboxId \u2014 record is malformed`);
800
+ }
801
+ return { sandboxId };
802
+ }
803
+ function mintBox(req) {
804
+ const id = randomBytes(4).toString("hex");
805
+ const name = req.name ?? `${basename2(req.workspacePath)}-${id}`;
806
+ return {
807
+ id,
808
+ name,
809
+ branch: `agentbox/${name}`
810
+ };
811
+ }
812
+ async function probe(box) {
813
+ try {
814
+ const h = handleFor(box);
815
+ const state = await backend.state(h);
816
+ return state;
817
+ } catch {
818
+ return "missing";
819
+ }
820
+ }
821
+ return {
822
+ name: providerName,
823
+ async create(req) {
824
+ const log = req.onLog ?? (() => {
825
+ });
826
+ const { id, name, branch } = mintBox(req);
827
+ const image = opts.provisionImage ? await opts.provisionImage(req) : req.image ?? FALLBACK_IMAGE;
828
+ const resources = opts.defaultResources ?? { cpu: 2, memory: 4, disk: 8 };
829
+ const relayToken = generateRelayToken();
830
+ const bridgeToken = generateRelayToken();
831
+ try {
832
+ await ensureRelay({ onLog: log });
833
+ } catch (err) {
834
+ log(`relay ensure failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
835
+ }
836
+ let snapshotName;
837
+ let resolvedCheckpointRef;
838
+ if (req.checkpointRef && req.projectRoot) {
839
+ const found = await resolveCloudCheckpoint(req.projectRoot, backend.name, req.checkpointRef);
840
+ if (found) {
841
+ snapshotName = found.manifest.snapshotName;
842
+ resolvedCheckpointRef = found.name;
843
+ log(`provisioning from cloud checkpoint '${found.name}' (snapshot ${snapshotName})`);
844
+ } else {
845
+ log(
846
+ `cloud checkpoint '${req.checkpointRef}' not found for ${backend.name}; provisioning from base image`
847
+ );
848
+ }
849
+ }
850
+ const agentVolumes = await ensureAgentVolumesForCloud(backend, { onLog: log });
851
+ log(
852
+ snapshotName ? `provisioning ${providerName} sandbox from snapshot` : `provisioning ${providerName} sandbox`
853
+ );
854
+ const handle = await backend.provision({
855
+ name,
856
+ image,
857
+ snapshot: snapshotName,
858
+ resources,
859
+ env: {
860
+ AGENTBOX_BOX_ID: id,
861
+ AGENTBOX_BOX_NAME: name,
862
+ AGENTBOX_BOX_KIND: "cloud",
863
+ // In-sandbox relay is on the box's loopback at the default port.
864
+ AGENTBOX_RELAY_URL: `http://127.0.0.1:${String(8787)}`,
865
+ AGENTBOX_RELAY_TOKEN: relayToken,
866
+ AGENTBOX_BRIDGE_TOKEN: bridgeToken,
867
+ ...agentVolumes.env
868
+ },
869
+ volumes: agentVolumes.mounts,
870
+ onLog: log
871
+ });
872
+ try {
873
+ if (snapshotName) {
874
+ log("skipping workspace seed \u2014 snapshot already contains /workspace");
875
+ } else {
876
+ await seedCloudWorkspace({
877
+ backend,
878
+ handle,
879
+ workspacePath: req.workspacePath,
880
+ branch,
881
+ workspaceDir: CLOUD_WORKSPACE_DIR,
882
+ onLog: log
883
+ });
884
+ }
885
+ if (agentVolumes.agents.length > 0) {
886
+ await seedAgentVolumesIfFresh(backend, handle, {
887
+ agents: agentVolumes.agents,
888
+ hostWorkspace: req.workspacePath,
889
+ onLog: log
890
+ });
891
+ }
892
+ if (req.envFilesToImport && req.envFilesToImport.length > 0) {
893
+ const { copied } = await uploadEnvFiles({
894
+ backend,
895
+ handle,
896
+ workspacePath: req.workspacePath,
897
+ files: req.envFilesToImport,
898
+ workspaceDir: CLOUD_WORKSPACE_DIR,
899
+ onLog: log
900
+ });
901
+ if (copied > 0) log(`copied ${String(copied)} env/config file(s) into /workspace`);
902
+ }
903
+ log("launching agentbox-ctl daemon");
904
+ await launchCloudCtlDaemon({
905
+ backend,
906
+ handle,
907
+ boxId: id,
908
+ boxName: name,
909
+ relayUrl: `http://127.0.0.1:${String(8787)}`,
910
+ relayToken,
911
+ bridgeToken
912
+ });
913
+ log("launching in-box dockerd");
914
+ try {
915
+ const dockerd = await launchCloudDockerdDaemon({ backend, handle, timeoutMs: 6e4 });
916
+ if (!dockerd.up) log(`dockerd did not become ready (continuing): ${dockerd.reason ?? "unknown"}`);
917
+ } catch (err) {
918
+ log(`dockerd daemon launch failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
919
+ }
920
+ const vncEnabled = req.vnc?.enabled !== false;
921
+ const vncPassword = vncEnabled ? generateVncPassword() : void 0;
922
+ if (vncEnabled && vncPassword) {
923
+ log("launching VNC stack (Xvnc + websockify + noVNC)");
924
+ try {
925
+ await launchCloudVncDaemon({ backend, handle, vncPassword });
926
+ } catch (err) {
927
+ log(
928
+ `VNC daemon launch failed (continuing): ${err instanceof Error ? err.message : String(err)}`
929
+ );
930
+ }
931
+ }
932
+ let webPreview;
933
+ try {
934
+ webPreview = await backend.previewUrl(handle, CLOUD_WEB_PROXY_PORT);
935
+ } catch {
936
+ webPreview = void 0;
937
+ }
938
+ const portlessOpt = req.providerOptions?.["portless"] ?? true;
939
+ let portlessAliasName;
940
+ let portlessUrlResolved;
941
+ if (portlessOpt && webPreview) {
942
+ const r = await bootstrapPortlessForCloudBox(backend, handle, {
943
+ boxName: name,
944
+ webPreviewUrl: webPreview.url,
945
+ webPort: CLOUD_WEB_PROXY_PORT,
946
+ onLog: log
947
+ });
948
+ if (r) {
949
+ portlessAliasName = r.alias;
950
+ portlessUrlResolved = r.url;
951
+ }
952
+ }
953
+ const servicePorts = await readExposedServicePorts(req.workspacePath);
954
+ const servicePreviews = {};
955
+ for (const port of servicePorts) {
956
+ if (port === CLOUD_WEB_PROXY_PORT) continue;
957
+ try {
958
+ const p = await backend.previewUrl(handle, port);
959
+ servicePreviews[port] = p.url;
960
+ } catch {
961
+ }
962
+ }
963
+ let relayPreview;
964
+ try {
965
+ relayPreview = await backend.previewUrl(handle, 8787);
966
+ } catch {
967
+ relayPreview = void 0;
968
+ }
969
+ if (relayPreview) {
970
+ try {
971
+ await registerBoxWithRelay({
972
+ boxId: id,
973
+ token: relayToken,
974
+ name,
975
+ kind: "cloud",
976
+ backend: backend.name,
977
+ previewUrl: relayPreview.url,
978
+ previewToken: relayPreview.token,
979
+ bridgeToken,
980
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
981
+ });
982
+ } catch (err) {
983
+ log(
984
+ `register with host relay failed (continuing): ${err instanceof Error ? err.message : String(err)}`
985
+ );
986
+ }
987
+ }
988
+ const state = await readState();
989
+ const projectIndex = req.projectRoot ? allocateProjectIndex(state, req.projectRoot) : void 0;
990
+ const record = {
991
+ id,
992
+ name,
993
+ provider: providerName,
994
+ // `container` carries the sandbox id with a `cloud:` prefix —
995
+ // unique within state, never collides with a real docker
996
+ // container, and grepping for `agentbox-cloud-*` (the old
997
+ // synthetic value) finds nothing now. `image` mirrors the
998
+ // resolved cloud image so `BoxRecord.image: string` stays
999
+ // required without docker-internal readers seeing `undefined`.
1000
+ container: `cloud:${handle.sandboxId}`,
1001
+ image,
1002
+ workspacePath: req.workspacePath,
1003
+ projectRoot: req.projectRoot,
1004
+ projectIndex,
1005
+ relayToken,
1006
+ withPlaywright: req.withPlaywright,
1007
+ withEnv: req.withEnv,
1008
+ portlessAlias: portlessAliasName,
1009
+ portlessUrl: portlessUrlResolved,
1010
+ vncEnabled,
1011
+ vncPassword,
1012
+ vncContainerPort: vncEnabled ? CLOUD_VNC_PORT : void 0,
1013
+ resourceLimits: req.limits ? {
1014
+ memoryBytes: req.limits.memoryBytes ?? void 0,
1015
+ cpus: req.limits.cpus ?? void 0,
1016
+ pidsLimit: req.limits.pidsLimit ?? void 0,
1017
+ disk: req.limits.disk ?? void 0
1018
+ } : void 0,
1019
+ cloud: {
1020
+ backend: backend.name,
1021
+ sandboxId: handle.sandboxId,
1022
+ image,
1023
+ webPort: CLOUD_WEB_PROXY_PORT,
1024
+ previewUrls: (() => {
1025
+ const m = { ...servicePreviews };
1026
+ if (webPreview) m[CLOUD_WEB_PROXY_PORT] = webPreview.url;
1027
+ return Object.keys(m).length > 0 ? m : void 0;
1028
+ })(),
1029
+ relayPreviewUrl: relayPreview?.url,
1030
+ relayPreviewToken: relayPreview?.token,
1031
+ bridgeToken,
1032
+ snapshotRef: resolvedCheckpointRef
1033
+ },
1034
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1035
+ };
1036
+ await recordBox(record);
1037
+ return { record, imageBuilt: false };
1038
+ } catch (err) {
1039
+ try {
1040
+ await backend.destroy(handle);
1041
+ } catch {
1042
+ }
1043
+ throw err;
1044
+ }
1045
+ },
1046
+ async start(box) {
1047
+ const h = handleFor(box);
1048
+ await backend.start(h);
1049
+ const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1050
+ let webPreview;
1051
+ try {
1052
+ webPreview = await backend.previewUrl(h, webPort);
1053
+ } catch {
1054
+ const cached = box.cloud?.previewUrls?.[webPort];
1055
+ webPreview = cached ? { url: cached } : void 0;
1056
+ }
1057
+ const servicePreviews = {};
1058
+ try {
1059
+ const ports = await readExposedServicePorts(box.workspacePath);
1060
+ for (const port of ports) {
1061
+ if (port === webPort) continue;
1062
+ try {
1063
+ const p = await backend.previewUrl(h, port);
1064
+ servicePreviews[port] = p.url;
1065
+ } catch {
1066
+ }
1067
+ }
1068
+ } catch {
1069
+ }
1070
+ let relayPreview;
1071
+ try {
1072
+ relayPreview = await backend.previewUrl(h, 8787);
1073
+ } catch {
1074
+ relayPreview = box.cloud?.relayPreviewUrl ? { url: box.cloud.relayPreviewUrl, token: box.cloud.relayPreviewToken } : void 0;
1075
+ }
1076
+ const mergedPreviews = {
1077
+ ...box.cloud?.previewUrls ?? {},
1078
+ ...servicePreviews
1079
+ };
1080
+ if (webPreview !== void 0) mergedPreviews[webPort] = webPreview.url;
1081
+ let portlessAliasName = box.portlessAlias;
1082
+ let portlessUrlResolved = box.portlessUrl;
1083
+ if (box.portlessAlias && webPreview) {
1084
+ const r = await bootstrapPortlessForCloudBox(backend, h, {
1085
+ boxName: box.name,
1086
+ webPreviewUrl: webPreview.url,
1087
+ webPort,
1088
+ onLog: () => {
1089
+ }
1090
+ });
1091
+ if (r) {
1092
+ portlessAliasName = r.alias;
1093
+ portlessUrlResolved = r.url;
1094
+ }
1095
+ }
1096
+ const next = {
1097
+ ...box,
1098
+ portlessAlias: portlessAliasName,
1099
+ portlessUrl: portlessUrlResolved,
1100
+ cloud: {
1101
+ ...box.cloud ?? { backend: providerName, sandboxId: h.sandboxId },
1102
+ webPort,
1103
+ previewUrls: Object.keys(mergedPreviews).length > 0 ? mergedPreviews : void 0,
1104
+ relayPreviewUrl: relayPreview?.url ?? box.cloud?.relayPreviewUrl,
1105
+ relayPreviewToken: relayPreview?.token ?? box.cloud?.relayPreviewToken
1106
+ }
1107
+ };
1108
+ await recordBox(next);
1109
+ await launchCloudCtlDaemon({
1110
+ backend,
1111
+ handle: h,
1112
+ boxId: box.id,
1113
+ boxName: box.name,
1114
+ relayUrl: `http://127.0.0.1:${String(8787)}`,
1115
+ relayToken: box.relayToken ?? "",
1116
+ bridgeToken: box.cloud?.bridgeToken
1117
+ });
1118
+ try {
1119
+ const dockerd = await launchCloudDockerdDaemon({ backend, handle: h, timeoutMs: 6e4 });
1120
+ if (!dockerd.up) {
1121
+ }
1122
+ } catch {
1123
+ }
1124
+ if (box.vncEnabled && box.vncPassword) {
1125
+ try {
1126
+ await launchCloudVncDaemon({ backend, handle: h, vncPassword: box.vncPassword });
1127
+ } catch {
1128
+ }
1129
+ }
1130
+ if (relayPreview && box.relayToken && box.cloud?.bridgeToken) {
1131
+ try {
1132
+ await registerBoxWithRelay({
1133
+ boxId: box.id,
1134
+ token: box.relayToken,
1135
+ name: box.name,
1136
+ kind: "cloud",
1137
+ backend: backend.name,
1138
+ previewUrl: relayPreview.url,
1139
+ previewToken: relayPreview.token,
1140
+ bridgeToken: box.cloud.bridgeToken,
1141
+ createdAt: box.createdAt,
1142
+ projectIndex: box.projectIndex
1143
+ });
1144
+ } catch {
1145
+ }
1146
+ }
1147
+ return next;
1148
+ },
1149
+ async pause(box) {
1150
+ await backend.pause(handleFor(box));
1151
+ },
1152
+ async resume(box) {
1153
+ await backend.resume(handleFor(box));
1154
+ },
1155
+ async stop(box) {
1156
+ await backend.stop(handleFor(box));
1157
+ },
1158
+ async destroy(box) {
1159
+ try {
1160
+ await backend.destroy(handleFor(box));
1161
+ } catch (err) {
1162
+ const msg = err instanceof Error ? err.message : String(err);
1163
+ if (!/not.?found|missing/i.test(msg)) throw err;
1164
+ }
1165
+ if (box.portlessAlias) {
1166
+ try {
1167
+ await portlessUnalias(box.portlessAlias);
1168
+ } catch {
1169
+ }
1170
+ }
1171
+ try {
1172
+ await forgetBoxFromRelay(box.id);
1173
+ } catch {
1174
+ }
1175
+ await removeBoxRecord(box.id);
1176
+ },
1177
+ async probeState(box) {
1178
+ return probe(box);
1179
+ },
1180
+ async inspect(box) {
1181
+ const state = await probe(box);
1182
+ const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1183
+ const portlessWebUrl = box.portlessAlias !== void 0 ? box.portlessUrl ?? `https://${box.portlessAlias}.localhost` : void 0;
1184
+ const cachedWebUrl = box.cloud?.previewUrls?.[webPort];
1185
+ const webUrl = portlessWebUrl ?? cachedWebUrl;
1186
+ const endpoints = [];
1187
+ if (webUrl) {
1188
+ endpoints.push({
1189
+ kind: "web",
1190
+ name: "web",
1191
+ containerPort: webPort,
1192
+ url: webUrl,
1193
+ reachable: true
1194
+ });
1195
+ }
1196
+ for (const [portStr, url] of Object.entries(box.cloud?.previewUrls ?? {})) {
1197
+ const port = Number.parseInt(portStr, 10);
1198
+ if (!Number.isFinite(port) || port === webPort) continue;
1199
+ endpoints.push({
1200
+ kind: "web",
1201
+ name: `service-${String(port)}`,
1202
+ containerPort: port,
1203
+ url,
1204
+ reachable: true
1205
+ });
1206
+ }
1207
+ return {
1208
+ record: box,
1209
+ state,
1210
+ endpoints: {
1211
+ domain: webUrl ? new URL(webUrl).host : "",
1212
+ domainIsOrb: false,
1213
+ endpoints
1214
+ },
1215
+ raw: void 0
1216
+ };
1217
+ },
1218
+ async exec(box, argv, opts2) {
1219
+ const r = await backend.exec(handleFor(box), quoteShellArgv(argv), {
1220
+ cwd: opts2?.cwd,
1221
+ env: opts2?.env,
1222
+ user: opts2?.user
1223
+ });
1224
+ return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
1225
+ },
1226
+ async buildAttach(box, kind, opts2) {
1227
+ if (!backend.attachArgv) {
1228
+ throw new Error(
1229
+ `cloud backend '${backend.name}' does not implement attachArgv \u2014 interactive attach not supported`
1230
+ );
1231
+ }
1232
+ const handle = handleFor(box);
1233
+ const baseArgv = await backend.attachArgv(handle);
1234
+ const inner = renderInnerCommand(kind, opts2);
1235
+ const argv = [...baseArgv.slice(1), "-t", inner];
1236
+ const fullArgv = [baseArgv[0], ...argv];
1237
+ const cleanup = backend.revokeAttachToken ? async () => {
1238
+ await backend.revokeAttachToken(handle, baseArgv);
1239
+ } : void 0;
1240
+ return { argv: fullArgv, cleanup };
1241
+ },
1242
+ async uploadPath(box, hostSrc, boxDst) {
1243
+ return uploadToCloudBox(backend, handleFor(box), hostSrc, boxDst);
1244
+ },
1245
+ async downloadPath(box, boxSrc, hostDst) {
1246
+ return downloadFromCloudBox(backend, handleFor(box), boxSrc, hostDst);
1247
+ },
1248
+ async downloadDirContents(box, boxSrc, hostDst) {
1249
+ return pullCloudDirContents(backend, handleFor(box), boxSrc, hostDst);
1250
+ },
1251
+ async resolveUrl(box, opts2) {
1252
+ const h = handleFor(box);
1253
+ const kind = opts2?.kind ?? "web";
1254
+ const port = kind === "vnc" ? CLOUD_VNC_PORT : box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1255
+ if (backend.signedPreviewUrl) {
1256
+ const ttl = opts2?.ttl ?? DEFAULT_SIGNED_URL_TTL_SECONDS;
1257
+ const signed = await backend.signedPreviewUrl(h, port, ttl);
1258
+ return signed.url;
1259
+ }
1260
+ const p = await backend.previewUrl(h, port);
1261
+ throw new Error(
1262
+ `cloud backend '${backend.name}' does not support signed preview URLs; the standard URL (${p.url}) requires a header token (e.g. x-daytona-preview-token: ${p.token ?? "<unset>"}) that browsers can't attach from a click. Use a programmatic client or wait for backend support.`
1263
+ );
1264
+ },
1265
+ // Cloud checkpoint capability. Backends without `createSnapshot` get a
1266
+ // capability stub whose methods throw — the CLI's `agentbox checkpoint
1267
+ // create` then surfaces a clean "not supported" error rather than a
1268
+ // silent no-op.
1269
+ checkpoint: makeCloudCheckpoint(backend)
1270
+ // stats is provider-optional; cloud backends without a metrics API just
1271
+ // omit it. Backends that have one can decorate the returned provider.
1272
+ };
1273
+ }
1274
+ function makeCloudCheckpoint(backend) {
1275
+ return {
1276
+ async create(box, name) {
1277
+ if (!backend.createSnapshot) {
1278
+ throw new Error(
1279
+ `cloud backend '${backend.name}' doesn't support snapshots \u2014 \`agentbox checkpoint\` unavailable`
1280
+ );
1281
+ }
1282
+ if (!box.projectRoot) {
1283
+ throw new Error(
1284
+ `cloud checkpoint requires the box to have a project root (run \`agentbox checkpoint\` from inside the project)`
1285
+ );
1286
+ }
1287
+ if (!box.cloud?.sandboxId) {
1288
+ throw new Error(`cloud box ${box.name} has no sandboxId \u2014 record is malformed`);
1289
+ }
1290
+ const snapshotName = cloudSnapshotName(box.projectRoot, name);
1291
+ await backend.createSnapshot({ sandboxId: box.cloud.sandboxId }, snapshotName);
1292
+ const info = await writeCloudCheckpointManifest(box.projectRoot, backend.name, name, {
1293
+ snapshotName,
1294
+ sourceBoxId: box.id,
1295
+ sourceBoxName: box.name
1296
+ });
1297
+ return { ref: info.name };
1298
+ },
1299
+ async list(projectRoot) {
1300
+ const entries = await listCloudCheckpoints(projectRoot, backend.name);
1301
+ return entries.map((e) => ({ ref: e.name, createdAt: e.manifest.createdAt }));
1302
+ },
1303
+ async remove(projectRoot, ref) {
1304
+ const entry = await resolveCloudCheckpoint(projectRoot, backend.name, ref);
1305
+ if (!entry) return;
1306
+ if (backend.deleteSnapshot) {
1307
+ try {
1308
+ await backend.deleteSnapshot(entry.manifest.snapshotName);
1309
+ } catch {
1310
+ }
1311
+ }
1312
+ await removeCloudCheckpointDir(projectRoot, backend.name, ref);
1313
+ }
1314
+ };
1315
+ }
1316
+ function renderInnerCommand(kind, opts) {
1317
+ const sessionName = opts?.sessionName ?? defaultSessionName(kind);
1318
+ const fallback = opts?.command ?? defaultCommand(kind, opts);
1319
+ if (kind === "logs") {
1320
+ return fallback;
1321
+ }
1322
+ if (opts?.noTmux) {
1323
+ return fallback;
1324
+ }
1325
+ return `command -v tmux >/dev/null || { echo "tmux not installed in sandbox"; exit 127; }; exec tmux new-session -A -c ${shellSingle(CLOUD_WORKSPACE_DIR)} -s ${shellSingle(sessionName)} ${shellSingle(fallback)}`;
1326
+ }
1327
+ function defaultSessionName(kind) {
1328
+ switch (kind) {
1329
+ case "shell":
1330
+ return "shell";
1331
+ case "agent":
1332
+ return "agent";
1333
+ case "logs":
1334
+ return "logs";
1335
+ }
1336
+ }
1337
+ function defaultCommand(kind, opts) {
1338
+ switch (kind) {
1339
+ case "shell":
1340
+ return "bash -l";
1341
+ case "agent":
1342
+ return "bash -l";
1343
+ case "logs": {
1344
+ if (!opts?.service) {
1345
+ return 'echo "no service specified \u2014 set BuildAttachOptions.service"';
1346
+ }
1347
+ const tail = opts.tail !== void 0 ? String(opts.tail) : "200";
1348
+ const args = [`--tail ${shellSingle(tail)}`];
1349
+ if (opts.follow !== false) args.push("--follow");
1350
+ return `/usr/local/bin/agentbox-ctl logs ${shellSingle(opts.service)} ${args.join(" ")}`;
1351
+ }
1352
+ }
1353
+ }
1354
+ function shellSingle(s) {
1355
+ return "'" + s.replace(/'/g, "'\\''") + "'";
1356
+ }
1357
+
1358
+ export {
1359
+ ensureAgentVolumesForCloud,
1360
+ seedAgentVolumesIfFresh,
1361
+ agentSpecsForCloud,
1362
+ listCloudCheckpoints,
1363
+ resolveCloudCheckpoint,
1364
+ createCloudProvider
1365
+ };
1366
+ //# sourceMappingURL=chunk-NW5NYTQM.js.map