@madarco/agentbox 0.7.0 → 0.8.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 (60) hide show
  1. package/dist/_cloud-attach-T727ZPRV.js +13 -0
  2. package/dist/{chunk-NW5NYTQM.js → chunk-67N47KUS.js} +359 -85
  3. package/dist/chunk-67N47KUS.js.map +1 -0
  4. package/dist/{chunk-NAVL4R34.js → chunk-6OZDFNBF.js} +1084 -516
  5. package/dist/chunk-6OZDFNBF.js.map +1 -0
  6. package/dist/chunk-BGK32PZE.js +455 -0
  7. package/dist/chunk-BGK32PZE.js.map +1 -0
  8. package/dist/{chunk-7KOEFGN2.js → chunk-FODMEHD3.js} +52 -14
  9. package/dist/chunk-FODMEHD3.js.map +1 -0
  10. package/dist/{chunk-UK72UQ5U.js → chunk-G3H2L3O2.js} +55 -4
  11. package/dist/chunk-G3H2L3O2.js.map +1 -0
  12. package/dist/{chunk-V5KZGB5V.js → chunk-LEV3KICD.js} +18 -2
  13. package/dist/chunk-LEV3KICD.js.map +1 -0
  14. package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js → cloud-poller-SUNA6ZQC-2RG5WPRN.js} +2 -2
  15. package/dist/{dist-R67WMLCF.js → dist-L4LCG5SJ.js} +120 -10
  16. package/dist/dist-L4LCG5SJ.js.map +1 -0
  17. package/dist/{dist-ETCFRVPA.js → dist-LOZBWMBF.js} +44 -20
  18. package/dist/{dist-QZGJIBT5.js → dist-ZODPD2I6.js} +142 -74
  19. package/dist/dist-ZODPD2I6.js.map +1 -0
  20. package/dist/index.js +3563 -845
  21. package/dist/index.js.map +1 -1
  22. package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js +18 -0
  23. package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js.map +1 -0
  24. package/package.json +4 -4
  25. package/runtime/daytona/custom-system-CLAUDE.md +39 -0
  26. package/runtime/docker/Dockerfile.box +22 -0
  27. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +1 -1
  28. package/runtime/docker/packages/ctl/dist/bin.cjs +1118 -71
  29. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +66 -35
  30. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
  31. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
  32. package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
  33. package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
  34. package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
  35. package/runtime/hetzner/agentbox-codex-hooks.json +66 -35
  36. package/runtime/hetzner/agentbox-setup-skill.md +1 -1
  37. package/runtime/hetzner/claude-managed-settings.json +62 -1
  38. package/runtime/hetzner/ctl.cjs +1118 -71
  39. package/runtime/hetzner/custom-system-CLAUDE.md +26 -14
  40. package/runtime/hetzner/gh-shim +263 -0
  41. package/runtime/hetzner/git-shim +131 -0
  42. package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
  43. package/runtime/hetzner/scripts/install-box.sh +11 -2
  44. package/runtime/relay/bin.cjs +927 -36
  45. package/share/agentbox-setup/SKILL.md +1 -1
  46. package/share/host-skills/agentbox/SKILL.md +29 -0
  47. package/share/host-skills/agentbox-info/SKILL.md +211 -0
  48. package/share/host-skills/codex/agentbox.md +35 -0
  49. package/share/host-skills/opencode/agentbox.md +26 -0
  50. package/dist/_cloud-attach-DMVH6GWO.js +0 -12
  51. package/dist/chunk-7KOEFGN2.js.map +0 -1
  52. package/dist/chunk-NAVL4R34.js.map +0 -1
  53. package/dist/chunk-NW5NYTQM.js.map +0 -1
  54. package/dist/chunk-UK72UQ5U.js.map +0 -1
  55. package/dist/chunk-V5KZGB5V.js.map +0 -1
  56. package/dist/dist-QZGJIBT5.js.map +0 -1
  57. package/dist/dist-R67WMLCF.js.map +0 -1
  58. /package/dist/{_cloud-attach-DMVH6GWO.js.map → _cloud-attach-T727ZPRV.js.map} +0 -0
  59. /package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
  60. /package/dist/{dist-ETCFRVPA.js.map → dist-LOZBWMBF.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,245 @@ 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
+ var GH_RPC_TIMEOUT_MS = 12e4;
18048
+ var GH_READY_CACHE_TTL_MS = 6e4;
18049
+ var ghReadyCache;
18050
+ async function assertGhReady() {
18051
+ const now = Date.now();
18052
+ if (ghReadyCache && ghReadyCache.expiresAt > now) {
18053
+ return ghReadyCache.result;
18054
+ }
18055
+ const result = await probeGh();
18056
+ ghReadyCache = { result, expiresAt: now + GH_READY_CACHE_TTL_MS };
18057
+ return result;
18058
+ }
18059
+ async function probeGh() {
18060
+ const version = await runHostGh(["--version"], process.cwd(), 1e4);
18061
+ if (version.exitCode === 127 || /ENOENT/.test(version.stderr)) {
18062
+ return {
18063
+ exitCode: 127,
18064
+ stdout: "",
18065
+ stderr: "gh not installed on host (https://cli.github.com)\n"
18066
+ };
18067
+ }
18068
+ if (version.exitCode !== 0) {
18069
+ return {
18070
+ exitCode: version.exitCode,
18071
+ stdout: "",
18072
+ stderr: `gh --version failed: ${version.stderr || version.stdout}`.trimEnd() + "\n"
18073
+ };
18074
+ }
18075
+ const auth = await runHostGh(["auth", "status"], process.cwd(), 15e3);
18076
+ if (auth.exitCode !== 0) {
18077
+ return {
18078
+ exitCode: 4,
18079
+ stdout: "",
18080
+ stderr: "gh not authenticated on host (run `gh auth login`)\n"
18081
+ };
18082
+ }
18083
+ return null;
18084
+ }
18085
+ function runHostGh(args, cwd, timeoutMs = GH_RPC_TIMEOUT_MS) {
18086
+ return new Promise((resolve2) => {
18087
+ const child = (0, import_node_child_process6.spawn)("gh", args, {
18088
+ cwd,
18089
+ env: process.env,
18090
+ stdio: ["ignore", "pipe", "pipe"]
18091
+ });
18092
+ let stdout = "";
18093
+ let stderr = "";
18094
+ let settled = false;
18095
+ const finish = (exitCode) => {
18096
+ if (settled) return;
18097
+ settled = true;
18098
+ resolve2({ exitCode, stdout, stderr });
18099
+ };
18100
+ const timer = setTimeout(() => {
18101
+ child.kill("SIGTERM");
18102
+ stderr += `
18103
+ relay: gh command timed out after ${String(timeoutMs)}ms
18104
+ `;
18105
+ finish(124);
18106
+ }, timeoutMs);
18107
+ child.stdout?.on("data", (chunk) => {
18108
+ stdout += chunk.toString("utf8");
18109
+ });
18110
+ child.stderr?.on("data", (chunk) => {
18111
+ stderr += chunk.toString("utf8");
18112
+ });
18113
+ child.on("error", (err) => {
18114
+ clearTimeout(timer);
18115
+ const code = err.code;
18116
+ stderr += String(err.message ?? err);
18117
+ finish(code === "ENOENT" ? 127 : 1);
18118
+ });
18119
+ child.on("close", (code) => {
18120
+ clearTimeout(timer);
18121
+ finish(code ?? -1);
18122
+ });
18123
+ });
18124
+ }
18125
+ async function checkoutGuards(hostMainRepo, registeredBranches) {
18126
+ const status = await runGitProbe(["-C", hostMainRepo, "status", "--porcelain"]);
18127
+ if (status.exitCode !== 0) {
18128
+ return {
18129
+ exitCode: status.exitCode,
18130
+ stdout: "",
18131
+ stderr: `gh pr checkout: failed to inspect host repo: ${status.stderr || status.stdout}`.trimEnd() + "\n"
18132
+ };
18133
+ }
18134
+ if (status.stdout.trim().length > 0) {
18135
+ return {
18136
+ exitCode: 12,
18137
+ stdout: "",
18138
+ stderr: `gh pr checkout: ${hostMainRepo} has uncommitted changes; refusing to switch branches
18139
+ `
18140
+ };
18141
+ }
18142
+ const head = await runGitProbe(["-C", hostMainRepo, "rev-parse", "--abbrev-ref", "HEAD"]);
18143
+ if (head.exitCode !== 0) {
18144
+ return {
18145
+ exitCode: head.exitCode,
18146
+ stdout: "",
18147
+ stderr: `gh pr checkout: failed to resolve HEAD: ${head.stderr || head.stdout}`.trimEnd() + "\n"
18148
+ };
18149
+ }
18150
+ const currentBranch = head.stdout.trim();
18151
+ if (registeredBranches.includes(currentBranch)) {
18152
+ return {
18153
+ exitCode: 12,
18154
+ stdout: "",
18155
+ stderr: `gh pr checkout: ${hostMainRepo} is on registered box branch ${currentBranch}; refusing (would corrupt the bind-mounted box HEAD)
18156
+ `
18157
+ };
18158
+ }
18159
+ return null;
18160
+ }
18161
+ function runGitProbe(args) {
18162
+ return new Promise((resolve2) => {
18163
+ const child = (0, import_node_child_process6.spawn)("git", args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
18164
+ let stdout = "";
18165
+ let stderr = "";
18166
+ child.stdout?.on("data", (c3) => {
18167
+ stdout += c3.toString("utf8");
18168
+ });
18169
+ child.stderr?.on("data", (c3) => {
18170
+ stderr += c3.toString("utf8");
18171
+ });
18172
+ child.on("error", (err) => {
18173
+ resolve2({ exitCode: 127, stdout, stderr: stderr + String(err.message ?? err) });
18174
+ });
18175
+ child.on("close", (code) => {
18176
+ resolve2({ exitCode: code ?? -1, stdout, stderr });
18177
+ });
18178
+ });
18179
+ }
18180
+ function refuseMergeBypass(op) {
18181
+ if (op !== "merge") return null;
18182
+ if (process.env["AGENTBOX_PROMPT"] !== "off") return null;
18183
+ if (process.env["AGENTBOX_GH_FORCE"] === "1") return null;
18184
+ return {
18185
+ exitCode: 10,
18186
+ stdout: "",
18187
+ stderr: "gh pr merge: AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1 (merge is irreversible)\n"
18188
+ };
18189
+ }
18190
+ function refuseCheckoutByDefault(op) {
18191
+ if (op !== "checkout") return null;
18192
+ if (process.env["AGENTBOX_GH_PR_CHECKOUT"] === "allow") return null;
18193
+ return {
18194
+ exitCode: 13,
18195
+ stdout: "",
18196
+ stderr: "gh pr checkout: disabled by default; set AGENTBOX_GH_PR_CHECKOUT=allow to enable\n"
18197
+ };
18198
+ }
18199
+
18200
+ // src/host-initiated.ts
17977
18201
  var import_node_crypto = require("crypto");
18202
+ var DEFAULT_TTL_MS = 12e4;
18203
+ function hashRpcParams(params) {
18204
+ return (0, import_node_crypto.createHash)("sha256").update(canonicalJson(params)).digest("hex");
18205
+ }
18206
+ function canonicalJson(v) {
18207
+ if (v === null) return "null";
18208
+ if (typeof v === "undefined") return "null";
18209
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
18210
+ if (typeof v === "boolean") return v ? "true" : "false";
18211
+ if (typeof v === "string") return JSON.stringify(v);
18212
+ if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
18213
+ if (typeof v === "object") {
18214
+ const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a2], [b]) => a2 < b ? -1 : a2 > b ? 1 : 0);
18215
+ return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
18216
+ }
18217
+ return "null";
18218
+ }
18219
+ var HostInitiatedTokens = class {
18220
+ store = /* @__PURE__ */ new Map();
18221
+ /**
18222
+ * Mint a fresh one-time token scoped to (boxId, method, paramsHash).
18223
+ * `paramsHash` MUST be supplied for any call surface where the box can
18224
+ * influence the eventual RPC params. Pass `null` only when there are no
18225
+ * params (no current call sites use this).
18226
+ */
18227
+ mint(boxId, method, paramsHash, ttlMs = DEFAULT_TTL_MS) {
18228
+ const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
18229
+ this.store.set(token, { boxId, method, paramsHash, expiresAt: Date.now() + ttlMs });
18230
+ return token;
18231
+ }
18232
+ /**
18233
+ * Returns true exactly once if `token` is a valid, unexpired token for the
18234
+ * given `(boxId, method)` AND the supplied `incomingParamsHash` matches
18235
+ * the hash bound at mint time. The token is removed on a successful match
18236
+ * (one-shot semantics). All failure modes return false — callers fall back
18237
+ * to the normal prompt path.
18238
+ */
18239
+ consume(token, boxId, method, incomingParamsHash) {
18240
+ if (!token || typeof token !== "string") return false;
18241
+ const record = this.store.get(token);
18242
+ if (!record) return false;
18243
+ if (record.expiresAt < Date.now()) {
18244
+ this.store.delete(token);
18245
+ return false;
18246
+ }
18247
+ if (record.boxId !== boxId || record.method !== method) return false;
18248
+ if (record.paramsHash !== null && record.paramsHash !== incomingParamsHash) {
18249
+ return false;
18250
+ }
18251
+ this.store.delete(token);
18252
+ return true;
18253
+ }
18254
+ /** Drop expired entries. Cheap; safe to call periodically. */
18255
+ gc() {
18256
+ const now = Date.now();
18257
+ for (const [token, record] of this.store) {
18258
+ if (record.expiresAt < now) this.store.delete(token);
18259
+ }
18260
+ }
18261
+ /** Test-only: number of live tokens. */
18262
+ size() {
18263
+ return this.store.size;
18264
+ }
18265
+ };
18266
+
18267
+ // src/prompts.ts
18268
+ var import_node_crypto2 = require("crypto");
17978
18269
  var PendingPrompts = class {
17979
18270
  entries = /* @__PURE__ */ new Map();
17980
18271
  add(boxId, ev) {
@@ -18063,7 +18354,7 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
18063
18354
  if (process.env.AGENTBOX_PROMPT === "off") {
18064
18355
  return { answer: "y" };
18065
18356
  }
18066
- const ev = { id: (0, import_node_crypto.randomUUID)(), ...params };
18357
+ const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
18067
18358
  const promise = prompts.add(boxId, ev);
18068
18359
  subscribers.broadcast(boxId, "prompt-ask", ev);
18069
18360
  if (opts?.ttlMs !== void 0 && opts.ttlMs > 0) {
@@ -18122,6 +18413,18 @@ async function resolveCloudBackend(name) {
18122
18413
  }
18123
18414
  throw new Error(`no host executor for cloud backend '${name}'`);
18124
18415
  }
18416
+ async function refreshCloudPreviewUrl(backendName, boxId, port) {
18417
+ try {
18418
+ const backend = await resolveCloudBackend(backendName);
18419
+ if (!backend.refreshPreviewUrl) return null;
18420
+ const lookup = await lookupCloudBox(boxId);
18421
+ const handle = { sandboxId: lookup.cloudSandboxId };
18422
+ const url = await backend.refreshPreviewUrl(handle, port);
18423
+ return url.url;
18424
+ } catch {
18425
+ return null;
18426
+ }
18427
+ }
18125
18428
  async function executeCloudAction(action, deps) {
18126
18429
  const log = deps.log ?? (() => {
18127
18430
  });
@@ -18141,6 +18444,17 @@ async function executeCloudAction(action, deps) {
18141
18444
  if (action.method === "browser.open.mirror") {
18142
18445
  return runBrowserOpenMirror(action, deps);
18143
18446
  }
18447
+ if (action.method.startsWith("gh.pr.")) {
18448
+ return runGhPrRpc(action, deps);
18449
+ }
18450
+ if (action.method === "git.clone" || action.method === "gh.repo.clone") {
18451
+ return {
18452
+ exitCode: 64,
18453
+ stdout: "",
18454
+ 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.
18455
+ `
18456
+ };
18457
+ }
18144
18458
  return {
18145
18459
  exitCode: 1,
18146
18460
  stdout: "",
@@ -18148,6 +18462,86 @@ async function executeCloudAction(action, deps) {
18148
18462
  `
18149
18463
  };
18150
18464
  }
18465
+ async function runGhPrRpc(action, deps) {
18466
+ const op = action.method.slice("gh.pr.".length);
18467
+ if (!isGhPrOp(op)) {
18468
+ return {
18469
+ exitCode: 64,
18470
+ stdout: "",
18471
+ stderr: `unknown gh.pr.* op: ${op}
18472
+ `
18473
+ };
18474
+ }
18475
+ const mergeBypass = refuseMergeBypass(op);
18476
+ if (mergeBypass) return mergeBypass;
18477
+ const checkoutOptIn = refuseCheckoutByDefault(op);
18478
+ if (checkoutOptIn) return checkoutOptIn;
18479
+ const params = action.params ?? {};
18480
+ const args = Array.isArray(params.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
18481
+ const ghReady = await assertGhReady();
18482
+ if (ghReady) return ghReady;
18483
+ const lookup = await lookupCloudBox(deps.boxId);
18484
+ if (op === "checkout") {
18485
+ const guard = await checkoutGuards(lookup.workspacePath, []);
18486
+ if (guard) return guard;
18487
+ }
18488
+ const tokenClaimedGhCloud = typeof params.hostInitiated === "string";
18489
+ const incomingHashGhCloud = hashRpcParams(params);
18490
+ const hostInitiatedGhOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && (deps.hostInitiatedTokens?.consume(
18491
+ params.hostInitiated,
18492
+ deps.boxId,
18493
+ `gh.pr.${op}`,
18494
+ incomingHashGhCloud
18495
+ ) ?? false);
18496
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && !hostInitiatedGhOk) {
18497
+ return {
18498
+ exitCode: 10,
18499
+ stdout: "",
18500
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
18501
+ };
18502
+ }
18503
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedGhOk && deps.prompts && deps.subscribers) {
18504
+ const detail = args.join(" ").slice(0, 200);
18505
+ const ctx = {
18506
+ kind: "confirm",
18507
+ message: `Allow gh pr ${op} from cloud box ${deps.boxName ?? deps.boxId}?`,
18508
+ detail,
18509
+ defaultAnswer: "n",
18510
+ context: {
18511
+ command: `gh pr ${op}`,
18512
+ cwd: params.path,
18513
+ argv: args
18514
+ }
18515
+ };
18516
+ const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
18517
+ if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
18518
+ const noSubMode = (process.env["AGENTBOX_GH_NO_SUB"] ?? "deny").toLowerCase();
18519
+ if (noSubMode === "deny") {
18520
+ return {
18521
+ exitCode: 10,
18522
+ stdout: "",
18523
+ stderr: "denied automatically \u2014 no attached wrapper to confirm. Attach `agentbox claude` (or similar) and retry, or set AGENTBOX_GH_NO_SUB=allow.\n"
18524
+ };
18525
+ }
18526
+ if (noSubMode === "allow") {
18527
+ deps.log?.(`gh.pr.${op} auto-approved (no subscribers, AGENTBOX_GH_NO_SUB=allow)`);
18528
+ } else {
18529
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx, {
18530
+ ttlMs: 5 * 60 * 1e3
18531
+ });
18532
+ if (verdict.answer !== "y") {
18533
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
18534
+ }
18535
+ }
18536
+ } else {
18537
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx);
18538
+ if (verdict.answer !== "y") {
18539
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
18540
+ }
18541
+ }
18542
+ }
18543
+ return runHostGh(["pr", op, ...args], lookup.workspacePath);
18544
+ }
18151
18545
  async function runBrowserOpenMirror(action, deps) {
18152
18546
  const params = action.params ?? {};
18153
18547
  const url = typeof params.url === "string" ? params.url.trim() : "";
@@ -18174,8 +18568,8 @@ async function runBrowserOpenMirror(action, deps) {
18174
18568
  { ttlMs: TTL_MS }
18175
18569
  );
18176
18570
  if (verdict.answer === "y" && !verdict.cancelled) {
18177
- const { spawn: spawn4 } = await import("child_process");
18178
- const child = spawn4("open", [url], { stdio: "ignore", detached: true });
18571
+ const { spawn: spawn6 } = await import("child_process");
18572
+ const child = spawn6("open", [url], { stdio: "ignore", detached: true });
18179
18573
  child.unref();
18180
18574
  }
18181
18575
  } catch (err) {
@@ -18346,7 +18740,23 @@ async function runGitRpc(action, deps) {
18346
18740
  stderr: `failed to resolve branch in sandbox ${containerPath}: ${branchProbe.stderr || branch}`
18347
18741
  };
18348
18742
  }
