@madarco/agentbox 0.8.0 → 0.10.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 (51) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +161 -0
  3. package/dist/{_cloud-attach-T727ZPRV.js → _cloud-attach-O6NYTLES.js} +4 -4
  4. package/dist/{chunk-67N47KUS.js → chunk-2GPORKYF.js} +349 -182
  5. package/dist/chunk-2GPORKYF.js.map +1 -0
  6. package/dist/{chunk-6OZDFNBF.js → chunk-7UIAO7PC.js} +401 -82
  7. package/dist/chunk-7UIAO7PC.js.map +1 -0
  8. package/dist/{chunk-BGK32PZE.js → chunk-KL36BRN4.js} +2 -2
  9. package/dist/chunk-KL36BRN4.js.map +1 -0
  10. package/dist/chunk-MTVI44DW.js +662 -0
  11. package/dist/chunk-MTVI44DW.js.map +1 -0
  12. package/dist/{chunk-FODMEHD3.js → chunk-R4O5WPHW.js} +705 -77
  13. package/dist/chunk-R4O5WPHW.js.map +1 -0
  14. package/dist/{dist-ZODPD2I6.js → dist-5FQGYRW5.js} +20 -10
  15. package/dist/dist-5FQGYRW5.js.map +1 -0
  16. package/dist/{dist-LOZBWMBF.js → dist-BQNX7RQE.js} +19 -3
  17. package/dist/dist-PZW3GWWU.js +874 -0
  18. package/dist/dist-PZW3GWWU.js.map +1 -0
  19. package/dist/{dist-L4LCG5SJ.js → dist-TMHSUVTP.js} +4 -4
  20. package/dist/index.js +2385 -842
  21. package/dist/index.js.map +1 -1
  22. package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js → prepared-state-CL4CWXQA-H5THETIM.js} +2 -2
  23. package/package.json +11 -7
  24. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +9 -8
  25. package/runtime/docker/packages/ctl/dist/bin.cjs +129 -31
  26. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
  27. package/runtime/hetzner/agentbox-setup-skill.md +9 -8
  28. package/runtime/hetzner/agentbox-vnc-start +15 -1
  29. package/runtime/hetzner/ctl.cjs +129 -31
  30. package/runtime/relay/bin.cjs +260 -39
  31. package/runtime/vercel/agentbox-checkpoint-cleanup +52 -0
  32. package/runtime/vercel/agentbox-codex-hooks.json +68 -0
  33. package/runtime/vercel/agentbox-open +28 -0
  34. package/runtime/vercel/agentbox-setup-skill.md +197 -0
  35. package/runtime/vercel/agentbox-vnc-start +91 -0
  36. package/runtime/vercel/claude-managed-settings.json +115 -0
  37. package/runtime/vercel/ctl.cjs +23495 -0
  38. package/runtime/vercel/custom-system-CLAUDE.md +47 -0
  39. package/runtime/vercel/gh-shim +263 -0
  40. package/runtime/vercel/git-shim +131 -0
  41. package/runtime/vercel/scripts/provision.sh +314 -0
  42. package/share/agentbox-setup/SKILL.md +9 -8
  43. package/dist/chunk-67N47KUS.js.map +0 -1
  44. package/dist/chunk-6OZDFNBF.js.map +0 -1
  45. package/dist/chunk-BGK32PZE.js.map +0 -1
  46. package/dist/chunk-FODMEHD3.js.map +0 -1
  47. package/dist/dist-ZODPD2I6.js.map +0 -1
  48. /package/dist/{_cloud-attach-T727ZPRV.js.map → _cloud-attach-O6NYTLES.js.map} +0 -0
  49. /package/dist/{dist-LOZBWMBF.js.map → dist-BQNX7RQE.js.map} +0 -0
  50. /package/dist/{dist-L4LCG5SJ.js.map → dist-TMHSUVTP.js.map} +0 -0
  51. /package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js.map → prepared-state-CL4CWXQA-H5THETIM.js.map} +0 -0
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CLAUDE_FORWARDED_ENV_KEYS,
4
+ CODEX_CREDENTIALS_BACKUP_FILE,
4
5
  CODEX_FORWARDED_ENV_KEYS,
6
+ CREDENTIALS_BACKUP_FILE,
7
+ OPENCODE_CREDENTIALS_BACKUP_FILE,
5
8
  OPENCODE_FORWARDED_ENV_KEYS,
6
9
  buildHostEnvFindArgs,
7
10
  buildTmuxConfigShellSnippet,
@@ -10,6 +13,7 @@ import {
10
13
  generateRelayToken,
11
14
  generateVncPassword,
12
15
  hashProjectPath,
16
+ isRealAgentCredential,
13
17
  portlessAlias,
14
18
  portlessGetUrl,
15
19
  portlessUnalias,
@@ -23,22 +27,24 @@ import {
23
27
  stageOpencodeCredentialsForUpload,
24
28
  stageOpencodeStateForUpload,
25
29
  stageOpencodeStaticForUpload
26
- } from "./chunk-6OZDFNBF.js";
30
+ } from "./chunk-7UIAO7PC.js";
27
31
  import {
28
32
  allocateProjectIndex,
29
33
  detectGitRepos,
30
34
  readState,
31
35
  recordBox,
32
36
  removeBoxRecord
33
- } from "./chunk-BGK32PZE.js";
37
+ } from "./chunk-KL36BRN4.js";
34
38
 
35
39
  // ../../packages/sandbox-cloud/dist/index.js
36
40
  import { randomBytes } from "crypto";
37
41
  import { basename as basename2 } from "path";
