@madarco/agentbox 0.9.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 (36) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +161 -0
  3. package/dist/{_cloud-attach-ZXBCNWJX.js → _cloud-attach-O6NYTLES.js} +3 -3
  4. package/dist/{chunk-BXQMIEHC.js → chunk-2GPORKYF.js} +254 -162
  5. package/dist/chunk-2GPORKYF.js.map +1 -0
  6. package/dist/{chunk-NCJP5MTN.js → chunk-7UIAO7PC.js} +213 -51
  7. package/dist/chunk-7UIAO7PC.js.map +1 -0
  8. package/dist/{chunk-GU5LW4B5.js → chunk-R4O5WPHW.js} +374 -62
  9. package/dist/chunk-R4O5WPHW.js.map +1 -0
  10. package/dist/{dist-GDHP34ZK.js → dist-5FQGYRW5.js} +15 -3
  11. package/dist/dist-5FQGYRW5.js.map +1 -0
  12. package/dist/{dist-32EZBYG4.js → dist-BQNX7RQE.js} +12 -2
  13. package/dist/{dist-XML54CNB.js → dist-PZW3GWWU.js} +30 -5
  14. package/dist/dist-PZW3GWWU.js.map +1 -0
  15. package/dist/{dist-CX5CGVEB.js → dist-TMHSUVTP.js} +3 -3
  16. package/dist/index.js +1773 -526
  17. package/dist/index.js.map +1 -1
  18. package/package.json +9 -7
  19. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +9 -8
  20. package/runtime/docker/packages/ctl/dist/bin.cjs +32 -3
  21. package/runtime/hetzner/agentbox-setup-skill.md +9 -8
  22. package/runtime/hetzner/ctl.cjs +32 -3
  23. package/runtime/relay/bin.cjs +32 -3
  24. package/runtime/vercel/agentbox-setup-skill.md +9 -8
  25. package/runtime/vercel/ctl.cjs +32 -3
  26. package/runtime/vercel/custom-system-CLAUDE.md +1 -4
  27. package/runtime/vercel/scripts/provision.sh +40 -0
  28. package/share/agentbox-setup/SKILL.md +9 -8
  29. package/dist/chunk-BXQMIEHC.js.map +0 -1
  30. package/dist/chunk-GU5LW4B5.js.map +0 -1
  31. package/dist/chunk-NCJP5MTN.js.map +0 -1
  32. package/dist/dist-GDHP34ZK.js.map +0 -1
  33. package/dist/dist-XML54CNB.js.map +0 -1
  34. /package/dist/{_cloud-attach-ZXBCNWJX.js.map → _cloud-attach-O6NYTLES.js.map} +0 -0
  35. /package/dist/{dist-32EZBYG4.js.map → dist-BQNX7RQE.js.map} +0 -0
  36. /package/dist/{dist-CX5CGVEB.js.map → dist-TMHSUVTP.js.map} +0 -0