18349
- if (action.method === "git.push" && deps.prompts && deps.subscribers) {
18743
+ const isAgentboxBranch = branch.startsWith("agentbox/");
18744
+ const tokenClaimedGit = typeof params.hostInitiated === "string";
18745
+ const incomingHashGit = hashRpcParams(params);
18746
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimedGit && (deps.hostInitiatedTokens?.consume(
18747
+ params.hostInitiated,
18748
+ deps.boxId,
18749
+ "git.push",
18750
+ incomingHashGit
18751
+ ) ?? false);
18752
+ if (action.method === "git.push" && !isAgentboxBranch && tokenClaimedGit && !hostInitiatedOk) {
18753
+ return {
18754
+ exitCode: 10,
18755
+ stdout: "",
18756
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
18757
+ };
18758
+ }
18759
+ if (action.method === "git.push" && !isAgentboxBranch && !hostInitiatedOk && deps.prompts && deps.subscribers) {
18350
18760
  const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
18351
18761
  if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
18352
18762
  const noSubMode = (process.env["AGENTBOX_GIT_PUSH_NO_SUB"] ?? "deny").toLowerCase();
@@ -18421,10 +18831,45 @@ async function runGitRpc(action, deps) {
18421
18831
  for (const a2 of params.args) if (typeof a2 === "string") argv.push(a2);
18422
18832
  }
18423
18833
  const push = await execa("git", argv, { reject: false });
18834
+ let pushStderr = push.stderr ?? "";
18835
+ if ((push.exitCode ?? 1) === 0 && !branch.startsWith("agentbox/")) {
18836
+ try {
18837
+ const sha = await execa(
18838
+ "git",
18839
+ ["-C", lookup.workspacePath, "rev-parse", branch],
18840
+ { reject: false }
18841
+ );
18842
+ const shaText = (sha.stdout ?? "").trim();
18843
+ if (sha.exitCode === 0 && shaText.length > 0) {
18844
+ const updateRef = await backend.exec(
18845
+ handle,
18846
+ `git -C ${shellQuote(containerPath)} update-ref refs/remotes/${remote2}/${branch} ${shellQuote(shaText)}`
18847
+ );
18848
+ if (updateRef.exitCode !== 0) {
18849
+ pushStderr += `
18850
+ relay: post-push in-box update-ref refs/remotes/${remote2}/${branch} failed: ${updateRef.stderr || updateRef.stdout}`;
18851
+ }
18852
+ const setUpstream = await backend.exec(
18853
+ handle,
18854
+ `git -C ${shellQuote(containerPath)} branch --set-upstream-to=${remote2}/${branch} ${shellQuote(branch)}`
18855
+ );
18856
+ if (setUpstream.exitCode !== 0) {
18857
+ pushStderr += `
18858
+ relay: post-push in-box --set-upstream-to=${remote2}/${branch} failed: ${setUpstream.stderr || setUpstream.stdout}`;
18859
+ }
18860
+ } else {
18861
+ pushStderr += `
18862
+ relay: post-push rev-parse ${branch} failed on host; skipping in-box origin/upstream sync`;
18863
+ }
18864
+ } catch (err) {
18865
+ pushStderr += `
18866
+ relay: post-push in-box origin/upstream sync threw: ${err instanceof Error ? err.message : String(err)}`;
18867
+ }
18868
+ }
18424
18869
  return {
18425
18870
  exitCode: push.exitCode ?? 1,
18426
18871
  stdout: push.stdout ?? "",
18427
- stderr: push.stderr ?? ""
18872
+ stderr: pushStderr
18428
18873
  };
18429
18874
  }
18430
18875
  const remote = params.remote ?? "origin";
@@ -18471,7 +18916,7 @@ function shellQuote(arg) {
18471
18916
  }
18472
18917
 
18473
18918
  // src/host-action-queue.ts
18474
- var import_node_crypto2 = require("crypto");
18919
+ var import_node_crypto3 = require("crypto");
18475
18920
  var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
18476
18921
  var HostActionQueue = class {
18477
18922
  map = /* @__PURE__ */ new Map();
@@ -18489,7 +18934,7 @@ var HostActionQueue = class {
18489
18934
  * is open" semantics).
18490
18935
  */
18491
18936
  enqueue(boxId, method, params) {
18492
- const id = (0, import_node_crypto2.randomUUID)();
18937
+ const id = (0, import_node_crypto3.randomUUID)();
18493
18938
  const action = {
18494
18939
  id,
18495
18940
  boxId,
@@ -18555,7 +19000,7 @@ var HostActionQueue = class {
18555
19000
  };
18556
19001
 
18557
19002
  // src/notices.ts
18558
- var import_node_crypto3 = require("crypto");
19003
+ var import_node_crypto4 = require("crypto");
18559
19004
  var DEFAULT_NOTICE_TTL_MS = 66e4;
18560
19005
  var BoxNotices = class {
18561
19006
  constructor(subscribers) {
@@ -18578,7 +19023,7 @@ var BoxNotices = class {
18578
19023
  this.entries.delete(id);
18579
19024
  }
18580
19025
  }
18581
- const ev = { id: (0, import_node_crypto3.randomUUID)(), kind, message };
19026
+ const ev = { id: (0, import_node_crypto4.randomUUID)(), kind, message };
18582
19027
  const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;
18583
19028
  const timer = setTimeout(() => {
18584
19029
  if (this.entries.delete(ev.id)) {
@@ -18796,6 +19241,8 @@ function createRelayServer(opts) {
18796
19241
  const prompts = new PendingPrompts();
18797
19242
  const subscribers = new PromptSubscribers();
18798
19243
  const notices = new BoxNotices(subscribers);
19244
+ const hostInitiatedTokens = new HostInitiatedTokens();
19245
+ let queuePoke = null;
18799
19246
  const host = opts.host ?? "0.0.0.0";
18800
19247
  const mode = opts.mode ?? "host";
18801
19248
  const hostActions = mode === "box" ? new HostActionQueue() : null;
@@ -18928,21 +19375,36 @@ function createRelayServer(opts) {
18928
19375
  if (body.method === "git.push" || body.method === "git.fetch") {
18929
19376
  if (body.method === "git.push") {
18930
19377
  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" });
19378
+ const worktree = resolveWorktree(reg, params?.path ?? "/workspace");
19379
+ const isAgentboxBranch = worktree?.branch.startsWith("agentbox/") ?? false;
19380
+ const tokenClaimed = typeof params?.hostInitiated === "string";
19381
+ const incomingHash = hashRpcParams(params);
19382
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimed && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, "git.push", incomingHash);
19383
+ if (!isAgentboxBranch && tokenClaimed && !hostInitiatedOk) {
19384
+ send(res, 500, {
19385
+ exitCode: 10,
19386
+ stdout: "",
19387
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19388
+ });
18944
19389
  return;
18945
19390
  }
19391
+ if (!isAgentboxBranch && !hostInitiatedOk) {
19392
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
19393
+ kind: "confirm",
19394
+ message: `Allow git push from box ${reg.name}?`,
19395
+ detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
19396
+ defaultAnswer: "n",
19397
+ context: {
19398
+ command: "git push",
19399
+ cwd: params?.path,
19400
+ argv: params?.args
19401
+ }
19402
+ });
19403
+ if (verdict.answer !== "y") {
19404
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
19405
+ return;
19406
+ }
19407
+ }
18946
19408
  }
18947
19409
  const result = await handleGitRpc(reg, body.method, body.params);
18948
19410
  const status = result.exitCode === 0 ? 200 : 500;
@@ -18975,6 +19437,33 @@ function createRelayServer(opts) {
18975
19437
  send(res, status, result);
18976
19438
  return;
18977
19439
  }
19440
+ if (body.method.startsWith("gh.pr.")) {
19441
+ const op = body.method.slice("gh.pr.".length);
19442
+ if (!isGhPrOp(op)) {
19443
+ send(res, 400, { error: `unknown gh.pr.* op: ${op}` });
19444
+ return;
19445
+ }
19446
+ const result = await handleGhPrRpc(
19447
+ op,
19448
+ reg,
19449
+ body.params,
19450
+ prompts,
19451
+ subscribers,
19452
+ hostInitiatedTokens
19453
+ );
19454
+ const status = result.exitCode === 0 ? 200 : 500;
19455
+ send(res, status, result);
19456
+ return;
19457
+ }
19458
+ if (body.method === "git.clone" || body.method === "gh.repo.clone") {
19459
+ send(res, 501, {
19460
+ exitCode: 64,
19461
+ stdout: "",
19462
+ 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.
19463
+ `
19464
+ });
19465
+ return;
19466
+ }
18978
19467
  if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
18979
19468
  const params = body.params;
18980
19469
  const kind = body.method.split(".")[1] ?? "workspace";
@@ -19111,6 +19600,7 @@ function createRelayServer(opts) {
19111
19600
  boxName: reg.name,
19112
19601
  prompts,
19113
19602
  subscribers,
19603
+ hostInitiatedTokens,
19114
19604
  log
19115
19605
  });
19116
19606
  await respond(result);
@@ -19123,6 +19613,11 @@ function createRelayServer(opts) {
19123
19613
  });
19124
19614
  }
19125
19615
  } : void 0,
19616
+ // Self-heal a dead preview transport (hetzner SSH `-L` after a
19617
+ // ControlMaster death). The relay strips the `cloud:` prefix
19618
+ // the cloud-provider tags onto BoxRecord.container — what the
19619
+ // backend's `get(sandboxId)` expects is the bare sandbox id.
19620
+ recoverPreviewUrl: reg.backend ? async () => refreshCloudPreviewUrl(reg.backend, reg.boxId, DEFAULT_BOX_RELAY_PORT) : void 0,
19126
19621
  logger: log
19127
19622
  });
19128
19623
  } catch (err) {
@@ -19237,6 +19732,27 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19237
19732
  send(res, 204, null);
19238
19733
  return;
19239
19734
  }
19735
+ if (route === "POST /admin/host-initiated/mint") {
19736
+ const body = await readJsonBody(req);
19737
+ if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.method !== "string" || body.method.length === 0) {
19738
+ send(res, 400, { error: "expected {boxId, method, paramsHash, ttlMs?}" });
19739
+ return;
19740
+ }
19741
+ let paramsHash;
19742
+ if (body.paramsHash === null || body.paramsHash === void 0) {
19743
+ paramsHash = null;
19744
+ } else if (typeof body.paramsHash === "string" && /^[0-9a-f]{64}$/.test(body.paramsHash)) {
19745
+ paramsHash = body.paramsHash;
19746
+ } else {
19747
+ send(res, 400, { error: "paramsHash must be a 64-hex sha256 string or null" });
19748
+ return;
19749
+ }
19750
+ const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
19751
+ const token = hostInitiatedTokens.mint(body.boxId, body.method, paramsHash, ttlMs);
19752
+ log(`host-initiated-mint box=${body.boxId} method=${body.method} paramsBound=${paramsHash !== null}`);
19753
+ send(res, 200, { token });
19754
+ return;
19755
+ }
19240
19756
  if (route === "POST /admin/notices/set") {
19241
19757
  const body = await readJsonBody(req);
19242
19758
  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 +19765,17 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19249
19765
  send(res, 200, { id });
19250
19766
  return;
19251
19767
  }
19768
+ if (route === "POST /admin/queue/enqueue") {
19769
+ const body = await readJsonBody(req);
19770
+ if (!body || typeof body.id !== "string" || body.id.length === 0) {
19771
+ send(res, 400, { error: "expected {id}" });
19772
+ return;
19773
+ }
19774
+ log(`queue-enqueue id=${body.id}`);
19775
+ queuePoke?.();
19776
+ send(res, 204, null);
19777
+ return;
19778
+ }
19252
19779
  if (route === "POST /admin/notices/clear") {
19253
19780
  const body = await readJsonBody(req);
19254
19781
  if (!body || typeof body.id !== "string" || body.id.length === 0) {
@@ -19285,6 +19812,9 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19285
19812
  notices,
19286
19813
  hostActions: hostActions ?? void 0,
19287
19814
  url: `http://${host}:${String(opts.port)}`,
19815
+ setQueuePoke: (fn) => {
19816
+ queuePoke = fn;
19817
+ },
19288
19818
  close: async () => {
19289
19819
  if (pollers) await pollers.stopAll();
19290
19820
  await new Promise((resolve2, reject) => {
@@ -19336,7 +19866,69 @@ async function handleGitRpc(reg, method, params) {
19336
19866
  if (typeof a2 === "string") argv.push(a2);
19337
19867
  }
19338
19868
  }
19339
- return runHostCommand(argv);
19869
+ const result = await runHostCommand(argv);
19870
+ if (method === "git.push" && result.exitCode === 0 && !worktree.branch.startsWith("agentbox/")) {
19871
+ await runHostCommand([
19872
+ "git",
19873
+ "-C",
19874
+ worktree.hostMainRepo,
19875
+ "branch",
19876
+ `--set-upstream-to=${remote}/${worktree.branch}`,
19877
+ worktree.branch
19878
+ ]);
19879
+ }
19880
+ return result;
19881
+ }
19882
+ async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiatedTokens) {
19883
+ const mergeBypass = refuseMergeBypass(op);
19884
+ if (mergeBypass) return mergeBypass;
19885
+ const checkoutOptIn = refuseCheckoutByDefault(op);
19886
+ if (checkoutOptIn) return checkoutOptIn;
19887
+ const containerPath = params?.path ?? "/workspace";
19888
+ const worktree = resolveWorktree(reg, containerPath);
19889
+ if (!worktree) {
19890
+ return {
19891
+ exitCode: 64,
19892
+ stdout: "",
19893
+ stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
19894
+ };
19895
+ }
19896
+ const ghReady = await assertGhReady();
19897
+ if (ghReady) return ghReady;
19898
+ const args = Array.isArray(params?.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
19899
+ if (op === "checkout") {
19900
+ const branches = (reg.worktrees ?? []).map((w) => w.branch);
19901
+ const guard = await checkoutGuards(worktree.hostMainRepo, branches);
19902
+ if (guard) return guard;
19903
+ }
19904
+ const tokenClaimedGh = typeof params?.hostInitiated === "string";
19905
+ const incomingHashGh = hashRpcParams(params);
19906
+ const hostInitiatedOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, `gh.pr.${op}`, incomingHashGh);
19907
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && !hostInitiatedOk) {
19908
+ return {
19909
+ exitCode: 10,
19910
+ stdout: "",
19911
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19912
+ };
19913
+ }
19914
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedOk) {
19915
+ const detail = args.join(" ").slice(0, 200);
19916
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
19917
+ kind: "confirm",
19918
+ message: `Allow gh pr ${op} from box ${reg.name}?`,
19919
+ detail,
19920
+ defaultAnswer: "n",
19921
+ context: {
19922
+ command: `gh pr ${op}`,
19923
+ cwd: containerPath,
19924
+ argv: args
19925
+ }
19926
+ });
19927
+ if (verdict.answer !== "y") {
19928
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19929
+ }
19930
+ }
19931
+ return runHostGh(["pr", op, ...args], worktree.hostMainRepo);
19340
19932
  }
19341
19933
  async function handleCpRpc(reg, method, params) {
19342
19934
  const entry = process.env.AGENTBOX_CLI_ENTRY;
@@ -19397,7 +19989,7 @@ function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
19397
19989
  resolve2({ exitCode: 64, stdout: "", stderr: "empty command" });
19398
19990
  return;
19399
19991
  }
19400
- const child = (0, import_node_child_process6.spawn)(cmd, rest, {
19992
+ const child = (0, import_node_child_process7.spawn)(cmd, rest, {
19401
19993
  env: process.env,
19402
19994
  stdio: ["ignore", "pipe", "pipe"]
19403
19995
  });
@@ -19446,7 +20038,7 @@ async function startRelayServer(opts) {
19446
20038
  }
19447
20039
 
19448
20040
  // src/autopause.ts
19449
- var import_node_child_process7 = require("child_process");
20041
+ var import_node_child_process8 = require("child_process");
19450
20042
  var import_promises16 = require("fs/promises");
19451
20043
 
19452
20044
  // ../config/dist/index.js
@@ -19475,7 +20067,8 @@ var BUILT_IN_DEFAULTS = {
19475
20067
  memory: 0,
19476
20068
  cpus: 0,
19477
20069
  pidsLimit: 0,
19478
- disk: ""
20070
+ disk: "",
20071
+ bundleDepth: void 0
19479
20072
  },
19480
20073
  checkpoint: {
19481
20074
  maxLayers: 3
@@ -19524,6 +20117,10 @@ var BUILT_IN_DEFAULTS = {
19524
20117
  maxRunningBoxes: 5,
19525
20118
  idleMinutes: 5
19526
20119
  },
20120
+ queue: {
20121
+ enabled: true,
20122
+ maxConcurrent: 5
20123
+ },
19527
20124
  maintenance: {
19528
20125
  pruneProjectConfigs: true,
19529
20126
  pruneProjectConfigsEvery: 50
@@ -19632,6 +20229,11 @@ var KEY_REGISTRY = [
19632
20229
  description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
19633
20230
  advanced: true
19634
20231
  },
20232
+ {
20233
+ key: "box.bundleDepth",
20234
+ type: "int",
20235
+ 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/)."
20236
+ },
19635
20237
  {
19636
20238
  key: "claude.sessionName",
19637
20239
  type: "string",
@@ -19739,6 +20341,16 @@ var KEY_REGISTRY = [
19739
20341
  type: "int",
19740
20342
  description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
19741
20343
  },
20344
+ {
20345
+ key: "queue.enabled",
20346
+ type: "bool",
20347
+ description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
20348
+ },
20349
+ {
20350
+ key: "queue.maxConcurrent",
20351
+ type: "int",
20352
+ 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>`."
20353
+ },
19742
20354
  {
19743
20355
  key: "maintenance.pruneProjectConfigs",
19744
20356
  type: "bool",
@@ -19833,6 +20445,15 @@ function parseUserConfigObject(doc, where) {
19833
20445
  }
19834
20446
  const out = {};
19835
20447
  for (const [branchName, branchRaw] of Object.entries(doc)) {
20448
+ if (branchName === "schema") {
20449
+ if (branchRaw !== void 0 && branchRaw !== null) {
20450
+ if (typeof branchRaw !== "number" || !Number.isInteger(branchRaw)) {
20451
+ throw new UserConfigError(`${where}.schema: must be an integer (got ${String(branchRaw)})`);
20452
+ }
20453
+ out.schema = branchRaw;
20454
+ }
20455
+ continue;
20456
+ }
19836
20457
  const branchSpec = BRANCHES.get(branchName);
19837
20458
  if (!branchSpec) {
19838
20459
  throw new UserConfigError(
@@ -20005,7 +20626,7 @@ var INSPECT_TIMEOUT_MS = 15e3;
20005
20626
  var PAUSE_TIMEOUT_MS = 3e4;
20006
20627
  function runDocker(args, timeoutMs) {
20007
20628
  return new Promise((resolve2) => {
20008
- const child = (0, import_node_child_process7.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
20629
+ const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
20009
20630
  let stdout = "";
20010
20631
  let stderr = "";
20011
20632
  let settled = false;
@@ -20063,6 +20684,269 @@ async function pauseContainer(name) {
20063
20684
  }
20064
20685
  }
20065
20686
 
20687
+ // src/queue.ts
20688
+ var import_node_child_process9 = require("child_process");
20689
+ var import_promises17 = require("fs/promises");
20690
+ var import_node_fs6 = require("fs");
20691
+ var import_node_path8 = require("path");
20692
+ var import_promises18 = require("timers/promises");
20693
+ var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
20694
+ async function loadQueueConfig() {
20695
+ const d = BUILT_IN_DEFAULTS.queue;
20696
+ let global3 = {};
20697
+ try {
20698
+ global3 = parseUserConfig(await (0, import_promises17.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
20699
+ } catch {
20700
+ }
20701
+ const q = global3.queue ?? {};
20702
+ return {
20703
+ enabled: q.enabled ?? d.enabled,
20704
+ maxConcurrent: q.maxConcurrent ?? d.maxConcurrent
20705
+ };
20706
+ }
20707
+ async function writeJob(job) {
20708
+ await (0, import_promises17.mkdir)(QUEUE_DIR, { recursive: true });
20709
+ const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
20710
+ const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
20711
+ await (0, import_promises17.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
20712
+ await (0, import_promises17.rename)(tmp, final);
20713
+ }
20714
+ async function readJob(id) {
20715
+ try {
20716
+ const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
20717
+ return JSON.parse(raw);
20718
+ } catch (err) {
20719
+ if (err.code === "ENOENT") return null;
20720
+ throw err;
20721
+ }
20722
+ }
20723
+ async function loadQueue() {
20724
+ let entries;
20725
+ try {
20726
+ entries = await (0, import_promises17.readdir)(QUEUE_DIR);
20727
+ } catch (err) {
20728
+ if (err.code === "ENOENT") return [];
20729
+ throw err;
20730
+ }
20731
+ const out = [];
20732
+ for (const name of entries) {
20733
+ if (!name.endsWith(".json")) continue;
20734
+ try {
20735
+ const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
20736
+ out.push(JSON.parse(raw));
20737
+ } catch {
20738
+ }
20739
+ }
20740
+ out.sort((a2, b) => a2.createdAt < b.createdAt ? -1 : a2.createdAt > b.createdAt ? 1 : 0);
20741
+ return out;
20742
+ }
20743
+ function selectNextRunnable(jobs, runningCount) {
20744
+ for (const j of jobs) {
20745
+ if (j.status !== "queued") continue;
20746
+ if (runningCount < j.maxConcurrent) return j;
20747
+ }
20748
+ return null;
20749
+ }
20750
+ var DEFAULT_INTERVAL_MS2 = 2e3;
20751
+ function startQueueLoop(deps) {
20752
+ const loadConfig = deps.loadConfig ?? loadQueueConfig;
20753
+ const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
20754
+ const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
20755
+ const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
20756
+ const { log, onStatusChange } = deps;
20757
+ let ticking = false;
20758
+ let stopped = false;
20759
+ let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
20760
+ log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
20761
+ });
20762
+ async function tick() {
20763
+ if (ticking) return;
20764
+ ticking = true;
20765
+ try {
20766
+ const cfg = await loadConfig();
20767
+ if (!cfg.enabled) return;
20768
+ const jobs = await loadQueue();
20769
+ const hasQueued = jobs.some((j) => j.status === "queued");
20770
+ if (!hasQueued) return;
20771
+ while (!stopped) {
20772
+ const running = await countRunning();
20773
+ const fresh = await loadQueue();
20774
+ const next = selectNextRunnable(fresh, running);
20775
+ if (!next) return;
20776
+ const current = await readJob(next.id);
20777
+ if (!current || current.status !== "queued") continue;
20778
+ const updated = {
20779
+ ...current,
20780
+ status: "running",
20781
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
20782
+ };
20783
+ await writeJob(updated);
20784
+ onStatusChange?.(updated);
20785
+ try {
20786
+ const pid = await spawnWorker(updated);
20787
+ if (typeof pid === "number") {
20788
+ const withPid = { ...updated, pid };
20789
+ await writeJob(withPid);
20790
+ onStatusChange?.(withPid);
20791
+ log(
20792
+ `queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; running ${String(running + 1)}/${String(updated.maxConcurrent)}`
20793
+ );
20794
+ } else {
20795
+ log(`queue: started job ${updated.id} (${updated.agent}); pid unknown`);
20796
+ }
20797
+ } catch (err) {
20798
+ const msg = err instanceof Error ? err.message : String(err);
20799
+ const failed = {
20800
+ ...updated,
20801
+ status: "failed",
20802
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
20803
+ reason: `worker-spawn-failed: ${msg}`
20804
+ };
20805
+ await writeJob(failed);
20806
+ onStatusChange?.(failed);
20807
+ log(`queue: spawn for job ${updated.id} failed: ${msg}`);
20808
+ }
20809
+ }
20810
+ } catch (err) {
20811
+ const msg = err instanceof Error ? err.message : String(err);
20812
+ log(`queue: tick error: ${msg}`);
20813
+ } finally {
20814
+ ticking = false;
20815
+ }
20816
+ }
20817
+ function poke() {
20818
+ if (stopped) return;
20819
+ inFlight = tick();
20820
+ }
20821
+ const handle = {
20822
+ poke,
20823
+ stop: async () => {
20824
+ stopped = true;
20825
+ clearInterval(timer);
20826
+ await inFlight.catch(() => {
20827
+ });
20828
+ }
20829
+ };
20830
+ const timer = setInterval(() => {
20831
+ if (stopped) return;
20832
+ inFlight = tick();
20833
+ }, intervalMs);
20834
+ timer.unref();
20835
+ return handle;
20836
+ }
20837
+ async function recoverOrphanedWorkers(log, onChange) {
20838
+ const jobs = await loadQueue();
20839
+ for (const j of jobs) {
20840
+ if (j.status !== "running") continue;
20841
+ if (typeof j.pid === "number" && processAlive(j.pid)) continue;
20842
+ const failed = {
20843
+ ...j,
20844
+ status: "failed",
20845
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
20846
+ reason: "worker-died"
20847
+ };
20848
+ await writeJob(failed);
20849
+ onChange?.(failed);
20850
+ log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
20851
+ }
20852
+ }
20853
+ function processAlive(pid) {
20854
+ try {
20855
+ process.kill(pid, 0);
20856
+ return true;
20857
+ } catch {
20858
+ return false;
20859
+ }
20860
+ }
20861
+ var RUNNING_COUNT_CACHE_MS = 3e3;
20862
+ var runningCountCache = null;
20863
+ async function defaultCountRunningBoxes() {
20864
+ const now = Date.now();
20865
+ if (runningCountCache && runningCountCache.expiresAt > now) {
20866
+ return runningCountCache.value;
20867
+ }
20868
+ const value = await uncachedCountRunningBoxes();
20869
+ runningCountCache = { value, expiresAt: now + RUNNING_COUNT_CACHE_MS };
20870
+ return value;
20871
+ }
20872
+ async function uncachedCountRunningBoxes() {
20873
+ let boxes;
20874
+ try {
20875
+ boxes = (await readState(STATE_FILE)).boxes;
20876
+ } catch {
20877
+ return 0;
20878
+ }
20879
+ if (boxes.length === 0) return 0;
20880
+ let count2 = 0;
20881
+ const dockerBoxes = [];
20882
+ for (const b of boxes) {
20883
+ const provider = b.provider ?? "docker";
20884
+ if (provider === "docker") {
20885
+ dockerBoxes.push(b);
20886
+ } else {
20887
+ count2 += 1;
20888
+ }
20889
+ }
20890
+ if (dockerBoxes.length > 0) {
20891
+ const states = await Promise.all(dockerBoxes.map((b) => inspectDockerState(b.container)));
20892
+ for (const s of states) {
20893
+ if (s === "running") count2 += 1;
20894
+ }
20895
+ }
20896
+ return count2;
20897
+ }
20898
+ function inspectDockerState(containerName) {
20899
+ return new Promise((resolveP) => {
20900
+ const child = (0, import_node_child_process9.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
20901
+ stdio: ["ignore", "pipe", "pipe"]
20902
+ });
20903
+ let out = "";
20904
+ let settled = false;
20905
+ const finish = (state) => {
20906
+ if (settled) return;
20907
+ settled = true;
20908
+ resolveP(state);
20909
+ };
20910
+ const timer = setTimeout(() => {
20911
+ child.kill("SIGTERM");
20912
+ finish("other");
20913
+ }, 1e4);
20914
+ child.stdout?.on("data", (c3) => {
20915
+ out += c3.toString("utf8");
20916
+ });
20917
+ child.on("error", () => {
20918
+ clearTimeout(timer);
20919
+ finish("other");
20920
+ });
20921
+ child.on("close", () => {
20922
+ clearTimeout(timer);
20923
+ finish(out.trim() === "running" ? "running" : "other");
20924
+ });
20925
+ });
20926
+ }
20927
+ async function defaultSpawnWorker(job) {
20928
+ const entry = process.env.AGENTBOX_CLI_ENTRY;
20929
+ if (!entry || !(0, import_node_fs6.existsSync)(entry)) {
20930
+ throw new Error(
20931
+ `AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
20932
+ );
20933
+ }
20934
+ await (0, import_promises17.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
20935
+ const fd = (0, import_node_fs6.openSync)(job.logPath, "a");
20936
+ const child = (0, import_node_child_process9.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
20937
+ detached: true,
20938
+ stdio: ["ignore", fd, fd],
20939
+ env: process.env
20940
+ });
20941
+ child.unref();
20942
+ try {
20943
+ (0, import_node_fs6.closeSync)(fd);
20944
+ } catch {
20945
+ }
20946
+ return typeof child.pid === "number" ? child.pid : null;
20947
+ }
20948
+ var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
20949
+
20066
20950
  // src/bin.ts
20067
20951
  var program2 = new Command();
20068
20952
  program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");
@@ -20088,10 +20972,17 @@ program2.command("serve").description("Run the HTTP relay in the foreground").op
20088
20972
  log: (line) => process.stdout.write(`agentbox-relay: ${line}
20089
20973
  `)
20090
20974
  });
20975
+ const queue = startQueueLoop({
20976
+ log: (line) => process.stdout.write(`agentbox-relay: ${line}
20977
+ `)
20978
+ });
20979
+ handle.setQueuePoke(() => {
20980
+ queue.poke?.();
20981
+ });
20091
20982
  const shutdown = (signal) => {
20092
20983
  process.stdout.write(`agentbox-relay: ${signal} \u2014 shutting down
20093
20984
  `);
20094
- autopause.stop().finally(() => handle.close()).finally(() => process.exit(0));
20985
+ Promise.allSettled([autopause.stop(), queue.stop()]).finally(() => handle.close()).finally(() => process.exit(0));
20095
20986
  };
20096
20987
  process.on("SIGTERM", () => shutdown("SIGTERM"));
20097
20988
  process.on("SIGINT", () => shutdown("SIGINT"));