38
- import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
42
+ import { chmod, mkdir, writeFile } from "fs/promises";
43
+ import { dirname } from "path";
44
+ import { mkdir as mkdir2, readFile, readdir, rm, writeFile as writeFile2 } from "fs/promises";
39
45
  import { homedir } from "os";
40
46
  import { basename, join } from "path";
41
- import { mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
47
+ import { mkdtemp, rm as rm2, writeFile as writeFile3 } from "fs/promises";
42
48
  import { tmpdir } from "os";
43
49
  import { join as join2 } from "path";
44
50
  import { execa } from "execa";
@@ -228,6 +234,39 @@ function agentSpecsForCloud() {
228
234
  credentialsSubpath: s.credentialsSubpath
229
235
  }));
230
236
  }
237
+ var EXTRACT_SPECS = [
238
+ { kind: "claude", boxPath: "/home/vscode/.claude/.credentials.json", hostBackup: CREDENTIALS_BACKUP_FILE },
239
+ { kind: "codex", boxPath: "/home/vscode/.codex/auth.json", hostBackup: CODEX_CREDENTIALS_BACKUP_FILE },
240
+ {
241
+ kind: "opencode",
242
+ boxPath: "/home/vscode/.local/share/opencode/auth.json",
243
+ hostBackup: OPENCODE_CREDENTIALS_BACKUP_FILE
244
+ }
245
+ ];
246
+ async function extractCloudAgentCredentials(backend, handle, opts = {}) {
247
+ const log = opts.onLog ?? (() => {
248
+ });
249
+ const extracted = [];
250
+ for (const spec of EXTRACT_SPECS) {
251
+ const hostBackup = opts.backups?.[spec.kind] ?? spec.hostBackup;
252
+ try {
253
+ const r = await backend.exec(handle, `cat ${spec.boxPath} 2>/dev/null`, { noRetry: true });
254
+ const text = r.stdout;
255
+ if (r.exitCode !== 0 || !text || !isRealAgentCredential(spec.kind, text)) continue;
256
+ await mkdir(dirname(hostBackup), { recursive: true });
257
+ await writeFile(hostBackup, text, { mode: 384 });
258
+ await chmod(hostBackup, 384).catch(() => {
259
+ });
260
+ extracted.push(spec.kind);
261
+ log(`extracted ${spec.kind} login from box to ${hostBackup}`);
262
+ } catch (err) {
263
+ log(
264
+ `WARN: ${spec.kind} credential extract failed (${err instanceof Error ? err.message : String(err)}) \u2014 skipping`
265
+ );
266
+ }
267
+ }
268
+ return extracted;
269
+ }
231
270
  var CLOUD_CHECKPOINTS_ROOT = join(homedir(), ".agentbox", "cloud-checkpoints");
232
271
  var CLOUD_SNAPSHOT_NAME_PREFIX = "agentbox-ckpt-";
