@madarco/agentbox 0.7.0 → 0.9.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 (77) hide show
  1. package/dist/_cloud-attach-ZXBCNWJX.js +13 -0
  2. package/dist/{chunk-NW5NYTQM.js → chunk-BXQMIEHC.js} +459 -110
  3. package/dist/chunk-BXQMIEHC.js.map +1 -0
  4. package/dist/{chunk-UK72UQ5U.js → chunk-G3H2L3O2.js} +55 -4
  5. package/dist/chunk-G3H2L3O2.js.map +1 -0
  6. package/dist/{chunk-7KOEFGN2.js → chunk-GU5LW4B5.js} +385 -31
  7. package/dist/chunk-GU5LW4B5.js.map +1 -0
  8. package/dist/chunk-KL36BRN4.js +455 -0
  9. package/dist/chunk-KL36BRN4.js.map +1 -0
  10. package/dist/{chunk-V5KZGB5V.js → chunk-LEV3KICD.js} +18 -2
  11. package/dist/chunk-LEV3KICD.js.map +1 -0
  12. package/dist/chunk-MTVI44DW.js +662 -0
  13. package/dist/chunk-MTVI44DW.js.map +1 -0
  14. package/dist/{chunk-NAVL4R34.js → chunk-NCJP5MTN.js} +1281 -556
  15. package/dist/chunk-NCJP5MTN.js.map +1 -0
  16. package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js → cloud-poller-SUNA6ZQC-2RG5WPRN.js} +2 -2
  17. package/dist/{dist-ETCFRVPA.js → dist-32EZBYG4.js} +50 -20
  18. package/dist/{dist-R67WMLCF.js → dist-CX5CGVEB.js} +120 -10
  19. package/dist/dist-CX5CGVEB.js.map +1 -0
  20. package/dist/{dist-QZGJIBT5.js → dist-GDHP34ZK.js} +141 -75
  21. package/dist/dist-GDHP34ZK.js.map +1 -0
  22. package/dist/dist-XML54CNB.js +849 -0
  23. package/dist/dist-XML54CNB.js.map +1 -0
  24. package/dist/index.js +3881 -867
  25. package/dist/index.js.map +1 -1
  26. package/dist/prepared-state-CL4CWXQA-H5THETIM.js +18 -0
  27. package/dist/prepared-state-CL4CWXQA-H5THETIM.js.map +1 -0
  28. package/package.json +7 -5
  29. package/runtime/daytona/custom-system-CLAUDE.md +39 -0
  30. package/runtime/docker/Dockerfile.box +22 -0
  31. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +1 -1
  32. package/runtime/docker/packages/ctl/dist/bin.cjs +1214 -98
  33. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +66 -35
  34. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
  35. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
  36. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
  37. package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
  38. package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
  39. package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
  40. package/runtime/hetzner/agentbox-codex-hooks.json +66 -35
  41. package/runtime/hetzner/agentbox-setup-skill.md +1 -1
  42. package/runtime/hetzner/agentbox-vnc-start +15 -1
  43. package/runtime/hetzner/claude-managed-settings.json +62 -1
  44. package/runtime/hetzner/ctl.cjs +1214 -98
  45. package/runtime/hetzner/custom-system-CLAUDE.md +26 -14
  46. package/runtime/hetzner/gh-shim +263 -0
  47. package/runtime/hetzner/git-shim +131 -0
  48. package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
  49. package/runtime/hetzner/scripts/install-box.sh +11 -2
  50. package/runtime/relay/bin.cjs +1146 -63
  51. package/runtime/vercel/agentbox-checkpoint-cleanup +52 -0
  52. package/runtime/vercel/agentbox-codex-hooks.json +68 -0
  53. package/runtime/vercel/agentbox-open +28 -0
  54. package/runtime/vercel/agentbox-setup-skill.md +196 -0
  55. package/runtime/vercel/agentbox-vnc-start +91 -0
  56. package/runtime/vercel/claude-managed-settings.json +115 -0
  57. package/runtime/vercel/ctl.cjs +23466 -0
  58. package/runtime/vercel/custom-system-CLAUDE.md +50 -0
  59. package/runtime/vercel/gh-shim +263 -0
  60. package/runtime/vercel/git-shim +131 -0
  61. package/runtime/vercel/scripts/provision.sh +274 -0
  62. package/share/agentbox-setup/SKILL.md +1 -1
  63. package/share/host-skills/agentbox/SKILL.md +29 -0
  64. package/share/host-skills/agentbox-info/SKILL.md +211 -0
  65. package/share/host-skills/codex/agentbox.md +35 -0
  66. package/share/host-skills/opencode/agentbox.md +26 -0
  67. package/dist/_cloud-attach-DMVH6GWO.js +0 -12
  68. package/dist/chunk-7KOEFGN2.js.map +0 -1
  69. package/dist/chunk-NAVL4R34.js.map +0 -1
  70. package/dist/chunk-NW5NYTQM.js.map +0 -1
  71. package/dist/chunk-UK72UQ5U.js.map +0 -1
  72. package/dist/chunk-V5KZGB5V.js.map +0 -1
  73. package/dist/dist-QZGJIBT5.js.map +0 -1
  74. package/dist/dist-R67WMLCF.js.map +0 -1
  75. /package/dist/{_cloud-attach-DMVH6GWO.js.map → _cloud-attach-ZXBCNWJX.js.map} +0 -0
  76. /package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
  77. /package/dist/{dist-ETCFRVPA.js.map → dist-32EZBYG4.js.map} +0 -0