@@ -27,7 +27,7 @@ import {
27
27
  stageOpencodeCredentialsForUpload,
28
28
  stageOpencodeStateForUpload,
29
29
  stageOpencodeStaticForUpload
30
- } from "./chunk-NCJP5MTN.js";
30
+ } from "./chunk-7UIAO7PC.js";
31
31
  import {
32
32
  allocateProjectIndex,
33
33
  detectGitRepos,
@@ -334,6 +334,27 @@ async function removeCloudCheckpointDir(projectRoot, backend, name) {
334
334
  await rm(dir, { recursive: true, force: true });
335
335
  return true;
336
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
+ }
337
358
  var WORKSPACE_DIR_DEFAULT = "/workspace";
338
359
  var REMOTE_TAR_PATH = "/tmp/agentbox-envfiles.tar";
339
360
  async function uploadEnvFiles(args) {
@@ -642,6 +663,8 @@ async function launchCloudCtlDaemon(args) {
642
663
  if (args.relayUrl) env.push(`AGENTBOX_RELAY_URL=${quoteShellArgv([args.relayUrl])}`);
643
664
  if (args.relayToken) env.push(`AGENTBOX_RELAY_TOKEN=${quoteShellArgv([args.relayToken])}`);
644
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)])}`);
645
668
  const script = [
646
669
  `set -e`,
647
670
  `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`,
@@ -1048,6 +1071,141 @@ function createCloudProvider(backend, opts = {}) {
1048
1071
  return "missing";
1049
1072
  }
1050
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
+ }
1051
1209
  return {
1052
1210
  name: providerName,
1053
1211
  async create(req) {
@@ -1085,32 +1243,56 @@ function createCloudProvider(backend, opts = {}) {
1085
1243
  }
1086
1244
  const agentVolumes = await ensureAgentVolumesForCloud(backend, { onLog: log });
1087
1245
  const exposeServicePorts = await readExposedServicePorts(req.workspacePath);
1088
- log(
1089
- snapshotName ? `provisioning ${providerName} sandbox from snapshot` : `provisioning ${providerName} sandbox`
1090
- );
1091
- const handle = await backend.provision({
1092
- name,
1093
- image,
1094
- snapshot: snapshotName,
1095
- resources,
1096
- timeoutMs,
1097
- exposePorts: exposeServicePorts,
1098
- networkPolicy,
1099
- env: {
1100
- AGENTBOX_BOX_ID: id,
1101
- AGENTBOX_BOX_NAME: name,
1102
- AGENTBOX_BOX_KIND: "cloud",
1103
- // In-sandbox relay is on the box's loopback at the in-box port.
1104
- // 8788 is distinct from the host relay's 8787 so a nested agentbox
1105
- // run inside the box can claim :8787 without colliding.
1106
- AGENTBOX_RELAY_URL: `http://127.0.0.1:${String(8788)}`,
1107
- AGENTBOX_RELAY_TOKEN: relayToken,
1108
- AGENTBOX_BRIDGE_TOKEN: bridgeToken,
1109
- ...agentVolumes.env
1110
- },
1111
- volumes: agentVolumes.mounts,
1112
- onLog: log
1113
- });
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
+ }
1114
1296
  try {
1115
1297
  if (snapshotName) {
1116
1298
  log("skipping workspace seed \u2014 snapshot already contains /workspace");
@@ -1169,7 +1351,8 @@ function createCloudProvider(backend, opts = {}) {
1169
1351
  boxName: name,
1170
1352
  relayUrl: `http://127.0.0.1:${String(8788)}`,
1171
1353
  relayToken,
1172
- bridgeToken
1354
+ bridgeToken,
1355
+ webProxyPort: backend.webProxyPort
1173
1356
  });
1174
1357
  if (opts.launchDockerd !== false) {
1175
1358
  log("launching in-box dockerd");
@@ -1192,9 +1375,10 @@ function createCloudProvider(backend, opts = {}) {
1192
1375
  );
1193
1376
  }
1194
1377
  }
1378
+ const wp = backend.webProxyPort ?? CLOUD_WEB_PROXY_PORT;
1195
1379
  let webPreview;
1196
1380
  try {
1197
- webPreview = await backend.previewUrl(handle, CLOUD_WEB_PROXY_PORT);
1381
+ webPreview = await backend.previewUrl(handle, wp);
1198
1382
  } catch {
1199
1383
  webPreview = void 0;
1200
1384
  }