233
272
  function cloudSnapshotName(projectRoot, name) {
@@ -275,7 +314,7 @@ async function resolveCloudCheckpoint(projectRoot, backend, ref) {
275
314
  }
276
315
  async function writeCloudCheckpointManifest(projectRoot, backend, name, fields) {
277
316
  const dir = checkpointDir(backend, projectRoot, name);
278
- await mkdir(dir, { recursive: true });
317
+ await mkdir2(dir, { recursive: true });
279
318
  const manifest = {
280
319
  schema: 1,
281
320
  name,
@@ -285,7 +324,7 @@ async function writeCloudCheckpointManifest(projectRoot, backend, name, fields)
285
324
  sourceBoxName: fields.sourceBoxName,
286
325
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
287
326
  };
288
- await writeFile(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
327
+ await writeFile2(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
289
328
  return { name, dir, manifest };
290
329
  }
291
330
  async function removeCloudCheckpointDir(projectRoot, backend, name) {
@@ -295,6 +334,27 @@ async function removeCloudCheckpointDir(projectRoot, backend, name) {
295
334
  await rm(dir, { recursive: true, force: true });
296
335
  return true;
297
336
  }
337
+ async function probeCloudCheckpoint(backend, projectRoot, ref) {
338
+ const found = await resolveCloudCheckpoint(projectRoot, backend.name, ref);
339
+ if (!found) return { live: false, pruned: false };
340
+ if (!backend.snapshotExists) return { live: true, pruned: false };
341
+ const live = await backend.snapshotExists(found.manifest.snapshotName);
342
+ if (live) return { live: true, pruned: false };
343
+ await removeCloudCheckpointDir(projectRoot, backend.name, ref);
344
+ return { live: false, pruned: true };
345
+ }
346
+ function isSnapshotGoneError(err) {
347
+ if (err === null || typeof err !== "object") return false;
348
+ const e = err;
349
+ const status = e.response?.status ?? e.status;
350
+ if (status === 410) return true;
351
+ const parts = [
352
+ typeof e.json?.error?.message === "string" ? e.json.error.message : "",
353
+ typeof e.message === "string" ? e.message : ""
354
+ ];
355
+ const msg = parts.join(" ").toLowerCase();
356
+ return /snapshot[^.]*\b(expired|deleted|gone|not[ -]?found)\b/.test(msg) || msg.includes("expired or deleted");
357
+ }
298
358
  var WORKSPACE_DIR_DEFAULT = "/workspace";
299
359
  var REMOTE_TAR_PATH = "/tmp/agentbox-envfiles.tar";
300
360
  async function uploadEnvFiles(args) {
@@ -324,7 +384,7 @@ async function uploadEnvFiles(args) {
324
384
  log(`env-file tar pack failed: ${String(packed.stderr).slice(0, 300)}`);
325
385
  return { copied: 0 };
326
386
  }
327
- await writeFile2(join2(stage, ".marker"), "").catch(() => {
387
+ await writeFile3(join2(stage, ".marker"), "").catch(() => {
328
388
  });
329
389
  await args.backend.uploadFile(args.handle, localTar, REMOTE_TAR_PATH);
330
390
  const extract = await args.backend.exec(
@@ -418,7 +478,8 @@ async function uploadOneEntry(args) {
418
478
  }
419
479
  parts.push(`rm -f ${remoteTar}`);
420
480
  const cmd = parts.join(" && ");
421
- const res = await args.backend.exec(args.handle, cmd);
481
+ const execOpts = args.backend.name === "vercel" ? { user: "root" } : void 0;
482
+ const res = await args.backend.exec(args.handle, cmd, execOpts);
422
483
  if (res.exitCode !== 0) {
423
484
  throw new Error(
424
485
  `in-box extract failed (exit ${String(res.exitCode)}): ${(res.stderr || res.stdout).slice(-300)}`
@@ -602,6 +663,8 @@ async function launchCloudCtlDaemon(args) {
602
663
  if (args.relayUrl) env.push(`AGENTBOX_RELAY_URL=${quoteShellArgv([args.relayUrl])}`);
603
664
  if (args.relayToken) env.push(`AGENTBOX_RELAY_TOKEN=${quoteShellArgv([args.relayToken])}`);
604
665
  if (args.bridgeToken) env.push(`AGENTBOX_BRIDGE_TOKEN=${quoteShellArgv([args.bridgeToken])}`);
666
+ if (args.webProxyPort !== void 0)
667
+ env.push(`AGENTBOX_WEB_PROXY_PORT=${quoteShellArgv([String(args.webProxyPort)])}`);
605
668
  const script = [
606
669
  `set -e`,
607
670
  `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`,
@@ -687,6 +750,7 @@ async function seedCloudWorkspace(args) {
687
750
  workspaceDir,
688
751
  bundleDepth: args.bundleDepth,
689
752
  fromBranch: args.fromBranch,
753
+ useBranch: args.useBranch,
690
754
  onLog: log
691
755
  });
692
756
  for (const r of nested) {
@@ -725,8 +789,8 @@ async function seedFromGitClone(args) {
725
789
  const cloneDir = join5(stage, "clone");
726
790
  const tarPath = join5(stage, "workspace.tar.gz");
727
791
  const untrackedTarPath = join5(stage, "untracked.tar.gz");
728
- const stashSha = await safeStashCreate(args.hostRepo);
729
- const untrackedSize = await maybeBuildUntrackedTar(args.hostRepo, untrackedTarPath);
792
+ const stashSha = args.useBranch ? null : await safeStashCreate(args.hostRepo);
793
+ const untrackedSize = args.useBranch ? 0 : await maybeBuildUntrackedTar(args.hostRepo, untrackedTarPath);
730
794
  let stashRefCreated = false;
731
795
  try {
732
796
  if (stashSha) {
@@ -743,7 +807,8 @@ async function seedFromGitClone(args) {
743
807
  log(
744
808
  adaptive ? `clone: depth=${String(DEFAULT_BUNDLE_DEPTH)} (default, adaptive)` : initialDepth === null ? "clone: depth=full (configured)" : `clone: depth=${String(initialDepth)} (configured)`
745
809
  );
746
- await runShallowClone(args.hostRepo, cloneDir, initialDepth, stashRefCreated, args.fromBranch);
810
+ const cloneBranch = args.useBranch ?? args.fromBranch;
811
+ await runShallowClone(args.hostRepo, cloneDir, initialDepth, stashRefCreated, cloneBranch);
747
812
  await tarCloneDir(cloneDir, tarPath);
748
813
  if (adaptive && initialDepth !== null) {
749
814
  const size = await safeFileSize(tarPath);
@@ -754,7 +819,7 @@ async function seedFromGitClone(args) {
754
819
  );
755
820
  await rm5(cloneDir, { recursive: true, force: true });
756
821
  await rm5(tarPath, { force: true });
757
- await runShallowClone(args.hostRepo, cloneDir, LARGE_BUNDLE_DEPTH, stashRefCreated, args.fromBranch);
822
+ await runShallowClone(args.hostRepo, cloneDir, LARGE_BUNDLE_DEPTH, stashRefCreated, cloneBranch);
758
823
  await tarCloneDir(cloneDir, tarPath);
759
824
  }
760
825
  }
@@ -791,7 +856,10 @@ async function seedFromGitClone(args) {
791
856
  `$SUDO chown "$(id -un):$(id -gn)" ${quoteShellArgv([args.workspaceDir])}`,
792
857
  `tar -C ${quoteShellArgv([args.workspaceDir])} -xzf ${quoteShellArgv([remoteTar])}`,
793
858
  setOrigin,
794
- `git -C ${quoteShellArgv([args.workspaceDir])} checkout -B ${quoteShellArgv([args.branch])}`,
859
+ // reuse: the clone already landed on `<branch>` (pinned via `--branch`);
860
+ // a plain checkout materializes the working tree without resetting the
861
+ // ref. fork: `-B` (re)points `<branch>` at the clone HEAD.
862
+ args.useBranch ? `git -C ${quoteShellArgv([args.workspaceDir])} checkout ${quoteShellArgv([args.branch])}` : `git -C ${quoteShellArgv([args.workspaceDir])} checkout -B ${quoteShellArgv([args.branch])}`,
795
863
  ...carryOverSteps,
796
864
  `rm -f ${quoteShellArgv([remoteTar])}`
797
865
  ].join("\n");
@@ -989,7 +1057,9 @@ function createCloudProvider(backend, opts = {}) {
989
1057
  return {
990
1058
  id,
991
1059
  name,
992
- branch: `agentbox/${name}`
1060
+ // --use-branch reuses the named branch directly; otherwise fork a fresh
1061
+ // per-box branch. The CLI validated `useBranch` exists host-side.
1062
+ branch: req.useBranch ?? `agentbox/${name}`
993
1063
  };
994
1064
  }
995
1065
  async function probe(box) {
@@ -1001,6 +1071,141 @@ function createCloudProvider(backend, opts = {}) {
1001
1071
  return "missing";
1002
1072
  }
1003
1073
  }
1074
+ async function persistLastState(box, lastState) {
1075
+ if (!box.cloud) return;
1076
+ try {
1077
+ await recordBox({ ...box, cloud: { ...box.cloud, lastState } });
1078
+ } catch {
1079
+ }
1080
+ }
1081
+ async function reEnsureCloudBox(box, h) {
1082
+ const webPort = box.cloud?.webPort ?? backend.webProxyPort ?? CLOUD_WEB_PROXY_PORT;
1083
+ let webPreview;
1084
+ try {
1085
+ webPreview = await backend.previewUrl(h, webPort);
1086
+ } catch {
1087
+ const cached = box.cloud?.previewUrls?.[webPort];
1088
+ webPreview = cached ? { url: cached } : void 0;
1089
+ }
1090
+ const servicePreviews = {};
1091
+ try {
1092
+ const ports = await readExposedServicePorts(box.workspacePath);
1093
+ for (const port of ports) {
1094
+ if (port === webPort) continue;
1095
+ try {
1096
+ const p = await backend.previewUrl(h, port);
1097
+ servicePreviews[port] = p.url;
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ } catch {
1102
+ }
1103
+ let relayPreview;
1104
+ try {
1105
+ relayPreview = await backend.previewUrl(h, 8788);
1106
+ } catch {
1107
+ relayPreview = box.cloud?.relayPreviewUrl ? { url: box.cloud.relayPreviewUrl, token: box.cloud.relayPreviewToken } : void 0;
1108
+ }
1109
+ const mergedPreviews = {
1110
+ ...box.cloud?.previewUrls ?? {},
1111
+ ...servicePreviews
1112
+ };
1113
+ if (webPreview !== void 0) mergedPreviews[webPort] = webPreview.url;
1114
+ let portlessAliasName = box.portlessAlias;
1115
+ let portlessUrlResolved = box.portlessUrl;
1116
+ if (box.portlessAlias && webPreview) {
1117
+ const r = await bootstrapPortlessForCloudBox(backend, h, {
1118
+ boxName: box.name,
1119
+ webPreviewUrl: webPreview.url,
1120
+ webPort,
1121
+ onLog: () => {
1122
+ }
1123
+ });
1124
+ if (r) {
1125
+ portlessAliasName = r.alias;
1126
+ portlessUrlResolved = r.url;
1127
+ }
1128
+ }
1129
+ let portlessVncAliasName = box.portlessVncAlias;
1130
+ let portlessVncUrlResolved = box.portlessVncUrl;
1131
+ if (box.portlessVncAlias && box.vncEnabled) {
1132
+ try {
1133
+ const vncPreview = await backend.previewUrl(h, CLOUD_VNC_PORT);
1134
+ const url = await registerHostPortlessAlias({
1135
+ alias: box.portlessVncAlias,
1136
+ previewUrl: vncPreview.url,
1137
+ label: "vnc",
1138
+ onLog: () => {
1139
+ }
1140
+ });
1141
+ if (url) {
1142
+ portlessVncAliasName = box.portlessVncAlias;
1143
+ portlessVncUrlResolved = url;
1144
+ }
1145
+ } catch {
1146
+ }
1147
+ }
1148
+ const next = {
1149
+ ...box,
1150
+ portlessAlias: portlessAliasName,
1151
+ portlessUrl: portlessUrlResolved,
1152
+ portlessVncAlias: portlessVncAliasName,
1153
+ portlessVncUrl: portlessVncUrlResolved,
1154
+ cloud: {
1155
+ ...box.cloud ?? { backend: providerName, sandboxId: h.sandboxId },
1156
+ webPort,
1157
+ previewUrls: Object.keys(mergedPreviews).length > 0 ? mergedPreviews : void 0,
1158
+ relayPreviewUrl: relayPreview?.url ?? box.cloud?.relayPreviewUrl,
1159
+ relayPreviewToken: relayPreview?.token ?? box.cloud?.relayPreviewToken,
1160
+ // reEnsureCloudBox only runs on a freshly-woken box (start/resume), so
1161
+ // the box is now running — persist it for the fast `agentbox list` path.
1162
+ lastState: "running"
1163
+ }
1164
+ };
1165
+ await recordBox(next);
1166
+ await launchCloudCtlDaemon({
1167
+ backend,
1168
+ handle: h,
1169
+ boxId: box.id,
1170
+ boxName: box.name,
1171
+ relayUrl: `http://127.0.0.1:${String(8788)}`,
1172
+ relayToken: box.relayToken ?? "",
1173
+ bridgeToken: box.cloud?.bridgeToken,
1174
+ webProxyPort: backend.webProxyPort
1175
+ });
1176
+ if (opts.launchDockerd !== false) {
1177
+ try {
1178
+ const dockerd = await launchCloudDockerdDaemon({ backend, handle: h, timeoutMs: 6e4 });
1179
+ if (!dockerd.up) {
1180
+ }
1181
+ } catch {
1182
+ }
1183
+ }
1184
+ if (box.vncEnabled && box.vncPassword) {
1185
+ try {
1186
+ await launchCloudVncDaemon({ backend, handle: h, vncPassword: box.vncPassword });
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ if (relayPreview && box.relayToken && box.cloud?.bridgeToken) {
1191
+ try {
1192
+ await registerBoxWithRelay({
1193
+ boxId: box.id,
1194
+ token: box.relayToken,
1195
+ name: box.name,
1196
+ kind: "cloud",
1197
+ backend: backend.name,
1198
+ previewUrl: relayPreview.url,
1199
+ previewToken: relayPreview.token,
1200
+ bridgeToken: box.cloud.bridgeToken,
1201
+ createdAt: box.createdAt,
1202
+ projectIndex: box.projectIndex
1203
+ });
1204
+ } catch {
1205
+ }
1206
+ }
1207
+ return next;
1208
+ }
1004
1209
  return {
1005
1210
  name: providerName,
1006
1211
  async create(req) {
@@ -1008,7 +1213,13 @@ function createCloudProvider(backend, opts = {}) {
1008
1213
  });
1009
1214
  const { id, name, branch } = mintBox(req);
1010
1215
  const image = opts.provisionImage ? await opts.provisionImage(req) : req.image ?? FALLBACK_IMAGE;
1011
- const resources = opts.defaultResources ?? { cpu: 2, memory: 4, disk: 8 };
1216
+ const baseResources = opts.defaultResources ?? { cpu: 2, memory: 4, disk: 8 };
1217
+ const vcpuOverride = req.providerOptions?.["vcpus"];
1218
+ const resources = typeof vcpuOverride === "number" && vcpuOverride > 0 ? { ...baseResources, cpu: vcpuOverride } : baseResources;
1219
+ const timeoutOverride = req.providerOptions?.["timeoutMs"];
1220
+ const timeoutMs = typeof timeoutOverride === "number" && timeoutOverride > 0 ? timeoutOverride : void 0;
1221
+ const networkPolicyOpt = req.providerOptions?.["networkPolicy"];
1222
+ const networkPolicy = typeof networkPolicyOpt === "string" && networkPolicyOpt.trim() !== "" ? networkPolicyOpt.trim() : void 0;
1012
1223
  const relayToken = generateRelayToken();
1013
1224
  const bridgeToken = generateRelayToken();
1014
1225
  try {
@@ -1031,29 +1242,57 @@ function createCloudProvider(backend, opts = {}) {
1031
1242
  }
1032
1243
  }
1033
1244
  const agentVolumes = await ensureAgentVolumesForCloud(backend, { onLog: log });
1034
- log(
1035
- snapshotName ? `provisioning ${providerName} sandbox from snapshot` : `provisioning ${providerName} sandbox`
1036
- );
1037
- const handle = await backend.provision({
1038
- name,
1039
- image,
1040
- snapshot: snapshotName,
1041
- resources,
1042
- env: {
1043
- AGENTBOX_BOX_ID: id,
1044
- AGENTBOX_BOX_NAME: name,
1045
- AGENTBOX_BOX_KIND: "cloud",
1046
- // In-sandbox relay is on the box's loopback at the in-box port.
1047
- // 8788 is distinct from the host relay's 8787 so a nested agentbox
1048
- // run inside the box can claim :8787 without colliding.
1049
- AGENTBOX_RELAY_URL: `http://127.0.0.1:${String(8788)}`,
1050
- AGENTBOX_RELAY_TOKEN: relayToken,
1051
- AGENTBOX_BRIDGE_TOKEN: bridgeToken,
1052
- ...agentVolumes.env
1053
- },
1054
- volumes: agentVolumes.mounts,
1055
- onLog: log
1056
- });
1245
+ const exposeServicePorts = await readExposedServicePorts(req.workspacePath);
1246
+ const provisionEnv = {
1247
+ AGENTBOX_BOX_ID: id,
1248
+ AGENTBOX_BOX_NAME: name,
1249
+ AGENTBOX_BOX_KIND: "cloud",
1250
+ // In-sandbox relay is on the box's loopback at the in-box port.
1251
+ // 8788 is distinct from the host relay's 8787 so a nested agentbox
1252
+ // run inside the box can claim :8787 without colliding.
1253
+ AGENTBOX_RELAY_URL: `http://127.0.0.1:${String(8788)}`,
1254
+ AGENTBOX_RELAY_TOKEN: relayToken,
1255
+ AGENTBOX_BRIDGE_TOKEN: bridgeToken,
1256
+ ...agentVolumes.env
1257
+ };
1258
+ const provisionFrom = async (snapshot) => {
1259
+ log(
1260
+ snapshot ? `provisioning ${providerName} sandbox from snapshot` : `provisioning ${providerName} sandbox`
1261
+ );
1262
+ return backend.provision({
1263
+ name,
1264
+ image,
1265
+ snapshot,
1266
+ resources,
1267
+ timeoutMs,
1268
+ exposePorts: exposeServicePorts,
1269
+ networkPolicy,
1270
+ env: provisionEnv,
1271
+ volumes: agentVolumes.mounts,
1272
+ onLog: log
1273
+ });
1274
+ };
1275
+ let handle;
1276
+ try {
1277
+ handle = await provisionFrom(snapshotName);
1278
+ } catch (err) {
1279
+ if (snapshotName && isSnapshotGoneError(err)) {
1280
+ log(
1281
+ `checkpoint snapshot '${resolvedCheckpointRef ?? snapshotName}' has expired or been deleted; starting a fresh box from the base image instead`
1282
+ );
1283
+ if (req.projectRoot && resolvedCheckpointRef) {
1284
+ try {
1285
+ await removeCloudCheckpointDir(req.projectRoot, backend.name, resolvedCheckpointRef);
1286
+ } catch {
1287
+ }
1288
+ }
1289
+ snapshotName = void 0;
1290
+ resolvedCheckpointRef = void 0;
1291
+ handle = await provisionFrom(void 0);
1292
+ } else {
1293
+ throw err;
1294
+ }
1295
+ }
1057
1296
  try {
1058
1297
  if (snapshotName) {
1059
1298
  log("skipping workspace seed \u2014 snapshot already contains /workspace");
@@ -1066,6 +1305,7 @@ function createCloudProvider(backend, opts = {}) {
1066
1305
  workspaceDir: CLOUD_WORKSPACE_DIR,
1067
1306
  bundleDepth: req.bundleDepth,
1068
1307
  fromBranch: req.fromBranch,
1308
+ useBranch: req.useBranch,
1069
1309
  onLog: log
1070
1310
  });
1071
1311
  }
@@ -1111,14 +1351,17 @@ function createCloudProvider(backend, opts = {}) {
1111
1351
  boxName: name,
1112
1352
  relayUrl: `http://127.0.0.1:${String(8788)}`,
1113
1353
  relayToken,
1114
- bridgeToken
1354
+ bridgeToken,
1355
+ webProxyPort: backend.webProxyPort
1115
1356
  });
1116
- log("launching in-box dockerd");
1117
- try {
1118
- const dockerd = await launchCloudDockerdDaemon({ backend, handle, timeoutMs: 6e4 });
1119
- if (!dockerd.up) log(`dockerd did not become ready (continuing): ${dockerd.reason ?? "unknown"}`);
1120
- } catch (err) {
1121
- log(`dockerd daemon launch failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1357
+ if (opts.launchDockerd !== false) {
1358
+ log("launching in-box dockerd");
1359
+ try {
1360
+ const dockerd = await launchCloudDockerdDaemon({ backend, handle, timeoutMs: 6e4 });
1361
+ if (!dockerd.up) log(`dockerd did not become ready (continuing): ${dockerd.reason ?? "unknown"}`);
1362
+ } catch (err) {
1363
+ log(`dockerd daemon launch failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1364
+ }
1122
1365
  }
1123
1366
  const vncEnabled = req.vnc?.enabled !== false;
1124
1367
  const vncPassword = vncEnabled ? generateVncPassword() : void 0;
@@ -1132,9 +1375,10 @@ function createCloudProvider(backend, opts = {}) {
1132
1375
  );
1133
1376
  }
1134
1377
  }
1378
+ const wp = backend.webProxyPort ?? CLOUD_WEB_PROXY_PORT;
1135
1379
  let webPreview;
1136
1380
  try {
1137
- webPreview = await backend.previewUrl(handle, CLOUD_WEB_PROXY_PORT);
1381
+ webPreview = await backend.previewUrl(handle, wp);
1138
1382
  } catch {
1139
1383
  webPreview = void 0;
1140
1384
  }
@@ -1145,7 +1389,7 @@ function createCloudProvider(backend, opts = {}) {
1145
1389
  const r = await bootstrapPortlessForCloudBox(backend, handle, {
1146
1390
  boxName: name,
1147
1391
  webPreviewUrl: webPreview.url,
1148
- webPort: CLOUD_WEB_PROXY_PORT,
1392
+ webPort: wp,
1149
1393
  onLog: log
1150
1394
  });
1151
1395
  if (r) {
@@ -1176,10 +1420,10 @@ function createCloudProvider(backend, opts = {}) {
1176
1420
  portlessVncUrlResolved = url;
1177
1421
  }
1178
1422
  }
1179
- const servicePorts = await readExposedServicePorts(req.workspacePath);
1423
+ const servicePorts = exposeServicePorts;
1180
1424
  const servicePreviews = {};
1181
1425
  for (const port of servicePorts) {
1182
- if (port === CLOUD_WEB_PROXY_PORT) continue;
1426
+ if (port === wp) continue;
1183
1427
  try {
1184
1428
  const p = await backend.previewUrl(handle, port);
1185
1429
  servicePreviews[port] = p.url;
@@ -1192,12 +1436,15 @@ function createCloudProvider(backend, opts = {}) {
1192
1436
  } catch {
1193
1437
  relayPreview = void 0;
1194
1438
  }
1439
+ const state = await readState();
1440
+ const projectIndex = req.projectRoot ? allocateProjectIndex(state, req.projectRoot) : void 0;
1195
1441
  if (relayPreview) {
1196
1442
  try {
1197
1443
  await registerBoxWithRelay({
1198
1444
  boxId: id,
1199
1445
  token: relayToken,
1200
1446
  name,
1447
+ projectIndex,
1201
1448
  kind: "cloud",
1202
1449
  backend: backend.name,
1203
1450
  previewUrl: relayPreview.url,
@@ -1211,8 +1458,6 @@ function createCloudProvider(backend, opts = {}) {
1211
1458
  );
1212
1459
  }
1213
1460
  }
1214
- const state = await readState();
1215
- const projectIndex = req.projectRoot ? allocateProjectIndex(state, req.projectRoot) : void 0;
1216
1461
  const record = {
1217
1462
  id,
1218
1463
  name,
@@ -1249,16 +1494,17 @@ function createCloudProvider(backend, opts = {}) {
1249
1494
  backend: backend.name,
1250
1495
  sandboxId: handle.sandboxId,
1251
1496
  image,
1252
- webPort: CLOUD_WEB_PROXY_PORT,
1497
+ webPort: wp,
1253
1498
  previewUrls: (() => {
1254
1499
  const m = { ...servicePreviews };
1255
- if (webPreview) m[CLOUD_WEB_PROXY_PORT] = webPreview.url;
1500
+ if (webPreview) m[wp] = webPreview.url;
1256
1501
  return Object.keys(m).length > 0 ? m : void 0;
1257
1502
  })(),
1258
1503
  relayPreviewUrl: relayPreview?.url,
1259
1504
  relayPreviewToken: relayPreview?.token,
1260
1505
  bridgeToken,
1261
- snapshotRef: resolvedCheckpointRef
1506
+ snapshotRef: resolvedCheckpointRef,
1507
+ lastState: "running"
1262
1508
  },
1263
1509
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1264
1510
  };
@@ -1275,135 +1521,20 @@ function createCloudProvider(backend, opts = {}) {
1275
1521
  async start(box) {
1276
1522
  const h = handleFor(box);
1277
1523
  await backend.start(h);
1278
- const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1279
- let webPreview;
1280
- try {
1281
- webPreview = await backend.previewUrl(h, webPort);
1282
- } catch {
1283
- const cached = box.cloud?.previewUrls?.[webPort];
1284
- webPreview = cached ? { url: cached } : void 0;
1285
- }
1286
- const servicePreviews = {};
1287
- try {
1288
- const ports = await readExposedServicePorts(box.workspacePath);
1289
- for (const port of ports) {
1290
- if (port === webPort) continue;
1291
- try {
1292
- const p = await backend.previewUrl(h, port);
1293
- servicePreviews[port] = p.url;
1294
- } catch {
1295
- }
1296
- }
1297
- } catch {
1298
- }
1299
- let relayPreview;
1300
- try {
1301
- relayPreview = await backend.previewUrl(h, 8788);
1302
- } catch {
1303
- relayPreview = box.cloud?.relayPreviewUrl ? { url: box.cloud.relayPreviewUrl, token: box.cloud.relayPreviewToken } : void 0;
1304
- }
1305
- const mergedPreviews = {
1306
- ...box.cloud?.previewUrls ?? {},
1307
- ...servicePreviews
1308
- };
1309
- if (webPreview !== void 0) mergedPreviews[webPort] = webPreview.url;
1310
- let portlessAliasName = box.portlessAlias;
1311
- let portlessUrlResolved = box.portlessUrl;
1312
- if (box.portlessAlias && webPreview) {
1313
- const r = await bootstrapPortlessForCloudBox(backend, h, {
1314
- boxName: box.name,
1315
- webPreviewUrl: webPreview.url,
1316
- webPort,
1317
- onLog: () => {
1318
- }
1319
- });
1320
- if (r) {
1321
- portlessAliasName = r.alias;
1322
- portlessUrlResolved = r.url;
1323
- }
1324
- }
1325
- let portlessVncAliasName = box.portlessVncAlias;
1326
- let portlessVncUrlResolved = box.portlessVncUrl;
1327
- if (box.portlessVncAlias && box.vncEnabled) {
1328
- try {
1329
- const vncPreview = await backend.previewUrl(h, CLOUD_VNC_PORT);
1330
- const url = await registerHostPortlessAlias({
1331
- alias: box.portlessVncAlias,
1332
- previewUrl: vncPreview.url,
1333
- label: "vnc",
1334
- onLog: () => {
1335
- }
1336
- });
1337
- if (url) {
1338
- portlessVncAliasName = box.portlessVncAlias;
1339
- portlessVncUrlResolved = url;
1340
- }
1341
- } catch {
1342
- }
1343
- }
1344
- const next = {
1345
- ...box,
1346
- portlessAlias: portlessAliasName,
1347
- portlessUrl: portlessUrlResolved,
1348
- portlessVncAlias: portlessVncAliasName,
1349
- portlessVncUrl: portlessVncUrlResolved,
1350
- cloud: {
1351
- ...box.cloud ?? { backend: providerName, sandboxId: h.sandboxId },
1352
- webPort,
1353
- previewUrls: Object.keys(mergedPreviews).length > 0 ? mergedPreviews : void 0,
1354
- relayPreviewUrl: relayPreview?.url ?? box.cloud?.relayPreviewUrl,
1355
- relayPreviewToken: relayPreview?.token ?? box.cloud?.relayPreviewToken
1356
- }
1357
- };
1358
- await recordBox(next);
1359
- await launchCloudCtlDaemon({
1360
- backend,
1361
- handle: h,
1362
- boxId: box.id,
1363
- boxName: box.name,
1364
- relayUrl: `http://127.0.0.1:${String(8788)}`,
1365
- relayToken: box.relayToken ?? "",
1366
- bridgeToken: box.cloud?.bridgeToken
1367
- });
1368
- try {
1369
- const dockerd = await launchCloudDockerdDaemon({ backend, handle: h, timeoutMs: 6e4 });
1370
- if (!dockerd.up) {
1371
- }
1372
- } catch {
1373
- }
1374
- if (box.vncEnabled && box.vncPassword) {
1375
- try {
1376
- await launchCloudVncDaemon({ backend, handle: h, vncPassword: box.vncPassword });
1377
- } catch {
1378
- }
1379
- }
1380
- if (relayPreview && box.relayToken && box.cloud?.bridgeToken) {
1381
- try {
1382
- await registerBoxWithRelay({
1383
- boxId: box.id,
1384
- token: box.relayToken,
1385
- name: box.name,
1386
- kind: "cloud",
1387
- backend: backend.name,
1388
- previewUrl: relayPreview.url,
1389
- previewToken: relayPreview.token,
1390
- bridgeToken: box.cloud.bridgeToken,
1391
- createdAt: box.createdAt,
1392
- projectIndex: box.projectIndex
1393
- });
1394
- } catch {
1395
- }
1396
- }
1397
- return next;
1524
+ return reEnsureCloudBox(box, h);
1398
1525
  },
1399
1526
  async pause(box) {
1400
1527
  await backend.pause(handleFor(box));
1528
+ await persistLastState(box, "paused");
1401
1529
  },
1402
1530
  async resume(box) {
1403
- await backend.resume(handleFor(box));
1531
+ const h = handleFor(box);
1532
+ await backend.resume(h);
1533
+ await reEnsureCloudBox(box, h);
1404
1534
  },
1405
1535
  async stop(box) {
1406
1536
  await backend.stop(handleFor(box));
1537
+ await persistLastState(box, "paused");
1407
1538
  },
1408
1539
  async destroy(box) {
1409
1540
  try {
@@ -1435,7 +1566,7 @@ function createCloudProvider(backend, opts = {}) {
1435
1566
  },
1436
1567
  async inspect(box) {
1437
1568
  const state = await probe(box);
1438
- const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1569
+ const webPort = box.cloud?.webPort ?? backend.webProxyPort ?? CLOUD_WEB_PROXY_PORT;
1439
1570
  const portlessWebUrl = box.portlessAlias !== void 0 ? box.portlessUrl ?? `https://${box.portlessAlias}.localhost` : void 0;
1440
1571
  const cachedWebUrl = box.cloud?.previewUrls?.[webPort];
1441
1572
  const webUrl = portlessWebUrl ?? cachedWebUrl;
@@ -1515,11 +1646,22 @@ function createCloudProvider(backend, opts = {}) {
1515
1646
  return box.portlessVncUrl ?? `https://${box.portlessVncAlias}.localhost`;
1516
1647
  }
1517
1648
  }
1518
- const port = kind === "vnc" ? CLOUD_VNC_PORT : box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1649
+ const port = kind === "vnc" ? CLOUD_VNC_PORT : box.cloud?.webPort ?? backend.webProxyPort ?? CLOUD_WEB_PROXY_PORT;
1519
1650
  if (backend.signedPreviewUrl) {
1520
1651
  const ttl = opts2?.ttl ?? DEFAULT_SIGNED_URL_TTL_SECONDS;
1521
- const signed = await backend.signedPreviewUrl(h, port, ttl);
1522
- return signed.url;
1652
+ try {
1653
+ const signed = await backend.signedPreviewUrl(h, port, ttl);
1654
+ return signed.url;
1655
+ } catch (err) {
1656
+ if (kind === "web") {
1657
+ const fallbackPort = await firstExposedServicePort(box);
1658
+ if (fallbackPort !== void 0 && fallbackPort !== port) {
1659
+ const signed = await backend.signedPreviewUrl(h, fallbackPort, ttl);
1660
+ return signed.url;
1661
+ }
1662
+ }
1663
+ throw err;
1664
+ }
1523
1665
  }
1524
1666
  const p = await backend.previewUrl(h, port);
1525
1667
  throw new Error(
@@ -1530,11 +1672,32 @@ function createCloudProvider(backend, opts = {}) {
1530
1672
  // capability stub whose methods throw — the CLI's `agentbox checkpoint
1531
1673
  // create` then surfaces a clean "not supported" error rather than a
1532
1674
  // silent no-op.
1533
- checkpoint: makeCloudCheckpoint(backend)
1675
+ checkpoint: makeCloudCheckpoint(backend),
1676
+ // Extract the box's agent login(s) back to the host (~/.agentbox) so the
1677
+ // next box inherits the login. Lives on the base cloud provider (not inside
1678
+ // `checkpoint.create`) so it works even for providers that override the
1679
+ // whole `checkpoint` capability (vercel). The CLI calls this on
1680
+ // `checkpoint create --set-default`, while the box is guaranteed running.
1681
+ async extractAgentCredentials(box) {
1682
+ if (!box.cloud?.sandboxId) return [];
1683
+ return extractCloudAgentCredentials(backend, { sandboxId: box.cloud.sandboxId });
1684
+ }
1534
1685
  // stats is provider-optional; cloud backends without a metrics API just
1535
1686
  // omit it. Backends that have one can decorate the returned provider.
1536
1687
  };
1537
1688
  }
1689
+ var RESERVED_CLOUD_PORTS = /* @__PURE__ */ new Set([CLOUD_WEB_PROXY_PORT, CLOUD_VNC_PORT, 8788]);
1690
+ async function firstExposedServicePort(box) {
1691
+ const reserved = (p) => RESERVED_CLOUD_PORTS.has(p) || p === box.cloud?.webPort;
1692
+ const fromRecord = Object.keys(box.cloud?.previewUrls ?? {}).map(Number).filter((p) => Number.isInteger(p) && !reserved(p));
1693
+ if (fromRecord.length > 0) return Math.min(...fromRecord);
1694
+ try {
1695
+ const fromYaml = (await readExposedServicePorts(box.workspacePath)).filter((p) => !reserved(p));
1696
+ if (fromYaml.length > 0) return Math.min(...fromYaml);
1697
+ } catch {
1698
+ }
1699
+ return void 0;
1700
+ }
1538
1701
  function makeCloudCheckpoint(backend) {
1539
1702
  return {
1540
1703
  async create(box, name) {
@@ -1635,6 +1798,10 @@ export {
1635
1798
  agentSpecsForCloud,
1636
1799
  listCloudCheckpoints,
1637
1800
  resolveCloudCheckpoint,
1638
- createCloudProvider
1801
+ writeCloudCheckpointManifest,
1802
+ removeCloudCheckpointDir,
1803
+ probeCloudCheckpoint,
1804
+ createCloudProvider,
1805
+ renderInnerCommand
1639
1806
  };
1640
- //# sourceMappingURL=chunk-67N47KUS.js.map
1807
+ //# sourceMappingURL=chunk-2GPORKYF.js.map