@@ -3514,7 +3514,7 @@ var require_cross_spawn = __commonJS({
3514
3514
  var cp = require("child_process");
3515
3515
  var parse = require_parse();
3516
3516
  var enoent = require_enoent();
3517
- function spawn4(command, args, options) {
3517
+ function spawn6(command, args, options) {
3518
3518
  const parsed = parse(command, args, options);
3519
3519
  const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
3520
3520
  enoent.hookChildProcess(spawned, parsed);
@@ -3526,8 +3526,8 @@ var require_cross_spawn = __commonJS({
3526
3526
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
3527
3527
  return result;
3528
3528
  }
3529
- module2.exports = spawn4;
3530
- module2.exports.spawn = spawn4;
3529
+ module2.exports = spawn6;
3530
+ module2.exports.spawn = spawn6;
3531
3531
  module2.exports.sync = spawnSync2;
3532
3532
  module2.exports._parse = parse;
3533
3533
  module2.exports._enoent = enoent;
@@ -3540,7 +3540,13 @@ __export(cloud_poller_exports, {
3540
3540
  CloudBoxPoller: () => CloudBoxPoller,
3541
3541
  CloudBoxPollers: () => CloudBoxPollers
3542
3542
  });
3543
- var import_node_http, import_node_https, import_promises15, BACKOFF_BASE_MS, BACKOFF_MAX_MS, REQUEST_TIMEOUT_MS, FAST_REQUEST_TIMEOUT_MS, FAST_MODE_DECAY_POLLS, STOPPED_TICK_MS, CloudBoxPoller, CloudBoxPollers;
3543
+ function isConnectionLevelError(err) {
3544
+ const code = err?.code;
3545
+ if (code && CONNECTION_LEVEL_CODES.has(code)) return true;
3546
+ const msg = err instanceof Error ? err.message : String(err);
3547
+ return /\b(ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ECONNRESET|EPIPE)\b/.test(msg);
3548
+ }
3549
+ var import_node_http, import_node_https, import_promises15, BACKOFF_BASE_MS, BACKOFF_MAX_MS, REQUEST_TIMEOUT_MS, FAST_REQUEST_TIMEOUT_MS, FAST_MODE_DECAY_POLLS, STOPPED_TICK_MS, CONNECTION_LEVEL_CODES, CloudBoxPoller, CloudBoxPollers;
3544
3550
  var init_cloud_poller = __esm({
3545
3551
  "src/cloud-poller.ts"() {
3546
3552
  "use strict";
@@ -3553,9 +3559,17 @@ var init_cloud_poller = __esm({
3553
3559
  FAST_REQUEST_TIMEOUT_MS = 8e3;
3554
3560
  FAST_MODE_DECAY_POLLS = 5;
3555
3561
  STOPPED_TICK_MS = 250;
3562
+ CONNECTION_LEVEL_CODES = /* @__PURE__ */ new Set([
3563
+ "ECONNREFUSED",
3564
+ "ENOTFOUND",
3565
+ "EHOSTUNREACH",
3566
+ "ECONNRESET",
3567
+ "EPIPE"
3568
+ ]);
3556
3569
  CloudBoxPoller = class {
3557
3570
  constructor(deps) {
3558
3571
  this.deps = deps;
3572
+ this.currentPreviewUrl = deps.previewUrl;
3559
3573
  }
3560
3574
  deps;
3561
3575
  stopped = false;
@@ -3569,6 +3583,15 @@ var init_cloud_poller = __esm({
3569
3583
  * within ~5 successful round-trips.
3570
3584
  */
3571
3585
  fastModePolls = 0;
3586
+ /**
3587
+ * Mutable copy of `deps.previewUrl`. We don't read `deps.previewUrl`
3588
+ * directly after construction because `recoverPreviewUrl` may hand us a
3589
+ * new URL (e.g. Hetzner reopens its SSH ControlMaster and the `-L`
3590
+ * forward gets a new ephemeral local port).
3591
+ */
3592
+ currentPreviewUrl;
3593
+ /** Guards against recovery storms — at most one recovery attempt in flight. */
3594
+ recovering = null;
3572
3595
  start() {
3573
3596
  if (this.loopPromise) return;
3574
3597
  this.loopPromise = this.run().catch((err) => {
@@ -3585,7 +3608,7 @@ var init_cloud_poller = __esm({
3585
3608
  * and the in-box agent finally sees the answer.
3586
3609
  */
3587
3610
  async respond(actionId, result) {
3588
- const base = this.deps.previewUrl.replace(/\/+$/, "");
3611
+ const base = this.currentPreviewUrl.replace(/\/+$/, "");
3589
3612
  const url = new URL(`${base}/bridge/action-result`);
3590
3613
  const isHttps = url.protocol === "https:";
3591
3614
  const transport = isHttps ? import_node_https.request : import_node_http.request;
@@ -3675,6 +3698,9 @@ var init_cloud_poller = __esm({
3675
3698
  this.fastModePolls = FAST_MODE_DECAY_POLLS;
3676
3699
  }
3677
3700
  this.log(`poll error: ${msg}`);
3701
+ if (this.deps.recoverPreviewUrl && isConnectionLevelError(err)) {
3702
+ await this.tryRecoverPreviewUrl();
3703
+ }
3678
3704
  await this.backoff();
3679
3705
  }
3680
3706
  if (this.currentBackoffMs === 0 && this.fastModePolls > 0) {
@@ -3687,8 +3713,33 @@ var init_cloud_poller = __esm({
3687
3713
  this.currentBackoffMs = this.currentBackoffMs === 0 ? BACKOFF_BASE_MS : Math.min(this.currentBackoffMs * 2, BACKOFF_MAX_MS);
3688
3714
  await (0, import_promises15.setTimeout)(this.currentBackoffMs);
3689
3715
  }
3716
+ async tryRecoverPreviewUrl() {
3717
+ if (!this.deps.recoverPreviewUrl) return;
3718
+ if (this.recovering) {
3719
+ await this.recovering;
3720
+ return;
3721
+ }
3722
+ this.recovering = (async () => {
3723
+ try {
3724
+ const next = await this.deps.recoverPreviewUrl();
3725
+ if (typeof next === "string" && next.length > 0 && next !== this.currentPreviewUrl) {
3726
+ this.log(`preview URL recovered: ${this.currentPreviewUrl} \u2192 ${next}`);
3727
+ this.currentPreviewUrl = next;
3728
+ this.currentBackoffMs = 0;
3729
+ } else if (typeof next === "string" && next === this.currentPreviewUrl) {
3730
+ this.log("preview URL recovered (unchanged)");
3731
+ this.currentBackoffMs = 0;
3732
+ }
3733
+ } catch (err) {
3734
+ this.log(`preview URL recover failed: ${err instanceof Error ? err.message : String(err)}`);
3735
+ } finally {
3736
+ this.recovering = null;
3737
+ }
3738
+ })();
3739
+ await this.recovering;
3740
+ }
3690
3741
  async pollOnce() {
3691
- const base = this.deps.previewUrl.replace(/\/+$/, "");
3742
+ const base = this.currentPreviewUrl.replace(/\/+$/, "");
3692
3743
  const url = new URL(`${base}/bridge/poll?since=${String(this.cursor)}`);
3693
3744
  const isHttps = url.protocol === "https:";
3694
3745
  const transport = isHttps ? import_node_https.request : import_node_http.request;
@@ -11124,10 +11175,11 @@ var import_node_http3 = require("http");
11124
11175
 
11125
11176
  // src/types.ts
11126
11177
  var DEFAULT_RELAY_PORT = 8787;
11178
+ var DEFAULT_BOX_RELAY_PORT = 8788;
11127
11179
  var RELAY_EVENT_RING_SIZE = 1e3;
11128
11180
 
11129
11181
  // src/server.ts
11130
- var import_node_child_process6 = require("child_process");
11182
+ var import_node_child_process7 = require("child_process");
11131
11183
  var import_node_http2 = require("http");
11132
11184
 
11133
11185
  // ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
@@ -17953,6 +18005,8 @@ function projectDockerFields(box) {
17953
18005
  webHostPort: box.webHostPort,
17954
18006
  portlessAlias: box.portlessAlias,
17955
18007
  portlessUrl: box.portlessUrl,
18008
+ portlessVncAlias: box.portlessVncAlias,
18009
+ portlessVncUrl: box.portlessVncUrl,
17956
18010
  dockerVolume: box.dockerVolume,
17957
18011
  dockerCacheShared: box.dockerCacheShared,
17958
18012
  checkpointImage: box.checkpointImage
@@ -17973,8 +18027,262 @@ function findBox(idOrName, state) {
17973
18027
  return { kind: "none" };
17974
18028
  }
17975
18029
 
17976
- // src/prompts.ts
18030
+ // src/gh.ts
18031
+ var import_node_child_process6 = require("child_process");
18032
+ var GH_PR_OPS = [
18033
+ "create",
18034
+ "view",
18035
+ "list",
18036
+ "comment",
18037
+ "review",
18038
+ "merge",
18039
+ "checkout",
18040
+ "close",
18041
+ "reopen"
18042
+ ];
18043
+ function isGhPrOp(value) {
18044
+ return GH_PR_OPS.includes(value);
18045
+ }
18046
+ var GH_PR_READ_ONLY_OPS = /* @__PURE__ */ new Set(["view", "list"]);
18047
+ function injectPrCreateHead(op, branch, args) {
18048
+ if (op !== "create") return args;
18049
+ if (!branch || branch === "HEAD") return args;
18050
+ if (hasHeadArg(args)) return args;
18051
+ return ["--head", branch, ...args];
18052
+ }
18053
+ function hasHeadArg(args) {
18054
+ return args.some((a2) => a2 === "--head" || a2.startsWith("--head=") || a2.startsWith("-H"));
18055
+ }
18056
+ function prCreateNeedsHead(op, args) {
18057
+ return op === "create" && !hasHeadArg(args);
18058
+ }
18059
+ var PR_CREATE_NO_HEAD_REFUSAL = {
18060
+ exitCode: 65,
18061
+ stdout: "",
18062
+ stderr: "gh pr create: refusing to run without --head \u2014 could not resolve this box's branch, and falling back to the host repo's checked-out branch would open a PR for the wrong branch. Ensure the box branch is pushed, or pass --head <branch> explicitly.\n"
18063
+ };
18064
+ var GH_RPC_TIMEOUT_MS = 12e4;
18065
+ var GH_READY_CACHE_TTL_MS = 6e4;
18066
+ var ghReadyCache;
18067
+ async function assertGhReady() {
18068
+ const now = Date.now();
18069
+ if (ghReadyCache && ghReadyCache.expiresAt > now) {
18070
+ return ghReadyCache.result;
18071
+ }
18072
+ const result = await probeGh();
18073
+ ghReadyCache = { result, expiresAt: now + GH_READY_CACHE_TTL_MS };
18074
+ return result;
18075
+ }
18076
+ async function probeGh() {
18077
+ const version = await runHostGh(["--version"], process.cwd(), 1e4);
18078
+ if (version.exitCode === 127 || /ENOENT/.test(version.stderr)) {
18079
+ return {
18080
+ exitCode: 127,
18081
+ stdout: "",
18082
+ stderr: "gh not installed on host (https://cli.github.com)\n"
18083
+ };
18084
+ }
18085
+ if (version.exitCode !== 0) {
18086
+ return {
18087
+ exitCode: version.exitCode,
18088
+ stdout: "",
18089
+ stderr: `gh --version failed: ${version.stderr || version.stdout}`.trimEnd() + "\n"
18090
+ };
18091
+ }
18092
+ const auth = await runHostGh(["auth", "status"], process.cwd(), 15e3);
18093
+ if (auth.exitCode !== 0) {
18094
+ return {
18095
+ exitCode: 4,
18096
+ stdout: "",
18097
+ stderr: "gh not authenticated on host (run `gh auth login`)\n"
18098
+ };
18099
+ }
18100
+ return null;
18101
+ }
18102
+ function runHostGh(args, cwd, timeoutMs = GH_RPC_TIMEOUT_MS) {
18103
+ return new Promise((resolve2) => {
18104
+ const child = (0, import_node_child_process6.spawn)("gh", args, {
18105
+ cwd,
18106
+ env: process.env,
18107
+ stdio: ["ignore", "pipe", "pipe"]
18108
+ });
18109
+ let stdout = "";
18110
+ let stderr = "";
18111
+ let settled = false;
18112
+ const finish = (exitCode) => {
18113
+ if (settled) return;
18114
+ settled = true;
18115
+ resolve2({ exitCode, stdout, stderr });
18116
+ };
18117
+ const timer = setTimeout(() => {
18118
+ child.kill("SIGTERM");
18119
+ stderr += `
18120
+ relay: gh command timed out after ${String(timeoutMs)}ms
18121
+ `;
18122
+ finish(124);
18123
+ }, timeoutMs);
18124
+ child.stdout?.on("data", (chunk) => {
18125
+ stdout += chunk.toString("utf8");
18126
+ });
18127
+ child.stderr?.on("data", (chunk) => {
18128
+ stderr += chunk.toString("utf8");
18129
+ });
18130
+ child.on("error", (err) => {
18131
+ clearTimeout(timer);
18132
+ const code = err.code;
18133
+ stderr += String(err.message ?? err);
18134
+ finish(code === "ENOENT" ? 127 : 1);
18135
+ });
18136
+ child.on("close", (code) => {
18137
+ clearTimeout(timer);
18138
+ finish(code ?? -1);
18139
+ });
18140
+ });
18141
+ }
18142
+ async function checkoutGuards(hostMainRepo, registeredBranches) {
18143
+ const status = await runGitProbe(["-C", hostMainRepo, "status", "--porcelain"]);
18144
+ if (status.exitCode !== 0) {
18145
+ return {
18146
+ exitCode: status.exitCode,
18147
+ stdout: "",
18148
+ stderr: `gh pr checkout: failed to inspect host repo: ${status.stderr || status.stdout}`.trimEnd() + "\n"
18149
+ };
18150
+ }
18151
+ if (status.stdout.trim().length > 0) {
18152
+ return {
18153
+ exitCode: 12,
18154
+ stdout: "",
18155
+ stderr: `gh pr checkout: ${hostMainRepo} has uncommitted changes; refusing to switch branches
18156
+ `
18157
+ };
18158
+ }
18159
+ const head = await runGitProbe(["-C", hostMainRepo, "rev-parse", "--abbrev-ref", "HEAD"]);
18160
+ if (head.exitCode !== 0) {
18161
+ return {
18162
+ exitCode: head.exitCode,
18163
+ stdout: "",
18164
+ stderr: `gh pr checkout: failed to resolve HEAD: ${head.stderr || head.stdout}`.trimEnd() + "\n"
18165
+ };
18166
+ }
18167
+ const currentBranch = head.stdout.trim();
18168
+ if (registeredBranches.includes(currentBranch)) {
18169
+ return {
18170
+ exitCode: 12,
18171
+ stdout: "",
18172
+ stderr: `gh pr checkout: ${hostMainRepo} is on registered box branch ${currentBranch}; refusing (would corrupt the bind-mounted box HEAD)
18173
+ `
18174
+ };
18175
+ }
18176
+ return null;
18177
+ }
18178
+ function runGitProbe(args) {
18179
+ return new Promise((resolve2) => {
18180
+ const child = (0, import_node_child_process6.spawn)("git", args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
18181
+ let stdout = "";
18182
+ let stderr = "";
18183
+ child.stdout?.on("data", (c3) => {
18184
+ stdout += c3.toString("utf8");
18185
+ });
18186
+ child.stderr?.on("data", (c3) => {
18187
+ stderr += c3.toString("utf8");
18188
+ });
18189
+ child.on("error", (err) => {
18190
+ resolve2({ exitCode: 127, stdout, stderr: stderr + String(err.message ?? err) });
18191
+ });
18192
+ child.on("close", (code) => {
18193
+ resolve2({ exitCode: code ?? -1, stdout, stderr });
18194
+ });
18195
+ });
18196
+ }
18197
+ function refuseMergeBypass(op) {
18198
+ if (op !== "merge") return null;
18199
+ if (process.env["AGENTBOX_PROMPT"] !== "off") return null;
18200
+ if (process.env["AGENTBOX_GH_FORCE"] === "1") return null;
18201
+ return {
18202
+ exitCode: 10,
18203
+ stdout: "",
18204
+ stderr: "gh pr merge: AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1 (merge is irreversible)\n"
18205
+ };
18206
+ }
18207
+ function refuseCheckoutByDefault(op) {
18208
+ if (op !== "checkout") return null;
18209
+ if (process.env["AGENTBOX_GH_PR_CHECKOUT"] === "allow") return null;
18210
+ return {
18211
+ exitCode: 13,
18212
+ stdout: "",
18213
+ stderr: "gh pr checkout: disabled by default; set AGENTBOX_GH_PR_CHECKOUT=allow to enable\n"
18214
+ };
18215
+ }
18216
+
18217
+ // src/host-initiated.ts
17977
18218
  var import_node_crypto = require("crypto");
18219
+ var DEFAULT_TTL_MS = 12e4;
18220
+ function hashRpcParams(params) {
18221
+ return (0, import_node_crypto.createHash)("sha256").update(canonicalJson(params)).digest("hex");
18222
+ }
18223
+ function canonicalJson(v) {
18224
+ if (v === null) return "null";
18225
+ if (typeof v === "undefined") return "null";
18226
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
18227
+ if (typeof v === "boolean") return v ? "true" : "false";
18228
+ if (typeof v === "string") return JSON.stringify(v);
18229
+ if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
18230
+ if (typeof v === "object") {
18231
+ const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a2], [b]) => a2 < b ? -1 : a2 > b ? 1 : 0);
18232
+ return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
18233
+ }
18234
+ return "null";
18235
+ }
18236
+ var HostInitiatedTokens = class {
18237
+ store = /* @__PURE__ */ new Map();
18238
+ /**
18239
+ * Mint a fresh one-time token scoped to (boxId, method, paramsHash).
18240
+ * `paramsHash` MUST be supplied for any call surface where the box can
18241
+ * influence the eventual RPC params. Pass `null` only when there are no
18242
+ * params (no current call sites use this).
18243
+ */
18244
+ mint(boxId, method, paramsHash, ttlMs = DEFAULT_TTL_MS) {
18245
+ const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
18246
+ this.store.set(token, { boxId, method, paramsHash, expiresAt: Date.now() + ttlMs });
18247
+ return token;
18248
+ }
18249
+ /**
18250
+ * Returns true exactly once if `token` is a valid, unexpired token for the
18251
+ * given `(boxId, method)` AND the supplied `incomingParamsHash` matches
18252
+ * the hash bound at mint time. The token is removed on a successful match
18253
+ * (one-shot semantics). All failure modes return false — callers fall back
18254
+ * to the normal prompt path.
18255
+ */
18256
+ consume(token, boxId, method, incomingParamsHash) {
18257
+ if (!token || typeof token !== "string") return false;
18258
+ const record = this.store.get(token);
18259
+ if (!record) return false;
18260
+ if (record.expiresAt < Date.now()) {
18261
+ this.store.delete(token);
18262
+ return false;
18263
+ }
18264
+ if (record.boxId !== boxId || record.method !== method) return false;
18265
+ if (record.paramsHash !== null && record.paramsHash !== incomingParamsHash) {
18266
+ return false;
18267
+ }
18268
+ this.store.delete(token);
18269
+ return true;
18270
+ }
18271
+ /** Drop expired entries. Cheap; safe to call periodically. */
18272
+ gc() {
18273
+ const now = Date.now();
18274
+ for (const [token, record] of this.store) {
18275
+ if (record.expiresAt < now) this.store.delete(token);
18276
+ }
18277
+ }
18278
+ /** Test-only: number of live tokens. */
18279
+ size() {
18280
+ return this.store.size;
18281
+ }
18282
+ };
18283
+
18284
+ // src/prompts.ts
18285
+ var import_node_crypto2 = require("crypto");
17978
18286
  var PendingPrompts = class {
17979
18287
  entries = /* @__PURE__ */ new Map();
17980
18288
  add(boxId, ev) {
@@ -18063,7 +18371,7 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
18063
18371
  if (process.env.AGENTBOX_PROMPT === "off") {
18064
18372
  return { answer: "y" };
18065
18373
  }
18066
- const ev = { id: (0, import_node_crypto.randomUUID)(), ...params };
18374
+ const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
18067
18375
  const promise = prompts.add(boxId, ev);
18068
18376
  subscribers.broadcast(boxId, "prompt-ask", ev);
18069
18377
  if (opts?.ttlMs !== void 0 && opts.ttlMs > 0) {
@@ -18092,36 +18400,43 @@ function isPromptAnswerBody(v) {
18092
18400
  async function resolveCloudBackend(name) {
18093
18401
  if (name === "daytona") {
18094
18402
  const pkg = "@agentbox/sandbox-daytona";
18095
- try {
18096
- const mod = await import(pkg);
18097
- return mod.daytonaBackend;
18098
- } catch (err) {
18099
- const msg = err instanceof Error ? err.message : String(err);
18100
- if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18101
- throw new Error(
18102
- `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18103
- );
18104
- }
18105
- throw err;
18106
- }
18403
+ return loadCloudBackend(pkg, async () => (await import(pkg)).daytonaBackend);
18107
18404
  }
18108
18405
  if (name === "hetzner") {
18109
18406
  const pkg = "@agentbox/sandbox-hetzner";
18110
- try {
18111
- const mod = await import(pkg);
18112
- return mod.hetznerBackend;
18113
- } catch (err) {
18114
- const msg = err instanceof Error ? err.message : String(err);
18115
- if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18116
- throw new Error(
18117
- `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18118
- );
18119
- }
18120
- throw err;
18121
- }
18407
+ return loadCloudBackend(pkg, async () => (await import(pkg)).hetznerBackend);
18408
+ }
18409
+ if (name === "vercel") {
18410
+ const pkg = "@agentbox/sandbox-vercel";
18411
+ return loadCloudBackend(pkg, async () => (await import(pkg)).vercelBackend);
18122
18412
  }
18123
18413
  throw new Error(`no host executor for cloud backend '${name}'`);
18124
18414
  }
18415
+ async function loadCloudBackend(pkg, load2) {
18416
+ try {
18417
+ return await load2();
18418
+ } catch (err) {
18419
+ const msg = err instanceof Error ? err.message : String(err);
18420
+ if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18421
+ throw new Error(
18422
+ `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18423
+ );
18424
+ }
18425
+ throw err;
18426
+ }
18427
+ }
18428
+ async function refreshCloudPreviewUrl(backendName, boxId, port) {
18429
+ try {
18430
+ const backend = await resolveCloudBackend(backendName);
18431
+ if (!backend.refreshPreviewUrl) return null;
18432
+ const lookup = await lookupCloudBox(boxId);
18433
+ const handle = { sandboxId: lookup.cloudSandboxId };
18434
+ const url = await backend.refreshPreviewUrl(handle, port);
18435
+ return url.url;
18436
+ } catch {
18437
+ return null;
18438
+ }
18439
+ }
18125
18440
  async function executeCloudAction(action, deps) {
18126
18441
  const log = deps.log ?? (() => {
18127
18442
  });
@@ -18141,6 +18456,17 @@ async function executeCloudAction(action, deps) {
18141
18456
  if (action.method === "browser.open.mirror") {
18142
18457
  return runBrowserOpenMirror(action, deps);
18143
18458
  }
18459
+ if (action.method.startsWith("gh.pr.")) {
18460
+ return runGhPrRpc(action, deps);
18461
+ }
18462
+ if (action.method === "git.clone" || action.method === "gh.repo.clone") {
18463
+ return {
18464
+ exitCode: 64,
18465
+ stdout: "",
18466
+ stderr: `${action.method}: not yet implemented (deferred; see docs/plans/gh-and-git-shims-host-only.md). Run \`gh\` / \`git\` on the host directly for now.
18467
+ `
18468
+ };
18469
+ }
18144
18470
  return {
18145
18471
  exitCode: 1,
18146
18472
  stdout: "",
@@ -18148,6 +18474,99 @@ async function executeCloudAction(action, deps) {
18148
18474
  `
18149
18475
  };
18150
18476
  }
18477
+ async function runGhPrRpc(action, deps) {
18478
+ const op = action.method.slice("gh.pr.".length);
18479
+ if (!isGhPrOp(op)) {
18480
+ return {
18481
+ exitCode: 64,
18482
+ stdout: "",
18483
+ stderr: `unknown gh.pr.* op: ${op}
18484
+ `
18485
+ };
18486
+ }
18487
+ const mergeBypass = refuseMergeBypass(op);
18488
+ if (mergeBypass) return mergeBypass;
18489
+ const checkoutOptIn = refuseCheckoutByDefault(op);
18490
+ if (checkoutOptIn) return checkoutOptIn;
18491
+ const params = action.params ?? {};
18492
+ const args = Array.isArray(params.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
18493
+ const ghReady = await assertGhReady();
18494
+ if (ghReady) return ghReady;
18495
+ const lookup = await lookupCloudBox(deps.boxId);
18496
+ if (op === "checkout") {
18497
+ const guard = await checkoutGuards(lookup.workspacePath, []);
18498
+ if (guard) return guard;
18499
+ }
18500
+ const tokenClaimedGhCloud = typeof params.hostInitiated === "string";
18501
+ const incomingHashGhCloud = hashRpcParams(params);
18502
+ const hostInitiatedGhOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && (deps.hostInitiatedTokens?.consume(
18503
+ params.hostInitiated,
18504
+ deps.boxId,
18505
+ `gh.pr.${op}`,
18506
+ incomingHashGhCloud
18507
+ ) ?? false);
18508
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && !hostInitiatedGhOk) {
18509
+ return {
18510
+ exitCode: 10,
18511
+ stdout: "",
18512
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
18513
+ };
18514
+ }
18515
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedGhOk && deps.prompts && deps.subscribers) {
18516
+ const detail = args.join(" ").slice(0, 200);
18517
+ const ctx = {
18518
+ kind: "confirm",
18519
+ message: `Allow gh pr ${op} from cloud box ${deps.boxName ?? deps.boxId}?`,
18520
+ detail,
18521
+ defaultAnswer: "n",
18522
+ context: {
18523
+ command: `gh pr ${op}`,
18524
+ cwd: params.path,
18525
+ argv: args
18526
+ }
18527
+ };
18528
+ const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
18529
+ if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
18530
+ const noSubMode = (process.env["AGENTBOX_GH_NO_SUB"] ?? "deny").toLowerCase();
18531
+ if (noSubMode === "deny") {
18532
+ return {
18533
+ exitCode: 10,
18534
+ stdout: "",
18535
+ stderr: "denied automatically \u2014 no attached wrapper to confirm. Attach `agentbox claude` (or similar) and retry, or set AGENTBOX_GH_NO_SUB=allow.\n"
18536
+ };
18537
+ }
18538
+ if (noSubMode === "allow") {
18539
+ deps.log?.(`gh.pr.${op} auto-approved (no subscribers, AGENTBOX_GH_NO_SUB=allow)`);
18540
+ } else {
18541
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx, {
18542
+ ttlMs: 5 * 60 * 1e3
18543
+ });
18544
+ if (verdict.answer !== "y") {
18545
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
18546
+ }
18547
+ }
18548
+ } else {
18549
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx);
18550
+ if (verdict.answer !== "y") {
18551
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
18552
+ }
18553
+ }
18554
+ }
18555
+ let finalArgs = args;
18556
+ if (op === "create" && !args.some((a2) => a2 === "--head" || a2.startsWith("--head="))) {
18557
+ const backend = await resolveCloudBackend(deps.backendName);
18558
+ const handle = { sandboxId: lookup.cloudSandboxId };
18559
+ const containerPath = params.path ?? "/workspace";
18560
+ const branchProbe = await backend.exec(
18561
+ handle,
18562
+ `git -C ${shellQuote(containerPath)} rev-parse --abbrev-ref HEAD`
18563
+ );
18564
+ const branch = branchProbe.exitCode === 0 ? (branchProbe.stdout ?? "").trim() : "";
18565
+ finalArgs = injectPrCreateHead(op, branch, args);
18566
+ }
18567
+ if (prCreateNeedsHead(op, finalArgs)) return PR_CREATE_NO_HEAD_REFUSAL;
18568
+ return runHostGh(["pr", op, ...finalArgs], lookup.workspacePath);
18569
+ }
18151
18570
  async function runBrowserOpenMirror(action, deps) {
18152
18571
  const params = action.params ?? {};
18153
18572
  const url = typeof params.url === "string" ? params.url.trim() : "";
@@ -18174,8 +18593,8 @@ async function runBrowserOpenMirror(action, deps) {
18174
18593
  { ttlMs: TTL_MS }
18175
18594
  );
18176
18595
  if (verdict.answer === "y" && !verdict.cancelled) {
18177
- const { spawn: spawn4 } = await import("child_process");
18178
- const child = spawn4("open", [url], { stdio: "ignore", detached: true });
18596
+ const { spawn: spawn6 } = await import("child_process");
18597
+ const child = spawn6("open", [url], { stdio: "ignore", detached: true });
18179
18598
  child.unref();
18180
18599
  }
18181
18600
  } catch (err) {
@@ -18346,7 +18765,23 @@ async function runGitRpc(action, deps) {
18346
18765
  stderr: `failed to resolve branch in sandbox ${containerPath}: ${branchProbe.stderr || branch}`
18347
18766
  };
18348
18767
  }
18349
- if (action.method === "git.push" && deps.prompts && deps.subscribers) {
18768
+ const isAgentboxBranch = branch.startsWith("agentbox/");
18769
+ const tokenClaimedGit = typeof params.hostInitiated === "string";
18770
+ const incomingHashGit = hashRpcParams(params);
18771
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimedGit && (deps.hostInitiatedTokens?.consume(
18772
+ params.hostInitiated,
18773
+ deps.boxId,
18774
+ "git.push",
18775
+ incomingHashGit
18776
+ ) ?? false);
18777
+ if (action.method === "git.push" && !isAgentboxBranch && tokenClaimedGit && !hostInitiatedOk) {
18778
+ return {
18779
+ exitCode: 10,
18780
+ stdout: "",
18781
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
18782
+ };
18783
+ }
18784
+ if (action.method === "git.push" && !isAgentboxBranch && !hostInitiatedOk && deps.prompts && deps.subscribers) {
18350
18785
  const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
18351
18786
  if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
18352
18787
  const noSubMode = (process.env["AGENTBOX_GIT_PUSH_NO_SUB"] ?? "deny").toLowerCase();
@@ -18421,10 +18856,45 @@ async function runGitRpc(action, deps) {
18421
18856
  for (const a2 of params.args) if (typeof a2 === "string") argv.push(a2);
18422
18857
  }
18423
18858
  const push = await execa("git", argv, { reject: false });
18859
+ let pushStderr = push.stderr ?? "";
18860
+ if ((push.exitCode ?? 1) === 0 && !branch.startsWith("agentbox/")) {
18861
+ try {
18862
+ const sha = await execa(
18863
+ "git",
18864
+ ["-C", lookup.workspacePath, "rev-parse", branch],
18865
+ { reject: false }
18866
+ );
18867
+ const shaText = (sha.stdout ?? "").trim();
18868
+ if (sha.exitCode === 0 && shaText.length > 0) {
18869
+ const updateRef = await backend.exec(
18870
+ handle,
18871
+ `git -C ${shellQuote(containerPath)} update-ref refs/remotes/${remote2}/${branch} ${shellQuote(shaText)}`
18872
+ );
18873
+ if (updateRef.exitCode !== 0) {
18874
+ pushStderr += `
18875
+ relay: post-push in-box update-ref refs/remotes/${remote2}/${branch} failed: ${updateRef.stderr || updateRef.stdout}`;
18876
+ }
18877
+ const setUpstream = await backend.exec(
18878
+ handle,
18879
+ `git -C ${shellQuote(containerPath)} branch --set-upstream-to=${remote2}/${branch} ${shellQuote(branch)}`
18880
+ );
18881
+ if (setUpstream.exitCode !== 0) {
18882
+ pushStderr += `
18883
+ relay: post-push in-box --set-upstream-to=${remote2}/${branch} failed: ${setUpstream.stderr || setUpstream.stdout}`;
18884
+ }
18885
+ } else {
18886
+ pushStderr += `
18887
+ relay: post-push rev-parse ${branch} failed on host; skipping in-box origin/upstream sync`;
18888
+ }
18889
+ } catch (err) {
18890
+ pushStderr += `
18891
+ relay: post-push in-box origin/upstream sync threw: ${err instanceof Error ? err.message : String(err)}`;
18892
+ }
18893
+ }
18424
18894
  return {
18425
18895
  exitCode: push.exitCode ?? 1,
18426
18896
  stdout: push.stdout ?? "",
18427
- stderr: push.stderr ?? ""
18897
+ stderr: pushStderr
18428
18898
  };
18429
18899
  }
18430
18900
  const remote = params.remote ?? "origin";
@@ -18471,7 +18941,7 @@ function shellQuote(arg) {
18471
18941
  }
18472
18942
 
18473
18943
  // src/host-action-queue.ts
18474
- var import_node_crypto2 = require("crypto");
18944
+ var import_node_crypto3 = require("crypto");
18475
18945
  var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
18476
18946
  var HostActionQueue = class {
18477
18947
  map = /* @__PURE__ */ new Map();
@@ -18489,7 +18959,7 @@ var HostActionQueue = class {
18489
18959
  * is open" semantics).
18490
18960
  */
18491
18961
  enqueue(boxId, method, params) {
18492
- const id = (0, import_node_crypto2.randomUUID)();
18962
+ const id = (0, import_node_crypto3.randomUUID)();
18493
18963
  const action = {
18494
18964
  id,
18495
18965
  boxId,
@@ -18555,7 +19025,7 @@ var HostActionQueue = class {
18555
19025
  };
18556
19026
 
18557
19027
  // src/notices.ts
18558
- var import_node_crypto3 = require("crypto");
19028
+ var import_node_crypto4 = require("crypto");
18559
19029
  var DEFAULT_NOTICE_TTL_MS = 66e4;
18560
19030
  var BoxNotices = class {
18561
19031
  constructor(subscribers) {
@@ -18578,7 +19048,7 @@ var BoxNotices = class {
18578
19048
  this.entries.delete(id);
18579
19049
  }
18580
19050
  }
18581
- const ev = { id: (0, import_node_crypto3.randomUUID)(), kind, message };
19051
+ const ev = { id: (0, import_node_crypto4.randomUUID)(), kind, message };
18582
19052
  const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;
18583
19053
  const timer = setTimeout(() => {
18584
19054
  if (this.entries.delete(ev.id)) {
@@ -18796,6 +19266,8 @@ function createRelayServer(opts) {
18796
19266
  const prompts = new PendingPrompts();
18797
19267
  const subscribers = new PromptSubscribers();
18798
19268
  const notices = new BoxNotices(subscribers);
19269
+ const hostInitiatedTokens = new HostInitiatedTokens();
19270
+ let queuePoke = null;
18799
19271
  const host = opts.host ?? "0.0.0.0";
18800
19272
  const mode = opts.mode ?? "host";
18801
19273
  const hostActions = mode === "box" ? new HostActionQueue() : null;
@@ -18823,7 +19295,13 @@ function createRelayServer(opts) {
18823
19295
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "relay"}`);
18824
19296
  const route = `${req.method ?? "GET"} ${url.pathname}`;
18825
19297
  if (route === "GET /healthz") {
18826
- send(res, 200, { ok: true, boxes: registry.size(), events: events.size() });
19298
+ send(res, 200, {
19299
+ ok: true,
19300
+ boxes: registry.size(),
19301
+ events: events.size(),
19302
+ pid: process.pid,
19303
+ cliEntry: Boolean(process.env.AGENTBOX_CLI_ENTRY)
19304
+ });
18827
19305
  return;
18828
19306
  }
18829
19307
  if (url.pathname.startsWith("/bridge/")) {
@@ -18928,21 +19406,36 @@ function createRelayServer(opts) {
18928
19406
  if (body.method === "git.push" || body.method === "git.fetch") {
18929
19407
  if (body.method === "git.push") {
18930
19408
  const params = body.params;
18931
- const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
18932
- kind: "confirm",
18933
- message: `Allow git push from box ${reg.name}?`,
18934
- detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
18935
- defaultAnswer: "n",
18936
- context: {
18937
- command: "git push",
18938
- cwd: params?.path,
18939
- argv: params?.args
18940
- }
18941
- });
18942
- if (verdict.answer !== "y") {
18943
- send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
19409
+ const worktree = resolveWorktree(reg, params?.path ?? "/workspace");
19410
+ const isAgentboxBranch = worktree?.branch.startsWith("agentbox/") ?? false;
19411
+ const tokenClaimed = typeof params?.hostInitiated === "string";
19412
+ const incomingHash = hashRpcParams(params);
19413
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimed && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, "git.push", incomingHash);
19414
+ if (!isAgentboxBranch && tokenClaimed && !hostInitiatedOk) {
19415
+ send(res, 500, {
19416
+ exitCode: 10,
19417
+ stdout: "",
19418
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19419
+ });
18944
19420
  return;
18945
19421
  }
19422
+ if (!isAgentboxBranch && !hostInitiatedOk) {
19423
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
19424
+ kind: "confirm",
19425
+ message: `Allow git push from box ${reg.name}?`,
19426
+ detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
19427
+ defaultAnswer: "n",
19428
+ context: {
19429
+ command: "git push",
19430
+ cwd: params?.path,
19431
+ argv: params?.args
19432
+ }
19433
+ });
19434
+ if (verdict.answer !== "y") {
19435
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
19436
+ return;
19437
+ }
19438
+ }
18946
19439
  }
18947
19440
  const result = await handleGitRpc(reg, body.method, body.params);
18948
19441
  const status = result.exitCode === 0 ? 200 : 500;
@@ -18975,6 +19468,33 @@ function createRelayServer(opts) {
18975
19468
  send(res, status, result);
18976
19469
  return;
18977
19470
  }
19471
+ if (body.method.startsWith("gh.pr.")) {
19472
+ const op = body.method.slice("gh.pr.".length);
19473
+ if (!isGhPrOp(op)) {
19474
+ send(res, 400, { error: `unknown gh.pr.* op: ${op}` });
19475
+ return;
19476
+ }
19477
+ const result = await handleGhPrRpc(
19478
+ op,
19479
+ reg,
19480
+ body.params,
19481
+ prompts,
19482
+ subscribers,
19483
+ hostInitiatedTokens
19484
+ );
19485
+ const status = result.exitCode === 0 ? 200 : 500;
19486
+ send(res, status, result);
19487
+ return;
19488
+ }
19489
+ if (body.method === "git.clone" || body.method === "gh.repo.clone") {
19490
+ send(res, 501, {
19491
+ exitCode: 64,
19492
+ stdout: "",
19493
+ stderr: `${body.method}: not yet implemented (deferred; see docs/plans/gh-and-git-shims-host-only.md). Run \`gh\` / \`git\` on the host directly for now.
19494
+ `
19495
+ });
19496
+ return;
19497
+ }
18978
19498
  if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
18979
19499
  const params = body.params;
18980
19500
  const kind = body.method.split(".")[1] ?? "workspace";
@@ -19111,6 +19631,7 @@ function createRelayServer(opts) {
19111
19631
  boxName: reg.name,
19112
19632
  prompts,
19113
19633
  subscribers,
19634
+ hostInitiatedTokens,
19114
19635
  log
19115
19636
  });
19116
19637
  await respond(result);
@@ -19123,6 +19644,11 @@ function createRelayServer(opts) {
19123
19644
  });
19124
19645
  }
19125
19646
  } : void 0,
19647
+ // Self-heal a dead preview transport (hetzner SSH `-L` after a
19648
+ // ControlMaster death). The relay strips the `cloud:` prefix
19649
+ // the cloud-provider tags onto BoxRecord.container — what the
19650
+ // backend's `get(sandboxId)` expects is the bare sandbox id.
19651
+ recoverPreviewUrl: reg.backend ? async () => refreshCloudPreviewUrl(reg.backend, reg.boxId, DEFAULT_BOX_RELAY_PORT) : void 0,
19126
19652
  logger: log
19127
19653
  });
19128
19654
  } catch (err) {
@@ -19237,6 +19763,27 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19237
19763
  send(res, 204, null);
19238
19764
  return;
19239
19765
  }
19766
+ if (route === "POST /admin/host-initiated/mint") {
19767
+ const body = await readJsonBody(req);
19768
+ if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.method !== "string" || body.method.length === 0) {
19769
+ send(res, 400, { error: "expected {boxId, method, paramsHash, ttlMs?}" });
19770
+ return;
19771
+ }
19772
+ let paramsHash;
19773
+ if (body.paramsHash === null || body.paramsHash === void 0) {
19774
+ paramsHash = null;
19775
+ } else if (typeof body.paramsHash === "string" && /^[0-9a-f]{64}$/.test(body.paramsHash)) {
19776
+ paramsHash = body.paramsHash;
19777
+ } else {
19778
+ send(res, 400, { error: "paramsHash must be a 64-hex sha256 string or null" });
19779
+ return;
19780
+ }
19781
+ const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
19782
+ const token = hostInitiatedTokens.mint(body.boxId, body.method, paramsHash, ttlMs);
19783
+ log(`host-initiated-mint box=${body.boxId} method=${body.method} paramsBound=${paramsHash !== null}`);
19784
+ send(res, 200, { token });
19785
+ return;
19786
+ }
19240
19787
  if (route === "POST /admin/notices/set") {
19241
19788
  const body = await readJsonBody(req);
19242
19789
  if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.kind !== "string" || body.kind.length === 0 || typeof body.message !== "string" || body.message.length === 0) {
@@ -19249,6 +19796,17 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19249
19796
  send(res, 200, { id });
19250
19797
  return;
19251
19798
  }
19799
+ if (route === "POST /admin/queue/enqueue") {
19800
+ const body = await readJsonBody(req);
19801
+ if (!body || typeof body.id !== "string" || body.id.length === 0) {
19802
+ send(res, 400, { error: "expected {id}" });
19803
+ return;
19804
+ }
19805
+ log(`queue-enqueue id=${body.id}`);
19806
+ queuePoke?.();
19807
+ send(res, 204, null);
19808
+ return;
19809
+ }
19252
19810
  if (route === "POST /admin/notices/clear") {
19253
19811
  const body = await readJsonBody(req);
19254
19812
  if (!body || typeof body.id !== "string" || body.id.length === 0) {
@@ -19285,6 +19843,9 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19285
19843
  notices,
19286
19844
  hostActions: hostActions ?? void 0,
19287
19845
  url: `http://${host}:${String(opts.port)}`,
19846
+ setQueuePoke: (fn) => {
19847
+ queuePoke = fn;
19848
+ },
19288
19849
  close: async () => {
19289
19850
  if (pollers) await pollers.stopAll();
19290
19851
  await new Promise((resolve2, reject) => {
@@ -19336,7 +19897,71 @@ async function handleGitRpc(reg, method, params) {
19336
19897
  if (typeof a2 === "string") argv.push(a2);
19337
19898
  }
19338
19899
  }
19339
- return runHostCommand(argv);
19900
+ const result = await runHostCommand(argv);
19901
+ if (method === "git.push" && result.exitCode === 0 && !worktree.branch.startsWith("agentbox/")) {
19902
+ await runHostCommand([
19903
+ "git",
19904
+ "-C",
19905
+ worktree.hostMainRepo,
19906
+ "branch",
19907
+ `--set-upstream-to=${remote}/${worktree.branch}`,
19908
+ worktree.branch
19909
+ ]);
19910
+ }
19911
+ return result;
19912
+ }
19913
+ async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiatedTokens) {
19914
+ const mergeBypass = refuseMergeBypass(op);
19915
+ if (mergeBypass) return mergeBypass;
19916
+ const checkoutOptIn = refuseCheckoutByDefault(op);
19917
+ if (checkoutOptIn) return checkoutOptIn;
19918
+ const containerPath = params?.path ?? "/workspace";
19919
+ const worktree = resolveWorktree(reg, containerPath);
19920
+ if (!worktree) {
19921
+ return {
19922
+ exitCode: 64,
19923
+ stdout: "",
19924
+ stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
19925
+ };
19926
+ }
19927
+ const ghReady = await assertGhReady();
19928
+ if (ghReady) return ghReady;
19929
+ const args = Array.isArray(params?.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
19930
+ if (op === "checkout") {
19931
+ const branches = (reg.worktrees ?? []).map((w) => w.branch);
19932
+ const guard = await checkoutGuards(worktree.hostMainRepo, branches);
19933
+ if (guard) return guard;
19934
+ }
19935
+ const tokenClaimedGh = typeof params?.hostInitiated === "string";
19936
+ const incomingHashGh = hashRpcParams(params);
19937
+ const hostInitiatedOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, `gh.pr.${op}`, incomingHashGh);
19938
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && !hostInitiatedOk) {
19939
+ return {
19940
+ exitCode: 10,
19941
+ stdout: "",
19942
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19943
+ };
19944
+ }
19945
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedOk) {
19946
+ const detail = args.join(" ").slice(0, 200);
19947
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
19948
+ kind: "confirm",
19949
+ message: `Allow gh pr ${op} from box ${reg.name}?`,
19950
+ detail,
19951
+ defaultAnswer: "n",
19952
+ context: {
19953
+ command: `gh pr ${op}`,
19954
+ cwd: containerPath,
19955
+ argv: args
19956
+ }
19957
+ });
19958
+ if (verdict.answer !== "y") {
19959
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19960
+ }
19961
+ }
19962
+ const finalArgs = injectPrCreateHead(op, worktree.branch, args);
19963
+ if (prCreateNeedsHead(op, finalArgs)) return PR_CREATE_NO_HEAD_REFUSAL;
19964
+ return runHostGh(["pr", op, ...finalArgs], worktree.hostMainRepo);
19340
19965
  }
19341
19966
  async function handleCpRpc(reg, method, params) {
19342
19967
  const entry = process.env.AGENTBOX_CLI_ENTRY;
@@ -19397,7 +20022,7 @@ function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
19397
20022
  resolve2({ exitCode: 64, stdout: "", stderr: "empty command" });
19398
20023
  return;
19399
20024
  }
19400
- const child = (0, import_node_child_process6.spawn)(cmd, rest, {
20025
+ const child = (0, import_node_child_process7.spawn)(cmd, rest, {
19401
20026
  env: process.env,
19402
20027
  stdio: ["ignore", "pipe", "pipe"]
19403
20028
  });
@@ -19446,7 +20071,7 @@ async function startRelayServer(opts) {
19446
20071
  }
19447
20072
 
19448
20073
  // src/autopause.ts
19449
- var import_node_child_process7 = require("child_process");
20074
+ var import_node_child_process8 = require("child_process");
19450
20075
  var import_promises16 = require("fs/promises");
19451
20076
 
19452
20077
  // ../config/dist/index.js
@@ -19464,6 +20089,7 @@ var BUILT_IN_DEFAULTS = {
19464
20089
  defaultCheckpointDocker: "",
19465
20090
  defaultCheckpointDaytona: "",
19466
20091
  defaultCheckpointHetzner: "",
20092
+ defaultCheckpointVercel: "",
19467
20093
  withPlaywright: false,
19468
20094
  withEnv: false,
19469
20095
  vnc: true,
@@ -19475,7 +20101,11 @@ var BUILT_IN_DEFAULTS = {
19475
20101
  memory: 0,
19476
20102
  cpus: 0,
19477
20103
  pidsLimit: 0,
19478
- disk: ""
20104
+ disk: "",
20105
+ bundleDepth: void 0,
20106
+ vercelVcpus: 2,
20107
+ vercelTimeoutMs: 27e5,
20108
+ vercelNetworkPolicy: ""
19479
20109
  },
19480
20110
  checkpoint: {
19481
20111
  maxLayers: 3
@@ -19524,6 +20154,15 @@ var BUILT_IN_DEFAULTS = {
19524
20154
  maxRunningBoxes: 5,
19525
20155
  idleMinutes: 5
19526
20156
  },
20157
+ queue: {
20158
+ enabled: true,
20159
+ maxConcurrent: 5,
20160
+ maxWorking: 0,
20161
+ idleGraceSeconds: 15
20162
+ },
20163
+ cloud: {
20164
+ useCurrentBranch: false
20165
+ },
19527
20166
  maintenance: {
19528
20167
  pruneProjectConfigs: true,
19529
20168
  pruneProjectConfigsEvery: 50
@@ -19533,8 +20172,8 @@ var KEY_REGISTRY = [
19533
20172
  {
19534
20173
  key: "box.provider",
19535
20174
  type: "enum",
19536
- enumValues: ["docker", "daytona", "hetzner"],
19537
- description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, or Hetzner Cloud VPSes."
20175
+ enumValues: ["docker", "daytona", "hetzner", "vercel"],
20176
+ description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or Vercel Sandboxes."
19538
20177
  },
19539
20178
  {
19540
20179
  key: "box.hostSnapshot",
@@ -19564,6 +20203,12 @@ var KEY_REGISTRY = [
19564
20203
  description: "Per-provider override of `box.defaultCheckpoint` for hetzner. Wins over the global when set; set via `agentbox checkpoint set-default --provider hetzner`.",
19565
20204
  advanced: true
19566
20205
  },
20206
+ {
20207
+ key: "box.defaultCheckpointVercel",
20208
+ type: "string",
20209
+ description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
20210
+ advanced: true
20211
+ },
19567
20212
  {
19568
20213
  key: "checkpoint.maxLayers",
19569
20214
  type: "int",
@@ -19632,6 +20277,26 @@ var KEY_REGISTRY = [
19632
20277
  description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
19633
20278
  advanced: true
19634
20279
  },
20280
+ {
20281
+ key: "box.bundleDepth",
20282
+ type: "int",
20283
+ description: "Cap git bundle history shipped to cloud sandboxes (daytona, hetzner). 0 = full history. Unset = adaptive default (last 200 commits; re-bundle at 100 if the bundle exceeds 20 MB). Ignored for docker (which bind-mounts .git/)."
20284
+ },
20285
+ {
20286
+ key: "box.vercelVcpus",
20287
+ type: "int",
20288
+ description: "vCPUs for new --provider vercel boxes (Vercel couples RAM at 2048 MB/vCPU). Default 2. Vercel only accepts specific counts (e.g. 1, 2, 4, 8) \u2014 an unsupported value fails create with a 400. Vercel-only; ignored by other providers."
20289
+ },
20290
+ {
20291
+ key: "box.vercelTimeoutMs",
20292
+ type: "int",
20293
+ description: "Max session length (ms) for new --provider vercel boxes before the VM auto-snapshots; persistent mode auto-resumes on the next call. Default 2700000 (45 min, the Hobby ceiling). Vercel-only."
20294
+ },
20295
+ {
20296
+ key: "box.vercelNetworkPolicy",
20297
+ type: "string",
20298
+ description: "Egress lock for new --provider vercel boxes: 'allow-all' (default, unset), 'deny-all', or a comma-separated domain allowlist (e.g. 'github.com,*.npmjs.org') that denies everything else. Vercel-only; ignored by other providers."
20299
+ },
19635
20300
  {
19636
20301
  key: "claude.sessionName",
19637
20302
  type: "string",
@@ -19739,6 +20404,31 @@ var KEY_REGISTRY = [
19739
20404
  type: "int",
19740
20405
  description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
19741
20406
  },
20407
+ {
20408
+ key: "queue.enabled",
20409
+ type: "bool",
20410
+ description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
20411
+ },
20412
+ {
20413
+ key: "queue.maxConcurrent",
20414
+ type: "int",
20415
+ description: "Max number of simultaneously-running boxes (across providers) before background `-i` jobs queue up instead of starting immediately. Per-invocation override: `--max-running <n>`."
20416
+ },
20417
+ {
20418
+ key: "queue.maxWorking",
20419
+ type: "int",
20420
+ description: "Max agents actively working/thinking (quota-consuming) at once before background `-i` jobs queue. 0 = disabled (use the queue.maxConcurrent running-box gate). Counts all boxes, foreground + queued. Per-invocation override: `--max-working <n>`."
20421
+ },
20422
+ {
20423
+ key: "queue.idleGraceSeconds",
20424
+ type: "int",
20425
+ description: "Seconds an agent must stay non-working before it frees its working slot (debounce against brief idle flaps between turns). Only used when queue.maxWorking > 0."
20426
+ },
20427
+ {
20428
+ key: "cloud.useCurrentBranch",
20429
+ type: "bool",
20430
+ description: "On cloud providers (daytona/hetzner), start new boxes on the host's current branch instead of forking a new agentbox/<box-name> branch. Overridden by an explicit --use-branch / --from-branch."
20431
+ },
19742
20432
  {
19743
20433
  key: "maintenance.pruneProjectConfigs",
19744
20434
  type: "bool",
@@ -19833,6 +20523,15 @@ function parseUserConfigObject(doc, where) {
19833
20523
  }
19834
20524
  const out = {};
19835
20525
  for (const [branchName, branchRaw] of Object.entries(doc)) {
20526
+ if (branchName === "schema") {
20527
+ if (branchRaw !== void 0 && branchRaw !== null) {
20528
+ if (typeof branchRaw !== "number" || !Number.isInteger(branchRaw)) {
20529
+ throw new UserConfigError(`${where}.schema: must be an integer (got ${String(branchRaw)})`);
20530
+ }
20531
+ out.schema = branchRaw;
20532
+ }
20533
+ continue;
20534
+ }
19836
20535
  const branchSpec = BRANCHES.get(branchName);
19837
20536
  if (!branchSpec) {
19838
20537
  throw new UserConfigError(
@@ -20005,7 +20704,7 @@ var INSPECT_TIMEOUT_MS = 15e3;
20005
20704
  var PAUSE_TIMEOUT_MS = 3e4;
20006
20705
  function runDocker(args, timeoutMs) {
20007
20706
  return new Promise((resolve2) => {
20008
- const child = (0, import_node_child_process7.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
20707
+ const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
20009
20708
  let stdout = "";
20010
20709
  let stderr = "";
20011
20710
  let settled = false;
@@ -20063,6 +20762,381 @@ async function pauseContainer(name) {
20063
20762
  }
20064
20763
  }
20065
20764
 
20765
+ // src/queue.ts
20766
+ var import_node_child_process9 = require("child_process");
20767
+ var import_promises17 = require("fs/promises");
20768
+ var import_node_fs6 = require("fs");
20769
+ var import_node_path8 = require("path");
20770
+ var import_promises18 = require("timers/promises");
20771
+ var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
20772
+ async function loadQueueConfig() {
20773
+ const d = BUILT_IN_DEFAULTS.queue;
20774
+ let global3 = {};
20775
+ try {
20776
+ global3 = parseUserConfig(await (0, import_promises17.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
20777
+ } catch {
20778
+ }
20779
+ const q = global3.queue ?? {};
20780
+ return {
20781
+ enabled: q.enabled ?? d.enabled,
20782
+ maxConcurrent: q.maxConcurrent ?? d.maxConcurrent,
20783
+ maxWorking: q.maxWorking ?? d.maxWorking,
20784
+ idleGraceMs: (q.idleGraceSeconds ?? d.idleGraceSeconds) * 1e3
20785
+ };
20786
+ }
20787
+ async function writeJob(job) {
20788
+ await (0, import_promises17.mkdir)(QUEUE_DIR, { recursive: true });
20789
+ const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
20790
+ const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
20791
+ await (0, import_promises17.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
20792
+ await (0, import_promises17.rename)(tmp, final);
20793
+ }
20794
+ async function readJob(id) {
20795
+ try {
20796
+ const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
20797
+ return JSON.parse(raw);
20798
+ } catch (err) {
20799
+ if (err.code === "ENOENT") return null;
20800
+ throw err;
20801
+ }
20802
+ }
20803
+ async function loadQueue() {
20804
+ let entries;
20805
+ try {
20806
+ entries = await (0, import_promises17.readdir)(QUEUE_DIR);
20807
+ } catch (err) {
20808
+ if (err.code === "ENOENT") return [];
20809
+ throw err;
20810
+ }
20811
+ const out = [];
20812
+ for (const name of entries) {
20813
+ if (!name.endsWith(".json")) continue;
20814
+ try {
20815
+ const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
20816
+ out.push(JSON.parse(raw));
20817
+ } catch {
20818
+ }
20819
+ }
20820
+ out.sort((a2, b) => a2.createdAt < b.createdAt ? -1 : a2.createdAt > b.createdAt ? 1 : 0);
20821
+ return out;
20822
+ }
20823
+ function selectNextRunnable(jobs, runningCount) {
20824
+ for (const j of jobs) {
20825
+ if (j.status !== "queued") continue;
20826
+ if (runningCount < j.maxConcurrent) return j;
20827
+ }
20828
+ return null;
20829
+ }
20830
+ var WORKING_AGENT_STATES = [
20831
+ "working",
20832
+ "idle",
20833
+ "waiting",
20834
+ "end-plan",
20835
+ "question",
20836
+ "compacting",
20837
+ "error",
20838
+ "unknown"
20839
+ ];
20840
+ function isWorkingAgentState(v) {
20841
+ return typeof v === "string" && WORKING_AGENT_STATES.includes(v);
20842
+ }
20843
+ var STARTUP_GRACE_MS = 9e4;
20844
+ function occupiesWorkingSlot(e, idleGraceMs) {
20845
+ if (e.agentState === "working" || e.agentState === "compacting") return true;
20846
+ if ((e.agentState === null || e.agentState === "unknown") && e.sinceCreateMs !== null && e.sinceCreateMs < STARTUP_GRACE_MS) {
20847
+ return true;
20848
+ }
20849
+ if ((e.agentState === "idle" || e.agentState === "waiting" || e.agentState === "end-plan" || e.agentState === "question") && e.sinceUpdateMs !== null && e.sinceUpdateMs < idleGraceMs) {
20850
+ return true;
20851
+ }
20852
+ return false;
20853
+ }
20854
+ function countWorkingSlots(entries, idleGraceMs) {
20855
+ return entries.reduce((n2, e) => occupiesWorkingSlot(e, idleGraceMs) ? n2 + 1 : n2, 0);
20856
+ }
20857
+ function selectNextRunnableByWorking(jobs, workingCount, globalMaxWorking) {
20858
+ for (const j of jobs) {
20859
+ if (j.status !== "queued") continue;
20860
+ const ceil = typeof j.maxWorking === "number" && j.maxWorking > 0 ? j.maxWorking : globalMaxWorking;
20861
+ if (workingCount < ceil) return j;
20862
+ }
20863
+ return null;
20864
+ }
20865
+ function readActiveAgent(snap) {
20866
+ if (!snap || typeof snap !== "object") return { state: null, updatedAt: null };
20867
+ const candidates = [];
20868
+ for (const key of ["claude", "codex", "opencode"]) {
20869
+ const sub = snap[key];
20870
+ if (!sub || typeof sub !== "object") continue;
20871
+ const o2 = sub;
20872
+ if (!isWorkingAgentState(o2.state)) continue;
20873
+ candidates.push({
20874
+ state: o2.state,
20875
+ updatedAt: typeof o2.updatedAt === "string" ? o2.updatedAt : null
20876
+ });
20877
+ }
20878
+ if (candidates.length === 0) return { state: null, updatedAt: null };
20879
+ const active = candidates.find((c3) => c3.state === "working" || c3.state === "compacting");
20880
+ if (active) return active;
20881
+ candidates.sort((a2, b) => parseTime(b.updatedAt) - parseTime(a2.updatedAt));
20882
+ return candidates[0];
20883
+ }
20884
+ function parseTime(iso) {
20885
+ if (!iso) return 0;
20886
+ const t = Date.parse(iso);
20887
+ return Number.isNaN(t) ? 0 : t;
20888
+ }
20889
+ function msSince2(iso) {
20890
+ if (!iso) return null;
20891
+ const t = Date.parse(iso);
20892
+ return Number.isNaN(t) ? null : Date.now() - t;
20893
+ }
20894
+ var DEFAULT_INTERVAL_MS2 = 2e3;
20895
+ function startQueueLoop(deps) {
20896
+ const loadConfig = deps.loadConfig ?? loadQueueConfig;
20897
+ const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
20898
+ const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
20899
+ const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
20900
+ const { log, onStatusChange } = deps;
20901
+ const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
20902
+ let ticking = false;
20903
+ let stopped = false;
20904
+ let warnedNoWorkingDeps = false;
20905
+ let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
20906
+ log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
20907
+ });
20908
+ async function tick() {
20909
+ if (ticking) return;
20910
+ ticking = true;
20911
+ try {
20912
+ const cfg = await loadConfig();
20913
+ if (!cfg.enabled) return;
20914
+ const jobs = await loadQueue();
20915
+ const hasQueued = jobs.some((j) => j.status === "queued");
20916
+ if (!hasQueued) return;
20917
+ let gateByWorking = cfg.maxWorking > 0;
20918
+ if (gateByWorking && !countWorking) {
20919
+ gateByWorking = false;
20920
+ if (!warnedNoWorkingDeps) {
20921
+ warnedNoWorkingDeps = true;
20922
+ log("queue: maxWorking set but registry/statusStore not wired; using running-box gate");
20923
+ }
20924
+ }
20925
+ while (!stopped) {
20926
+ let occupancy;
20927
+ let next;
20928
+ if (gateByWorking && countWorking) {
20929
+ occupancy = await countWorking(cfg.idleGraceMs);
20930
+ const fresh = await loadQueue();
20931
+ next = selectNextRunnableByWorking(fresh, occupancy, cfg.maxWorking);
20932
+ } else {
20933
+ occupancy = await countRunning();
20934
+ const fresh = await loadQueue();
20935
+ next = selectNextRunnable(fresh, occupancy);
20936
+ }
20937
+ if (!next) return;
20938
+ const current = await readJob(next.id);
20939
+ if (!current || current.status !== "queued") continue;
20940
+ const updated = {
20941
+ ...current,
20942
+ status: "running",
20943
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
20944
+ };
20945
+ await writeJob(updated);
20946
+ onStatusChange?.(updated);
20947
+ try {
20948
+ const pid = await spawnWorker(updated);
20949
+ if (typeof pid === "number") {
20950
+ const withPid = { ...updated, pid };
20951
+ await writeJob(withPid);
20952
+ onStatusChange?.(withPid);
20953
+ const ceil = gateByWorking ? typeof updated.maxWorking === "number" && updated.maxWorking > 0 ? updated.maxWorking : cfg.maxWorking : updated.maxConcurrent;
20954
+ log(
20955
+ `queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; ${gateByWorking ? "working" : "running"} ${String(occupancy + 1)}/${String(ceil)}`
20956
+ );
20957
+ } else {
20958
+ log(`queue: started job ${updated.id} (${updated.agent}); pid unknown`);
20959
+ }
20960
+ } catch (err) {
20961
+ const msg = err instanceof Error ? err.message : String(err);
20962
+ const failed = {
20963
+ ...updated,
20964
+ status: "failed",
20965
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
20966
+ reason: `worker-spawn-failed: ${msg}`
20967
+ };
20968
+ await writeJob(failed);
20969
+ onStatusChange?.(failed);
20970
+ log(`queue: spawn for job ${updated.id} failed: ${msg}`);
20971
+ }
20972
+ }
20973
+ } catch (err) {
20974
+ const msg = err instanceof Error ? err.message : String(err);
20975
+ log(`queue: tick error: ${msg}`);
20976
+ } finally {
20977
+ ticking = false;
20978
+ }
20979
+ }
20980
+ function poke() {
20981
+ if (stopped) return;
20982
+ inFlight = tick();
20983
+ }
20984
+ const handle = {
20985
+ poke,
20986
+ stop: async () => {
20987
+ stopped = true;
20988
+ clearInterval(timer);
20989
+ await inFlight.catch(() => {
20990
+ });
20991
+ }
20992
+ };
20993
+ const timer = setInterval(() => {
20994
+ if (stopped) return;
20995
+ inFlight = tick();
20996
+ }, intervalMs);
20997
+ timer.unref();
20998
+ return handle;
20999
+ }
21000
+ async function recoverOrphanedWorkers(log, onChange) {
21001
+ const jobs = await loadQueue();
21002
+ for (const j of jobs) {
21003
+ if (j.status !== "running") continue;
21004
+ if (typeof j.pid === "number" && processAlive(j.pid)) continue;
21005
+ const failed = {
21006
+ ...j,
21007
+ status: "failed",
21008
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
21009
+ reason: "worker-died"
21010
+ };
21011
+ await writeJob(failed);
21012
+ onChange?.(failed);
21013
+ log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
21014
+ }
21015
+ }
21016
+ function processAlive(pid) {
21017
+ try {
21018
+ process.kill(pid, 0);
21019
+ return true;
21020
+ } catch {
21021
+ return false;
21022
+ }
21023
+ }
21024
+ async function defaultCountWorkingBoxes(registry, statusStore, idleGraceMs) {
21025
+ const boxes = registry.list();
21026
+ const registeredIds = new Set(boxes.map((b) => b.boxId));
21027
+ const entries = boxes.map((b) => {
21028
+ const active = readActiveAgent(statusStore.get(b.boxId));
21029
+ return {
21030
+ key: b.boxId,
21031
+ agentState: active.state,
21032
+ sinceUpdateMs: msSince2(active.updatedAt),
21033
+ sinceCreateMs: msSince2(b.createdAt)
21034
+ };
21035
+ });
21036
+ let count2 = countWorkingSlots(entries, idleGraceMs);
21037
+ let jobs;
21038
+ try {
21039
+ jobs = await loadQueue();
21040
+ } catch {
21041
+ return count2;
21042
+ }
21043
+ for (const j of jobs) {
21044
+ if (j.status !== "running") continue;
21045
+ if (j.boxId && registeredIds.has(j.boxId)) continue;
21046
+ if (typeof j.pid === "number" && !processAlive(j.pid)) continue;
21047
+ count2 += 1;
21048
+ }
21049
+ return count2;
21050
+ }
21051
+ var RUNNING_COUNT_CACHE_MS = 3e3;
21052
+ var runningCountCache = null;
21053
+ async function defaultCountRunningBoxes() {
21054
+ const now = Date.now();
21055
+ if (runningCountCache && runningCountCache.expiresAt > now) {
21056
+ return runningCountCache.value;
21057
+ }
21058
+ const value = await uncachedCountRunningBoxes();
21059
+ runningCountCache = { value, expiresAt: now + RUNNING_COUNT_CACHE_MS };
21060
+ return value;
21061
+ }
21062
+ async function uncachedCountRunningBoxes() {
21063
+ let boxes;
21064
+ try {
21065
+ boxes = (await readState(STATE_FILE)).boxes;
21066
+ } catch {
21067
+ return 0;
21068
+ }
21069
+ if (boxes.length === 0) return 0;
21070
+ let count2 = 0;
21071
+ const dockerBoxes = [];
21072
+ for (const b of boxes) {
21073
+ const provider = b.provider ?? "docker";
21074
+ if (provider === "docker") {
21075
+ dockerBoxes.push(b);
21076
+ } else {
21077
+ count2 += 1;
21078
+ }
21079
+ }
21080
+ if (dockerBoxes.length > 0) {
21081
+ const states = await Promise.all(dockerBoxes.map((b) => inspectDockerState(b.container)));
21082
+ for (const s of states) {
21083
+ if (s === "running") count2 += 1;
21084
+ }
21085
+ }
21086
+ return count2;
21087
+ }
21088
+ function inspectDockerState(containerName) {
21089
+ return new Promise((resolveP) => {
21090
+ const child = (0, import_node_child_process9.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
21091
+ stdio: ["ignore", "pipe", "pipe"]
21092
+ });
21093
+ let out = "";
21094
+ let settled = false;
21095
+ const finish = (state) => {
21096
+ if (settled) return;
21097
+ settled = true;
21098
+ resolveP(state);
21099
+ };
21100
+ const timer = setTimeout(() => {
21101
+ child.kill("SIGTERM");
21102
+ finish("other");
21103
+ }, 1e4);
21104
+ child.stdout?.on("data", (c3) => {
21105
+ out += c3.toString("utf8");
21106
+ });
21107
+ child.on("error", () => {
21108
+ clearTimeout(timer);
21109
+ finish("other");
21110
+ });
21111
+ child.on("close", () => {
21112
+ clearTimeout(timer);
21113
+ finish(out.trim() === "running" ? "running" : "other");
21114
+ });
21115
+ });
21116
+ }
21117
+ async function defaultSpawnWorker(job) {
21118
+ const entry = process.env.AGENTBOX_CLI_ENTRY;
21119
+ if (!entry || !(0, import_node_fs6.existsSync)(entry)) {
21120
+ throw new Error(
21121
+ `AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
21122
+ );
21123
+ }
21124
+ await (0, import_promises17.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
21125
+ const fd = (0, import_node_fs6.openSync)(job.logPath, "a");
21126
+ const child = (0, import_node_child_process9.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
21127
+ detached: true,
21128
+ stdio: ["ignore", fd, fd],
21129
+ env: process.env
21130
+ });
21131
+ child.unref();
21132
+ try {
21133
+ (0, import_node_fs6.closeSync)(fd);
21134
+ } catch {
21135
+ }
21136
+ return typeof child.pid === "number" ? child.pid : null;
21137
+ }
21138
+ var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
21139
+
20066
21140
  // src/bin.ts
20067
21141
  var program2 = new Command();
20068
21142
  program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");
@@ -20088,10 +21162,19 @@ program2.command("serve").description("Run the HTTP relay in the foreground").op
20088
21162
  log: (line) => process.stdout.write(`agentbox-relay: ${line}
20089
21163
  `)
20090
21164
  });
21165
+ const queue = startQueueLoop({
21166
+ log: (line) => process.stdout.write(`agentbox-relay: ${line}
21167
+ `),
21168
+ registry: handle.registry,
21169
+ statusStore: handle.statusStore
21170
+ });
21171
+ handle.setQueuePoke(() => {
21172
+ queue.poke?.();
21173
+ });
20091
21174
  const shutdown = (signal) => {
20092
21175
  process.stdout.write(`agentbox-relay: ${signal} \u2014 shutting down
20093
21176
  `);
20094
- autopause.stop().finally(() => handle.close()).finally(() => process.exit(0));
21177
+ Promise.allSettled([autopause.stop(), queue.stop()]).finally(() => handle.close()).finally(() => process.exit(0));
20095
21178
  };
20096
21179
  process.on("SIGTERM", () => shutdown("SIGTERM"));
20097
21180
  process.on("SIGINT", () => shutdown("SIGINT"));