@@ -1205,7 +1389,7 @@ function createCloudProvider(backend, opts = {}) {
1205
1389
  const r = await bootstrapPortlessForCloudBox(backend, handle, {
1206
1390
  boxName: name,
1207
1391
  webPreviewUrl: webPreview.url,
1208
- webPort: CLOUD_WEB_PROXY_PORT,
1392
+ webPort: wp,
1209
1393
  onLog: log
1210
1394
  });
1211
1395
  if (r) {
@@ -1239,7 +1423,7 @@ function createCloudProvider(backend, opts = {}) {
1239
1423
  const servicePorts = exposeServicePorts;
1240
1424
  const servicePreviews = {};
1241
1425
  for (const port of servicePorts) {
1242
- if (port === CLOUD_WEB_PROXY_PORT) continue;
1426
+ if (port === wp) continue;
1243
1427
  try {
1244
1428
  const p = await backend.previewUrl(handle, port);
1245
1429
  servicePreviews[port] = p.url;
@@ -1310,16 +1494,17 @@ function createCloudProvider(backend, opts = {}) {
1310
1494
  backend: backend.name,
1311
1495
  sandboxId: handle.sandboxId,
1312
1496
  image,
1313
- webPort: CLOUD_WEB_PROXY_PORT,
1497
+ webPort: wp,
1314
1498
  previewUrls: (() => {
1315
1499
  const m = { ...servicePreviews };
1316
- if (webPreview) m[CLOUD_WEB_PROXY_PORT] = webPreview.url;
1500
+ if (webPreview) m[wp] = webPreview.url;
1317
1501
  return Object.keys(m).length > 0 ? m : void 0;
1318
1502
  })(),
1319
1503
  relayPreviewUrl: relayPreview?.url,
1320
1504
  relayPreviewToken: relayPreview?.token,
1321
1505
  bridgeToken,
1322
- snapshotRef: resolvedCheckpointRef
1506
+ snapshotRef: resolvedCheckpointRef,
1507
+ lastState: "running"
1323
1508
  },
1324
1509
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1325
1510
  };
@@ -1336,137 +1521,20 @@ function createCloudProvider(backend, opts = {}) {
1336
1521
  async start(box) {
1337
1522
  const h = handleFor(box);
1338
1523
  await backend.start(h);
1339
- const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1340
- let webPreview;
1341
- try {
1342
- webPreview = await backend.previewUrl(h, webPort);
1343
- } catch {
1344
- const cached = box.cloud?.previewUrls?.[webPort];
1345
- webPreview = cached ? { url: cached } : void 0;
1346
- }
1347
- const servicePreviews = {};
1348
- try {
1349
- const ports = await readExposedServicePorts(box.workspacePath);
1350
- for (const port of ports) {
1351
- if (port === webPort) continue;
1352
- try {
1353
- const p = await backend.previewUrl(h, port);
1354
- servicePreviews[port] = p.url;
1355
- } catch {
1356
- }
1357
- }
1358
- } catch {
1359
- }
1360
- let relayPreview;
1361
- try {
1362
- relayPreview = await backend.previewUrl(h, 8788);
1363
- } catch {
1364
- relayPreview = box.cloud?.relayPreviewUrl ? { url: box.cloud.relayPreviewUrl, token: box.cloud.relayPreviewToken } : void 0;
1365
- }
1366
- const mergedPreviews = {
1367
- ...box.cloud?.previewUrls ?? {},
1368
- ...servicePreviews
1369
- };
1370
- if (webPreview !== void 0) mergedPreviews[webPort] = webPreview.url;
1371
- let portlessAliasName = box.portlessAlias;
1372
- let portlessUrlResolved = box.portlessUrl;
1373
- if (box.portlessAlias && webPreview) {
1374
- const r = await bootstrapPortlessForCloudBox(backend, h, {
1375
- boxName: box.name,
1376
- webPreviewUrl: webPreview.url,
1377
- webPort,
1378
- onLog: () => {
1379
- }
1380
- });
1381
- if (r) {
1382
- portlessAliasName = r.alias;
1383
- portlessUrlResolved = r.url;
1384
- }
1385
- }
1386
- let portlessVncAliasName = box.portlessVncAlias;
1387
- let portlessVncUrlResolved = box.portlessVncUrl;
1388
- if (box.portlessVncAlias && box.vncEnabled) {
1389
- try {
1390
- const vncPreview = await backend.previewUrl(h, CLOUD_VNC_PORT);
1391
- const url = await registerHostPortlessAlias({
1392
- alias: box.portlessVncAlias,
1393
- previewUrl: vncPreview.url,
1394
- label: "vnc",
1395
- onLog: () => {
1396
- }
1397
- });
1398
- if (url) {
1399
- portlessVncAliasName = box.portlessVncAlias;
1400
- portlessVncUrlResolved = url;
1401
- }
1402
- } catch {
1403
- }
1404
- }
1405
- const next = {
1406
- ...box,
1407
- portlessAlias: portlessAliasName,
1408
- portlessUrl: portlessUrlResolved,
1409
- portlessVncAlias: portlessVncAliasName,
1410
- portlessVncUrl: portlessVncUrlResolved,
1411
- cloud: {
1412
- ...box.cloud ?? { backend: providerName, sandboxId: h.sandboxId },
1413
- webPort,
1414
- previewUrls: Object.keys(mergedPreviews).length > 0 ? mergedPreviews : void 0,
1415
- relayPreviewUrl: relayPreview?.url ?? box.cloud?.relayPreviewUrl,
1416
- relayPreviewToken: relayPreview?.token ?? box.cloud?.relayPreviewToken
1417
- }
1418
- };
1419
- await recordBox(next);
1420
- await launchCloudCtlDaemon({
1421
- backend,
1422
- handle: h,
1423
- boxId: box.id,
1424
- boxName: box.name,
1425
- relayUrl: `http://127.0.0.1:${String(8788)}`,
1426
- relayToken: box.relayToken ?? "",
1427
- bridgeToken: box.cloud?.bridgeToken
1428
- });
1429
- if (opts.launchDockerd !== false) {
1430
- try {
1431
- const dockerd = await launchCloudDockerdDaemon({ backend, handle: h, timeoutMs: 6e4 });
1432
- if (!dockerd.up) {
1433
- }
1434
- } catch {
1435
- }
1436
- }
1437
- if (box.vncEnabled && box.vncPassword) {
1438
- try {
1439
- await launchCloudVncDaemon({ backend, handle: h, vncPassword: box.vncPassword });
1440
- } catch {
1441
- }
1442
- }
1443
- if (relayPreview && box.relayToken && box.cloud?.bridgeToken) {
1444
- try {
1445
- await registerBoxWithRelay({
1446
- boxId: box.id,
1447
- token: box.relayToken,
1448
- name: box.name,
1449
- kind: "cloud",
1450
- backend: backend.name,
1451
- previewUrl: relayPreview.url,
1452
- previewToken: relayPreview.token,
1453
- bridgeToken: box.cloud.bridgeToken,
1454
- createdAt: box.createdAt,
1455
- projectIndex: box.projectIndex
1456
- });
1457
- } catch {
1458
- }
1459
- }
1460
- return next;
1524
+ return reEnsureCloudBox(box, h);
1461
1525
  },
1462
1526
  async pause(box) {
1463
1527
  await backend.pause(handleFor(box));
1528
+ await persistLastState(box, "paused");
1464
1529
  },
1465
1530
  async resume(box) {
1466
- await backend.resume(handleFor(box));
1531
+ const h = handleFor(box);
1532
+ await backend.resume(h);
1533
+ await reEnsureCloudBox(box, h);
1467
1534
  },
1468
1535
  async stop(box) {
1469
1536
  await backend.stop(handleFor(box));
1537
+ await persistLastState(box, "paused");
1470
1538
  },
1471
1539
  async destroy(box) {
1472
1540
  try {
@@ -1498,7 +1566,7 @@ function createCloudProvider(backend, opts = {}) {
1498
1566
  },
1499
1567
  async inspect(box) {
1500
1568
  const state = await probe(box);
1501
- const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
1569
+ const webPort = box.cloud?.webPort ?? backend.webProxyPort ?? CLOUD_WEB_PROXY_PORT;
1502
1570
  const portlessWebUrl = box.portlessAlias !== void 0 ? box.portlessUrl ?? `https://${box.portlessAlias}.localhost` : void 0;
1503
1571
  const cachedWebUrl = box.cloud?.previewUrls?.[webPort];
1504
1572
  const webUrl = portlessWebUrl ?? cachedWebUrl;
@@ -1578,11 +1646,22 @@ function createCloudProvider(backend, opts = {}) {
1578
1646
  return box.portlessVncUrl ?? `https://${box.portlessVncAlias}.localhost`;
1579
1647
  }
1580
1648
  }
1581
- 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;
1582
1650
  if (backend.signedPreviewUrl) {
1583
1651
  const ttl = opts2?.ttl ?? DEFAULT_SIGNED_URL_TTL_SECONDS;
1584
- const signed = await backend.signedPreviewUrl(h, port, ttl);
1585
- 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
+ }
1586
1665
  }
1587
1666
  const p = await backend.previewUrl(h, port);
1588
1667
  throw new Error(
@@ -1607,6 +1686,18 @@ function createCloudProvider(backend, opts = {}) {
1607
1686
  // omit it. Backends that have one can decorate the returned provider.
1608
1687
  };
1609
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
+ }
1610
1701
  function makeCloudCheckpoint(backend) {
1611
1702
  return {
1612
1703
  async create(box, name) {
@@ -1709,7 +1800,8 @@ export {
1709
1800
  resolveCloudCheckpoint,
1710
1801
  writeCloudCheckpointManifest,
1711
1802
  removeCloudCheckpointDir,
1803
+ probeCloudCheckpoint,
1712
1804
  createCloudProvider,
1713
1805
  renderInnerCommand
1714
1806
  };
1715
- //# sourceMappingURL=chunk-BXQMIEHC.js.map
1807
+ //# sourceMappingURL=chunk-2GPORKYF.js.map