@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
@@ -3033,10 +3033,16 @@ var require_commander = __commonJS({
3033
3033
  }
3034
3034
  });
3035
3035
 
3036
- // ../relay/dist/chunk-SJUIVWA3.js
3037
- var import_http, import_https, import_promises, BACKOFF_BASE_MS, BACKOFF_MAX_MS, REQUEST_TIMEOUT_MS, FAST_REQUEST_TIMEOUT_MS, FAST_MODE_DECAY_POLLS, STOPPED_TICK_MS, CloudBoxPoller, CloudBoxPollers;
3038
- var init_chunk_SJUIVWA3 = __esm({
3039
- "../relay/dist/chunk-SJUIVWA3.js"() {
3036
+ // ../relay/dist/chunk-YVJLTAM3.js
3037
+ function isConnectionLevelError(err) {
3038
+ const code = err?.code;
3039
+ if (code && CONNECTION_LEVEL_CODES.has(code)) return true;
3040
+ const msg = err instanceof Error ? err.message : String(err);
3041
+ return /\b(ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ECONNRESET|EPIPE)\b/.test(msg);
3042
+ }
3043
+ var import_http, import_https, import_promises, 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;
3044
+ var init_chunk_YVJLTAM3 = __esm({
3045
+ "../relay/dist/chunk-YVJLTAM3.js"() {
3040
3046
  "use strict";
3041
3047
  import_http = require("http");
3042
3048
  import_https = require("https");
@@ -3047,9 +3053,17 @@ var init_chunk_SJUIVWA3 = __esm({
3047
3053
  FAST_REQUEST_TIMEOUT_MS = 8e3;
3048
3054
  FAST_MODE_DECAY_POLLS = 5;
3049
3055
  STOPPED_TICK_MS = 250;
3056
+ CONNECTION_LEVEL_CODES = /* @__PURE__ */ new Set([
3057
+ "ECONNREFUSED",
3058
+ "ENOTFOUND",
3059
+ "EHOSTUNREACH",
3060
+ "ECONNRESET",
3061
+ "EPIPE"
3062
+ ]);
3050
3063
  CloudBoxPoller = class {
3051
3064
  constructor(deps) {
3052
3065
  this.deps = deps;
3066
+ this.currentPreviewUrl = deps.previewUrl;
3053
3067
  }
3054
3068
  deps;
3055
3069
  stopped = false;
@@ -3063,6 +3077,15 @@ var init_chunk_SJUIVWA3 = __esm({
3063
3077
  * within ~5 successful round-trips.
3064
3078
  */
3065
3079
  fastModePolls = 0;
3080
+ /**
3081
+ * Mutable copy of `deps.previewUrl`. We don't read `deps.previewUrl`
3082
+ * directly after construction because `recoverPreviewUrl` may hand us a
3083
+ * new URL (e.g. Hetzner reopens its SSH ControlMaster and the `-L`
3084
+ * forward gets a new ephemeral local port).
3085
+ */
3086
+ currentPreviewUrl;
3087
+ /** Guards against recovery storms — at most one recovery attempt in flight. */
3088
+ recovering = null;
3066
3089
  start() {
3067
3090
  if (this.loopPromise) return;
3068
3091
  this.loopPromise = this.run().catch((err) => {
@@ -3079,7 +3102,7 @@ var init_chunk_SJUIVWA3 = __esm({
3079
3102
  * and the in-box agent finally sees the answer.
3080
3103
  */
3081
3104
  async respond(actionId, result) {
3082
- const base = this.deps.previewUrl.replace(/\/+$/, "");
3105
+ const base = this.currentPreviewUrl.replace(/\/+$/, "");
3083
3106
  const url = new URL(`${base}/bridge/action-result`);
3084
3107
  const isHttps = url.protocol === "https:";
3085
3108
  const transport = isHttps ? import_https.request : import_http.request;
@@ -3169,6 +3192,9 @@ var init_chunk_SJUIVWA3 = __esm({
3169
3192
  this.fastModePolls = FAST_MODE_DECAY_POLLS;
3170
3193
  }
3171
3194
  this.log(`poll error: ${msg}`);
3195
+ if (this.deps.recoverPreviewUrl && isConnectionLevelError(err)) {
3196
+ await this.tryRecoverPreviewUrl();
3197
+ }
3172
3198
  await this.backoff();
3173
3199
  }
3174
3200
  if (this.currentBackoffMs === 0 && this.fastModePolls > 0) {
@@ -3181,8 +3207,33 @@ var init_chunk_SJUIVWA3 = __esm({
3181
3207
  this.currentBackoffMs = this.currentBackoffMs === 0 ? BACKOFF_BASE_MS : Math.min(this.currentBackoffMs * 2, BACKOFF_MAX_MS);
3182
3208
  await (0, import_promises.setTimeout)(this.currentBackoffMs);
3183
3209
  }
3210
+ async tryRecoverPreviewUrl() {
3211
+ if (!this.deps.recoverPreviewUrl) return;
3212
+ if (this.recovering) {
3213
+ await this.recovering;
3214
+ return;
3215
+ }
3216
+ this.recovering = (async () => {
3217
+ try {
3218
+ const next = await this.deps.recoverPreviewUrl();
3219
+ if (typeof next === "string" && next.length > 0 && next !== this.currentPreviewUrl) {
3220
+ this.log(`preview URL recovered: ${this.currentPreviewUrl} \u2192 ${next}`);
3221
+ this.currentPreviewUrl = next;
3222
+ this.currentBackoffMs = 0;
3223
+ } else if (typeof next === "string" && next === this.currentPreviewUrl) {
3224
+ this.log("preview URL recovered (unchanged)");
3225
+ this.currentBackoffMs = 0;
3226
+ }
3227
+ } catch (err) {
3228
+ this.log(`preview URL recover failed: ${err instanceof Error ? err.message : String(err)}`);
3229
+ } finally {
3230
+ this.recovering = null;
3231
+ }
3232
+ })();
3233
+ await this.recovering;
3234
+ }
3184
3235
  async pollOnce() {
3185
- const base = this.deps.previewUrl.replace(/\/+$/, "");
3236
+ const base = this.currentPreviewUrl.replace(/\/+$/, "");
3186
3237
  const url = new URL(`${base}/bridge/poll?since=${String(this.cursor)}`);
3187
3238
  const isHttps = url.protocol === "https:";
3188
3239
  const transport = isHttps ? import_https.request : import_http.request;
@@ -3750,7 +3801,7 @@ var require_cross_spawn = __commonJS({
3750
3801
  var cp = require("child_process");
3751
3802
  var parse = require_parse();
3752
3803
  var enoent = require_enoent();
3753
- function spawn9(command, args, options) {
3804
+ function spawn10(command, args, options) {
3754
3805
  const parsed = parse(command, args, options);
3755
3806
  const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
3756
3807
  enoent.hookChildProcess(spawned, parsed);
@@ -3762,8 +3813,8 @@ var require_cross_spawn = __commonJS({
3762
3813
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
3763
3814
  return result;
3764
3815
  }
3765
- module2.exports = spawn9;
3766
- module2.exports.spawn = spawn9;
3816
+ module2.exports = spawn10;
3817
+ module2.exports.spawn = spawn10;
3767
3818
  module2.exports.sync = spawnSync2;
3768
3819
  module2.exports._parse = parse;
3769
3820
  module2.exports._enoent = enoent;
@@ -11097,16 +11148,16 @@ var require_dist = __commonJS({
11097
11148
  }
11098
11149
  });
11099
11150
 
11100
- // ../relay/dist/cloud-poller-ZIWSADJB.js
11101
- var cloud_poller_ZIWSADJB_exports = {};
11102
- __export(cloud_poller_ZIWSADJB_exports, {
11151
+ // ../relay/dist/cloud-poller-SUNA6ZQC.js
11152
+ var cloud_poller_SUNA6ZQC_exports = {};
11153
+ __export(cloud_poller_SUNA6ZQC_exports, {
11103
11154
  CloudBoxPoller: () => CloudBoxPoller,
11104
11155
  CloudBoxPollers: () => CloudBoxPollers
11105
11156
  });
11106
- var init_cloud_poller_ZIWSADJB = __esm({
11107
- "../relay/dist/cloud-poller-ZIWSADJB.js"() {
11157
+ var init_cloud_poller_SUNA6ZQC = __esm({
11158
+ "../relay/dist/cloud-poller-SUNA6ZQC.js"() {
11108
11159
  "use strict";
11109
- init_chunk_SJUIVWA3();
11160
+ init_chunk_YVJLTAM3();
11110
11161
  }
11111
11162
  });
11112
11163
 
@@ -11231,12 +11282,21 @@ async function claudeSession(opts) {
11231
11282
  sessionName: opts.sessionName
11232
11283
  });
11233
11284
  }
11234
- async function claudeState(opts, state) {
11235
- return sendOneShot(opts, { op: "claude-state", state });
11285
+ async function claudeState(opts, state, payload) {
11286
+ return sendOneShot(opts, {
11287
+ op: "claude-state",
11288
+ state,
11289
+ ...payload?.plan ? { plan: payload.plan } : {},
11290
+ ...payload?.question ? { question: payload.question } : {},
11291
+ ...payload?.clearPending ? { clearPending: true } : {}
11292
+ });
11236
11293
  }
11237
11294
  async function codexState(opts, state) {
11238
11295
  return sendOneShot(opts, { op: "codex-state", state });
11239
11296
  }
11297
+ async function opencodeState(opts, state) {
11298
+ return sendOneShot(opts, { op: "opencode-state", state });
11299
+ }
11240
11300
  async function logs(opts, args) {
11241
11301
  const sock = await connect(opts);
11242
11302
  sock.write(`${JSON.stringify({ op: "logs", ...args })}
@@ -11291,6 +11351,10 @@ var CLAUDE_ACTIVITY_STATES = [
11291
11351
  "working",
11292
11352
  "idle",
11293
11353
  "waiting",
11354
+ "end-plan",
11355
+ "question",
11356
+ "compacting",
11357
+ "error",
11294
11358
  "unknown"
11295
11359
  ];
11296
11360
  var BOX_STATUS_SCHEMA = 1;
@@ -11324,18 +11388,90 @@ var claudeSessionCommand = new Command("claude-session").description("Report whe
11324
11388
  });
11325
11389
 
11326
11390
  // src/commands/claude-state.ts
11327
- var claudeStateCommand = new Command("claude-state").description("Report Claude activity state to the box supervisor (used by hooks)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).action(async (state, opts) => {
11391
+ var claudeStateCommand = new Command("claude-state").description("Report Claude activity state to the box supervisor (used by hooks)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).option("--payload-stdin", "parse Claude Code's hook JSON from stdin (PreToolUse plan/question)").option("--clear-pending", "force-clear a sticky end-plan/question state (PostToolUse cleanup)").action(async (state, opts) => {
11328
11392
  try {
11329
- if (CLAUDE_ACTIVITY_STATES.includes(state)) {
11330
- await claudeState(
11331
- { socketPath: opts.socket, timeoutMs: 1500 },
11332
- state
11333
- );
11334
- }
11393
+ if (!CLAUDE_ACTIVITY_STATES.includes(state)) {
11394
+ process.exit(0);
11395
+ }
11396
+ const typedState = state;
11397
+ const extracted = opts.payloadStdin ? await extractPayload(typedState, await readStdinJson()) : void 0;
11398
+ const payload = { ...extracted ?? {} };
11399
+ if (opts.clearPending) payload.clearPending = true;
11400
+ const hasField = payload.plan !== void 0 || payload.question !== void 0 || payload.clearPending !== void 0;
11401
+ await claudeState(
11402
+ { socketPath: opts.socket, timeoutMs: 1500 },
11403
+ typedState,
11404
+ hasField ? payload : void 0
11405
+ );
11335
11406
  } catch {
11336
11407
  }
11337
11408
  process.exit(0);
11338
11409
  });
11410
+ function extractPayload(state, raw) {
11411
+ if (!raw || typeof raw !== "object") return void 0;
11412
+ const tool = raw.tool_input ?? {};
11413
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
11414
+ if (state === "end-plan" && typeof tool.plan === "string") {
11415
+ const plan = { plan: tool.plan, capturedAt };
11416
+ return { plan };
11417
+ }
11418
+ if (state === "question" && Array.isArray(tool.questions)) {
11419
+ const questions = tool.questions.map((q) => normalizeQuestion(q)).filter((q) => q !== null);
11420
+ if (questions.length === 0) return void 0;
11421
+ const question = { questions, capturedAt };
11422
+ return { question };
11423
+ }
11424
+ return void 0;
11425
+ }
11426
+ function normalizeQuestion(raw) {
11427
+ if (!raw || typeof raw !== "object") return null;
11428
+ const q = raw;
11429
+ if (typeof q.question !== "string") return null;
11430
+ const opts = Array.isArray(q.options) ? q.options : [];
11431
+ const options = opts.map((o2) => {
11432
+ if (!o2 || typeof o2 !== "object") return null;
11433
+ const oo = o2;
11434
+ if (typeof oo.label !== "string") return null;
11435
+ const entry = { label: oo.label };
11436
+ if (typeof oo.description === "string") entry.description = oo.description;
11437
+ return entry;
11438
+ }).filter((o2) => o2 !== null);
11439
+ const out = { question: q.question, options };
11440
+ if (typeof q.header === "string") out.header = q.header;
11441
+ if (typeof q.multiSelect === "boolean") out.multiSelect = q.multiSelect;
11442
+ return out;
11443
+ }
11444
+ function readStdinJson() {
11445
+ return new Promise((resolve2) => {
11446
+ if (process.stdin.isTTY) {
11447
+ resolve2(null);
11448
+ return;
11449
+ }
11450
+ const chunks = [];
11451
+ const cap = setTimeout(() => {
11452
+ process.stdin.removeAllListeners();
11453
+ resolve2(null);
11454
+ }, 1e3);
11455
+ cap.unref();
11456
+ process.stdin.on("data", (b) => chunks.push(b));
11457
+ process.stdin.on("end", () => {
11458
+ clearTimeout(cap);
11459
+ if (chunks.length === 0) {
11460
+ resolve2(null);
11461
+ return;
11462
+ }
11463
+ try {
11464
+ resolve2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
11465
+ } catch {
11466
+ resolve2(null);
11467
+ }
11468
+ });
11469
+ process.stdin.on("error", () => {
11470
+ clearTimeout(cap);
11471
+ resolve2(null);
11472
+ });
11473
+ });
11474
+ }
11339
11475
 
11340
11476
  // src/commands/codex-state.ts
11341
11477
  var codexStateCommand = new Command("codex-state").description("Report Codex activity state to the box supervisor (used by hooks)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).action(async (state, opts) => {
@@ -11450,15 +11586,31 @@ var cpCommand = new Command("cp").description("Copy a file/dir between this box
11450
11586
  })
11451
11587
  );
11452
11588
 
11589
+ // src/commands/opencode-state.ts
11590
+ var opencodeStateCommand = new Command("opencode-state").description("Report OpenCode activity state to the box supervisor (used by the plugin)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).action(async (state, opts) => {
11591
+ try {
11592
+ if (CLAUDE_ACTIVITY_STATES.includes(state)) {
11593
+ await opencodeState(
11594
+ { socketPath: opts.socket, timeoutMs: 1500 },
11595
+ state
11596
+ );
11597
+ }
11598
+ } catch {
11599
+ }
11600
+ process.exit(0);
11601
+ });
11602
+
11453
11603
  // ../relay/dist/index.js
11454
- init_chunk_SJUIVWA3();
11604
+ init_chunk_YVJLTAM3();
11455
11605
  var import_crypto = require("crypto");
11456
11606
  var import_crypto2 = require("crypto");
11457
11607
  var import_crypto3 = require("crypto");
11608
+ var import_crypto4 = require("crypto");
11609
+ var import_child_process = require("child_process");
11458
11610
  var import_promises14 = require("fs/promises");
11459
11611
  var import_os3 = require("os");
11460
11612
  var import_path4 = require("path");
11461
- var import_child_process = require("child_process");
11613
+ var import_child_process2 = require("child_process");
11462
11614
  var import_http2 = require("http");
11463
11615
 
11464
11616
  // ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
@@ -18284,6 +18436,8 @@ function projectDockerFields(box) {
18284
18436
  webHostPort: box.webHostPort,
18285
18437
  portlessAlias: box.portlessAlias,
18286
18438
  portlessUrl: box.portlessUrl,
18439
+ portlessVncAlias: box.portlessVncAlias,
18440
+ portlessVncUrl: box.portlessVncUrl,
18287
18441
  dockerVolume: box.dockerVolume,
18288
18442
  dockerCacheShared: box.dockerCacheShared,
18289
18443
  checkpointImage: box.checkpointImage
@@ -18315,8 +18469,8 @@ var KEY_REGISTRY = [
18315
18469
  {
18316
18470
  key: "box.provider",
18317
18471
  type: "enum",
18318
- enumValues: ["docker", "daytona", "hetzner"],
18319
- description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, or Hetzner Cloud VPSes."
18472
+ enumValues: ["docker", "daytona", "hetzner", "vercel"],
18473
+ description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or Vercel Sandboxes."
18320
18474
  },
18321
18475
  {
18322
18476
  key: "box.hostSnapshot",
@@ -18346,6 +18500,12 @@ var KEY_REGISTRY = [
18346
18500
  description: "Per-provider override of `box.defaultCheckpoint` for hetzner. Wins over the global when set; set via `agentbox checkpoint set-default --provider hetzner`.",
18347
18501
  advanced: true
18348
18502
  },
18503
+ {
18504
+ key: "box.defaultCheckpointVercel",
18505
+ type: "string",
18506
+ description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
18507
+ advanced: true
18508
+ },
18349
18509
  {
18350
18510
  key: "checkpoint.maxLayers",
18351
18511
  type: "int",
@@ -18414,6 +18574,26 @@ var KEY_REGISTRY = [
18414
18574
  description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
18415
18575
  advanced: true
18416
18576
  },
18577
+ {
18578
+ key: "box.bundleDepth",
18579
+ type: "int",
18580
+ 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/)."
18581
+ },
18582
+ {
18583
+ key: "box.vercelVcpus",
18584
+ type: "int",
18585
+ 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."
18586
+ },
18587
+ {
18588
+ key: "box.vercelTimeoutMs",
18589
+ type: "int",
18590
+ 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."
18591
+ },
18592
+ {
18593
+ key: "box.vercelNetworkPolicy",
18594
+ type: "string",
18595
+ 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."
18596
+ },
18417
18597
  {
18418
18598
  key: "claude.sessionName",
18419
18599
  type: "string",
@@ -18521,6 +18701,31 @@ var KEY_REGISTRY = [
18521
18701
  type: "int",
18522
18702
  description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
18523
18703
  },
18704
+ {
18705
+ key: "queue.enabled",
18706
+ type: "bool",
18707
+ description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
18708
+ },
18709
+ {
18710
+ key: "queue.maxConcurrent",
18711
+ type: "int",
18712
+ 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>`."
18713
+ },
18714
+ {
18715
+ key: "queue.maxWorking",
18716
+ type: "int",
18717
+ 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>`."
18718
+ },
18719
+ {
18720
+ key: "queue.idleGraceSeconds",
18721
+ type: "int",
18722
+ 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."
18723
+ },
18724
+ {
18725
+ key: "cloud.useCurrentBranch",
18726
+ type: "bool",
18727
+ 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."
18728
+ },
18524
18729
  {
18525
18730
  key: "maintenance.pruneProjectConfigs",
18526
18731
  type: "bool",
@@ -18557,7 +18762,8 @@ var PROJECTS_DIR = (0, import_path2.join)(STATE_DIR2, "projects");
18557
18762
  var PROJECT_GC_COUNTER_FILE = (0, import_path3.join)(PROJECTS_DIR, ".gc.json");
18558
18763
 
18559
18764
  // ../relay/dist/index.js
18560
- var DEFAULT_RELAY_PORT = 8787;
18765
+ var import_path6 = require("path");
18766
+ var DEFAULT_BOX_RELAY_PORT = 8788;
18561
18767
  var RELAY_EVENT_RING_SIZE = 1e3;
18562
18768
  var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
18563
18769
  var HostActionQueue = class {
@@ -18870,6 +19076,254 @@ var BoxNotices = class {
18870
19076
  return this.entries.size;
18871
19077
  }
18872
19078
  };
19079
+ var DEFAULT_TTL_MS = 12e4;
19080
+ function hashRpcParams(params) {
19081
+ return (0, import_crypto4.createHash)("sha256").update(canonicalJson(params)).digest("hex");
19082
+ }
19083
+ function canonicalJson(v) {
19084
+ if (v === null) return "null";
19085
+ if (typeof v === "undefined") return "null";
19086
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
19087
+ if (typeof v === "boolean") return v ? "true" : "false";
19088
+ if (typeof v === "string") return JSON.stringify(v);
19089
+ if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
19090
+ if (typeof v === "object") {
19091
+ const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a2], [b]) => a2 < b ? -1 : a2 > b ? 1 : 0);
19092
+ return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
19093
+ }
19094
+ return "null";
19095
+ }
19096
+ var HostInitiatedTokens = class {
19097
+ store = /* @__PURE__ */ new Map();
19098
+ /**
19099
+ * Mint a fresh one-time token scoped to (boxId, method, paramsHash).
19100
+ * `paramsHash` MUST be supplied for any call surface where the box can
19101
+ * influence the eventual RPC params. Pass `null` only when there are no
19102
+ * params (no current call sites use this).
19103
+ */
19104
+ mint(boxId, method, paramsHash, ttlMs = DEFAULT_TTL_MS) {
19105
+ const token = (0, import_crypto4.randomBytes)(32).toString("hex");
19106
+ this.store.set(token, { boxId, method, paramsHash, expiresAt: Date.now() + ttlMs });
19107
+ return token;
19108
+ }
19109
+ /**
19110
+ * Returns true exactly once if `token` is a valid, unexpired token for the
19111
+ * given `(boxId, method)` AND the supplied `incomingParamsHash` matches
19112
+ * the hash bound at mint time. The token is removed on a successful match
19113
+ * (one-shot semantics). All failure modes return false — callers fall back
19114
+ * to the normal prompt path.
19115
+ */
19116
+ consume(token, boxId, method, incomingParamsHash) {
19117
+ if (!token || typeof token !== "string") return false;
19118
+ const record = this.store.get(token);
19119
+ if (!record) return false;
19120
+ if (record.expiresAt < Date.now()) {
19121
+ this.store.delete(token);
19122
+ return false;
19123
+ }
19124
+ if (record.boxId !== boxId || record.method !== method) return false;
19125
+ if (record.paramsHash !== null && record.paramsHash !== incomingParamsHash) {
19126
+ return false;
19127
+ }
19128
+ this.store.delete(token);
19129
+ return true;
19130
+ }
19131
+ /** Drop expired entries. Cheap; safe to call periodically. */
19132
+ gc() {
19133
+ const now = Date.now();
19134
+ for (const [token, record] of this.store) {
19135
+ if (record.expiresAt < now) this.store.delete(token);
19136
+ }
19137
+ }
19138
+ /** Test-only: number of live tokens. */
19139
+ size() {
19140
+ return this.store.size;
19141
+ }
19142
+ };
19143
+ var GH_PR_OPS = [
19144
+ "create",
19145
+ "view",
19146
+ "list",
19147
+ "comment",
19148
+ "review",
19149
+ "merge",
19150
+ "checkout",
19151
+ "close",
19152
+ "reopen"
19153
+ ];
19154
+ function isGhPrOp(value) {
19155
+ return GH_PR_OPS.includes(value);
19156
+ }
19157
+ var GH_PR_READ_ONLY_OPS = /* @__PURE__ */ new Set(["view", "list"]);
19158
+ function injectPrCreateHead(op, branch, args) {
19159
+ if (op !== "create") return args;
19160
+ if (!branch || branch === "HEAD") return args;
19161
+ if (hasHeadArg(args)) return args;
19162
+ return ["--head", branch, ...args];
19163
+ }
19164
+ function hasHeadArg(args) {
19165
+ return args.some((a2) => a2 === "--head" || a2.startsWith("--head=") || a2.startsWith("-H"));
19166
+ }
19167
+ function prCreateNeedsHead(op, args) {
19168
+ return op === "create" && !hasHeadArg(args);
19169
+ }
19170
+ var PR_CREATE_NO_HEAD_REFUSAL = {
19171
+ exitCode: 65,
19172
+ stdout: "",
19173
+ 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"
19174
+ };
19175
+ var GH_RPC_TIMEOUT_MS = 12e4;
19176
+ var GH_READY_CACHE_TTL_MS = 6e4;
19177
+ var ghReadyCache;
19178
+ async function assertGhReady() {
19179
+ const now = Date.now();
19180
+ if (ghReadyCache && ghReadyCache.expiresAt > now) {
19181
+ return ghReadyCache.result;
19182
+ }
19183
+ const result = await probeGh();
19184
+ ghReadyCache = { result, expiresAt: now + GH_READY_CACHE_TTL_MS };
19185
+ return result;
19186
+ }
19187
+ async function probeGh() {
19188
+ const version = await runHostGh(["--version"], process.cwd(), 1e4);
19189
+ if (version.exitCode === 127 || /ENOENT/.test(version.stderr)) {
19190
+ return {
19191
+ exitCode: 127,
19192
+ stdout: "",
19193
+ stderr: "gh not installed on host (https://cli.github.com)\n"
19194
+ };
19195
+ }
19196
+ if (version.exitCode !== 0) {
19197
+ return {
19198
+ exitCode: version.exitCode,
19199
+ stdout: "",
19200
+ stderr: `gh --version failed: ${version.stderr || version.stdout}`.trimEnd() + "\n"
19201
+ };
19202
+ }
19203
+ const auth = await runHostGh(["auth", "status"], process.cwd(), 15e3);
19204
+ if (auth.exitCode !== 0) {
19205
+ return {
19206
+ exitCode: 4,
19207
+ stdout: "",
19208
+ stderr: "gh not authenticated on host (run `gh auth login`)\n"
19209
+ };
19210
+ }
19211
+ return null;
19212
+ }
19213
+ function runHostGh(args, cwd, timeoutMs = GH_RPC_TIMEOUT_MS) {
19214
+ return new Promise((resolve2) => {
19215
+ const child = (0, import_child_process.spawn)("gh", args, {
19216
+ cwd,
19217
+ env: process.env,
19218
+ stdio: ["ignore", "pipe", "pipe"]
19219
+ });
19220
+ let stdout = "";
19221
+ let stderr = "";
19222
+ let settled = false;
19223
+ const finish = (exitCode) => {
19224
+ if (settled) return;
19225
+ settled = true;
19226
+ resolve2({ exitCode, stdout, stderr });
19227
+ };
19228
+ const timer = setTimeout(() => {
19229
+ child.kill("SIGTERM");
19230
+ stderr += `
19231
+ relay: gh command timed out after ${String(timeoutMs)}ms
19232
+ `;
19233
+ finish(124);
19234
+ }, timeoutMs);
19235
+ child.stdout?.on("data", (chunk) => {
19236
+ stdout += chunk.toString("utf8");
19237
+ });
19238
+ child.stderr?.on("data", (chunk) => {
19239
+ stderr += chunk.toString("utf8");
19240
+ });
19241
+ child.on("error", (err) => {
19242
+ clearTimeout(timer);
19243
+ const code = err.code;
19244
+ stderr += String(err.message ?? err);
19245
+ finish(code === "ENOENT" ? 127 : 1);
19246
+ });
19247
+ child.on("close", (code) => {
19248
+ clearTimeout(timer);
19249
+ finish(code ?? -1);
19250
+ });
19251
+ });
19252
+ }
19253
+ async function checkoutGuards(hostMainRepo, registeredBranches) {
19254
+ const status2 = await runGitProbe(["-C", hostMainRepo, "status", "--porcelain"]);
19255
+ if (status2.exitCode !== 0) {
19256
+ return {
19257
+ exitCode: status2.exitCode,
19258
+ stdout: "",
19259
+ stderr: `gh pr checkout: failed to inspect host repo: ${status2.stderr || status2.stdout}`.trimEnd() + "\n"
19260
+ };
19261
+ }
19262
+ if (status2.stdout.trim().length > 0) {
19263
+ return {
19264
+ exitCode: 12,
19265
+ stdout: "",
19266
+ stderr: `gh pr checkout: ${hostMainRepo} has uncommitted changes; refusing to switch branches
19267
+ `
19268
+ };
19269
+ }
19270
+ const head = await runGitProbe(["-C", hostMainRepo, "rev-parse", "--abbrev-ref", "HEAD"]);
19271
+ if (head.exitCode !== 0) {
19272
+ return {
19273
+ exitCode: head.exitCode,
19274
+ stdout: "",
19275
+ stderr: `gh pr checkout: failed to resolve HEAD: ${head.stderr || head.stdout}`.trimEnd() + "\n"
19276
+ };
19277
+ }
19278
+ const currentBranch = head.stdout.trim();
19279
+ if (registeredBranches.includes(currentBranch)) {
19280
+ return {
19281
+ exitCode: 12,
19282
+ stdout: "",
19283
+ stderr: `gh pr checkout: ${hostMainRepo} is on registered box branch ${currentBranch}; refusing (would corrupt the bind-mounted box HEAD)
19284
+ `
19285
+ };
19286
+ }
19287
+ return null;
19288
+ }
19289
+ function runGitProbe(args) {
19290
+ return new Promise((resolve2) => {
19291
+ const child = (0, import_child_process.spawn)("git", args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
19292
+ let stdout = "";
19293
+ let stderr = "";
19294
+ child.stdout?.on("data", (c3) => {
19295
+ stdout += c3.toString("utf8");
19296
+ });
19297
+ child.stderr?.on("data", (c3) => {
19298
+ stderr += c3.toString("utf8");
19299
+ });
19300
+ child.on("error", (err) => {
19301
+ resolve2({ exitCode: 127, stdout, stderr: stderr + String(err.message ?? err) });
19302
+ });
19303
+ child.on("close", (code) => {
19304
+ resolve2({ exitCode: code ?? -1, stdout, stderr });
19305
+ });
19306
+ });
19307
+ }
19308
+ function refuseMergeBypass(op) {
19309
+ if (op !== "merge") return null;
19310
+ if (process.env["AGENTBOX_PROMPT"] !== "off") return null;
19311
+ if (process.env["AGENTBOX_GH_FORCE"] === "1") return null;
19312
+ return {
19313
+ exitCode: 10,
19314
+ stdout: "",
19315
+ stderr: "gh pr merge: AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1 (merge is irreversible)\n"
19316
+ };
19317
+ }
19318
+ function refuseCheckoutByDefault(op) {
19319
+ if (op !== "checkout") return null;
19320
+ if (process.env["AGENTBOX_GH_PR_CHECKOUT"] === "allow") return null;
19321
+ return {
19322
+ exitCode: 13,
19323
+ stdout: "",
19324
+ stderr: "gh pr checkout: disabled by default; set AGENTBOX_GH_PR_CHECKOUT=allow to enable\n"
19325
+ };
19326
+ }
18873
19327
  function sanitizeMnemonic(raw) {
18874
19328
  return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
18875
19329
  }
@@ -18919,36 +19373,43 @@ var BoxStatusStore = class {
18919
19373
  async function resolveCloudBackend(name) {
18920
19374
  if (name === "daytona") {
18921
19375
  const pkg = "@agentbox/sandbox-daytona";
18922
- try {
18923
- const mod = await import(pkg);
18924
- return mod.daytonaBackend;
18925
- } catch (err) {
18926
- const msg = err instanceof Error ? err.message : String(err);
18927
- if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18928
- throw new Error(
18929
- `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18930
- );
18931
- }
18932
- throw err;
18933
- }
19376
+ return loadCloudBackend(pkg, async () => (await import(pkg)).daytonaBackend);
18934
19377
  }
18935
19378
  if (name === "hetzner") {
18936
19379
  const pkg = "@agentbox/sandbox-hetzner";
18937
- try {
18938
- const mod = await import(pkg);
18939
- return mod.hetznerBackend;
18940
- } catch (err) {
18941
- const msg = err instanceof Error ? err.message : String(err);
18942
- if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18943
- throw new Error(
18944
- `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18945
- );
18946
- }
18947
- throw err;
18948
- }
19380
+ return loadCloudBackend(pkg, async () => (await import(pkg)).hetznerBackend);
19381
+ }
19382
+ if (name === "vercel") {
19383
+ const pkg = "@agentbox/sandbox-vercel";
19384
+ return loadCloudBackend(pkg, async () => (await import(pkg)).vercelBackend);
18949
19385
  }
18950
19386
  throw new Error(`no host executor for cloud backend '${name}'`);
18951
19387
  }
19388
+ async function loadCloudBackend(pkg, load2) {
19389
+ try {
19390
+ return await load2();
19391
+ } catch (err) {
19392
+ const msg = err instanceof Error ? err.message : String(err);
19393
+ if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
19394
+ throw new Error(
19395
+ `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
19396
+ );
19397
+ }
19398
+ throw err;
19399
+ }
19400
+ }
19401
+ async function refreshCloudPreviewUrl(backendName, boxId, port) {
19402
+ try {
19403
+ const backend = await resolveCloudBackend(backendName);
19404
+ if (!backend.refreshPreviewUrl) return null;
19405
+ const lookup = await lookupCloudBox(boxId);
19406
+ const handle = { sandboxId: lookup.cloudSandboxId };
19407
+ const url = await backend.refreshPreviewUrl(handle, port);
19408
+ return url.url;
19409
+ } catch {
19410
+ return null;
19411
+ }
19412
+ }
18952
19413
  async function executeCloudAction(action, deps) {
18953
19414
  const log = deps.log ?? (() => {
18954
19415
  });
@@ -18968,6 +19429,17 @@ async function executeCloudAction(action, deps) {
18968
19429
  if (action.method === "browser.open.mirror") {
18969
19430
  return runBrowserOpenMirror(action, deps);
18970
19431
  }
19432
+ if (action.method.startsWith("gh.pr.")) {
19433
+ return runGhPrRpc(action, deps);
19434
+ }
19435
+ if (action.method === "git.clone" || action.method === "gh.repo.clone") {
19436
+ return {
19437
+ exitCode: 64,
19438
+ stdout: "",
19439
+ 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.
19440
+ `
19441
+ };
19442
+ }
18971
19443
  return {
18972
19444
  exitCode: 1,
18973
19445
  stdout: "",
@@ -18975,6 +19447,99 @@ async function executeCloudAction(action, deps) {
18975
19447
  `
18976
19448
  };
18977
19449
  }
19450
+ async function runGhPrRpc(action, deps) {
19451
+ const op = action.method.slice("gh.pr.".length);
19452
+ if (!isGhPrOp(op)) {
19453
+ return {
19454
+ exitCode: 64,
19455
+ stdout: "",
19456
+ stderr: `unknown gh.pr.* op: ${op}
19457
+ `
19458
+ };
19459
+ }
19460
+ const mergeBypass = refuseMergeBypass(op);
19461
+ if (mergeBypass) return mergeBypass;
19462
+ const checkoutOptIn = refuseCheckoutByDefault(op);
19463
+ if (checkoutOptIn) return checkoutOptIn;
19464
+ const params = action.params ?? {};
19465
+ const args = Array.isArray(params.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
19466
+ const ghReady = await assertGhReady();
19467
+ if (ghReady) return ghReady;
19468
+ const lookup = await lookupCloudBox(deps.boxId);
19469
+ if (op === "checkout") {
19470
+ const guard = await checkoutGuards(lookup.workspacePath, []);
19471
+ if (guard) return guard;
19472
+ }
19473
+ const tokenClaimedGhCloud = typeof params.hostInitiated === "string";
19474
+ const incomingHashGhCloud = hashRpcParams(params);
19475
+ const hostInitiatedGhOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && (deps.hostInitiatedTokens?.consume(
19476
+ params.hostInitiated,
19477
+ deps.boxId,
19478
+ `gh.pr.${op}`,
19479
+ incomingHashGhCloud
19480
+ ) ?? false);
19481
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && !hostInitiatedGhOk) {
19482
+ return {
19483
+ exitCode: 10,
19484
+ stdout: "",
19485
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19486
+ };
19487
+ }
19488
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedGhOk && deps.prompts && deps.subscribers) {
19489
+ const detail = args.join(" ").slice(0, 200);
19490
+ const ctx = {
19491
+ kind: "confirm",
19492
+ message: `Allow gh pr ${op} from cloud box ${deps.boxName ?? deps.boxId}?`,
19493
+ detail,
19494
+ defaultAnswer: "n",
19495
+ context: {
19496
+ command: `gh pr ${op}`,
19497
+ cwd: params.path,
19498
+ argv: args
19499
+ }
19500
+ };
19501
+ const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
19502
+ if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
19503
+ const noSubMode = (process.env["AGENTBOX_GH_NO_SUB"] ?? "deny").toLowerCase();
19504
+ if (noSubMode === "deny") {
19505
+ return {
19506
+ exitCode: 10,
19507
+ stdout: "",
19508
+ stderr: "denied automatically \u2014 no attached wrapper to confirm. Attach `agentbox claude` (or similar) and retry, or set AGENTBOX_GH_NO_SUB=allow.\n"
19509
+ };
19510
+ }
19511
+ if (noSubMode === "allow") {
19512
+ deps.log?.(`gh.pr.${op} auto-approved (no subscribers, AGENTBOX_GH_NO_SUB=allow)`);
19513
+ } else {
19514
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx, {
19515
+ ttlMs: 5 * 60 * 1e3
19516
+ });
19517
+ if (verdict.answer !== "y") {
19518
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19519
+ }
19520
+ }
19521
+ } else {
19522
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx);
19523
+ if (verdict.answer !== "y") {
19524
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19525
+ }
19526
+ }
19527
+ }
19528
+ let finalArgs = args;
19529
+ if (op === "create" && !args.some((a2) => a2 === "--head" || a2.startsWith("--head="))) {
19530
+ const backend = await resolveCloudBackend(deps.backendName);
19531
+ const handle = { sandboxId: lookup.cloudSandboxId };
19532
+ const containerPath = params.path ?? "/workspace";
19533
+ const branchProbe = await backend.exec(
19534
+ handle,
19535
+ `git -C ${shellQuote(containerPath)} rev-parse --abbrev-ref HEAD`
19536
+ );
19537
+ const branch = branchProbe.exitCode === 0 ? (branchProbe.stdout ?? "").trim() : "";
19538
+ finalArgs = injectPrCreateHead(op, branch, args);
19539
+ }
19540
+ if (prCreateNeedsHead(op, finalArgs)) return PR_CREATE_NO_HEAD_REFUSAL;
19541
+ return runHostGh(["pr", op, ...finalArgs], lookup.workspacePath);
19542
+ }
18978
19543
  async function runBrowserOpenMirror(action, deps) {
18979
19544
  const params = action.params ?? {};
18980
19545
  const url = typeof params.url === "string" ? params.url.trim() : "";
@@ -19001,8 +19566,8 @@ async function runBrowserOpenMirror(action, deps) {
19001
19566
  { ttlMs: TTL_MS }
19002
19567
  );
19003
19568
  if (verdict.answer === "y" && !verdict.cancelled) {
19004
- const { spawn: spawn32 } = await import("child_process");
19005
- const child = spawn32("open", [url], { stdio: "ignore", detached: true });
19569
+ const { spawn: spawn52 } = await import("child_process");
19570
+ const child = spawn52("open", [url], { stdio: "ignore", detached: true });
19006
19571
  child.unref();
19007
19572
  }
19008
19573
  } catch (err) {
@@ -19173,7 +19738,23 @@ async function runGitRpc(action, deps) {
19173
19738
  stderr: `failed to resolve branch in sandbox ${containerPath}: ${branchProbe.stderr || branch}`
19174
19739
  };
19175
19740
  }
19176
- if (action.method === "git.push" && deps.prompts && deps.subscribers) {
19741
+ const isAgentboxBranch = branch.startsWith("agentbox/");
19742
+ const tokenClaimedGit = typeof params.hostInitiated === "string";
19743
+ const incomingHashGit = hashRpcParams(params);
19744
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimedGit && (deps.hostInitiatedTokens?.consume(
19745
+ params.hostInitiated,
19746
+ deps.boxId,
19747
+ "git.push",
19748
+ incomingHashGit
19749
+ ) ?? false);
19750
+ if (action.method === "git.push" && !isAgentboxBranch && tokenClaimedGit && !hostInitiatedOk) {
19751
+ return {
19752
+ exitCode: 10,
19753
+ stdout: "",
19754
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19755
+ };
19756
+ }
19757
+ if (action.method === "git.push" && !isAgentboxBranch && !hostInitiatedOk && deps.prompts && deps.subscribers) {
19177
19758
  const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
19178
19759
  if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
19179
19760
  const noSubMode = (process.env["AGENTBOX_GIT_PUSH_NO_SUB"] ?? "deny").toLowerCase();
@@ -19248,10 +19829,45 @@ async function runGitRpc(action, deps) {
19248
19829
  for (const a2 of params.args) if (typeof a2 === "string") argv.push(a2);
19249
19830
  }
19250
19831
  const push = await execa("git", argv, { reject: false });
19832
+ let pushStderr = push.stderr ?? "";
19833
+ if ((push.exitCode ?? 1) === 0 && !branch.startsWith("agentbox/")) {
19834
+ try {
19835
+ const sha = await execa(
19836
+ "git",
19837
+ ["-C", lookup.workspacePath, "rev-parse", branch],
19838
+ { reject: false }
19839
+ );
19840
+ const shaText = (sha.stdout ?? "").trim();
19841
+ if (sha.exitCode === 0 && shaText.length > 0) {
19842
+ const updateRef = await backend.exec(
19843
+ handle,
19844
+ `git -C ${shellQuote(containerPath)} update-ref refs/remotes/${remote2}/${branch} ${shellQuote(shaText)}`
19845
+ );
19846
+ if (updateRef.exitCode !== 0) {
19847
+ pushStderr += `
19848
+ relay: post-push in-box update-ref refs/remotes/${remote2}/${branch} failed: ${updateRef.stderr || updateRef.stdout}`;
19849
+ }
19850
+ const setUpstream = await backend.exec(
19851
+ handle,
19852
+ `git -C ${shellQuote(containerPath)} branch --set-upstream-to=${remote2}/${branch} ${shellQuote(branch)}`
19853
+ );
19854
+ if (setUpstream.exitCode !== 0) {
19855
+ pushStderr += `
19856
+ relay: post-push in-box --set-upstream-to=${remote2}/${branch} failed: ${setUpstream.stderr || setUpstream.stdout}`;
19857
+ }
19858
+ } else {
19859
+ pushStderr += `
19860
+ relay: post-push rev-parse ${branch} failed on host; skipping in-box origin/upstream sync`;
19861
+ }
19862
+ } catch (err) {
19863
+ pushStderr += `
19864
+ relay: post-push in-box origin/upstream sync threw: ${err instanceof Error ? err.message : String(err)}`;
19865
+ }
19866
+ }
19251
19867
  return {
19252
19868
  exitCode: push.exitCode ?? 1,
19253
19869
  stdout: push.stdout ?? "",
19254
- stderr: push.stderr ?? ""
19870
+ stderr: pushStderr
19255
19871
  };
19256
19872
  }
19257
19873
  const remote = params.remote ?? "origin";
@@ -19363,6 +19979,8 @@ function createRelayServer(opts) {
19363
19979
  const prompts = new PendingPrompts();
19364
19980
  const subscribers = new PromptSubscribers();
19365
19981
  const notices = new BoxNotices(subscribers);
19982
+ const hostInitiatedTokens = new HostInitiatedTokens();
19983
+ let queuePoke = null;
19366
19984
  const host = opts.host ?? "0.0.0.0";
19367
19985
  const mode = opts.mode ?? "host";
19368
19986
  const hostActions = mode === "box" ? new HostActionQueue() : null;
@@ -19373,7 +19991,7 @@ function createRelayServer(opts) {
19373
19991
  let pollers = null;
19374
19992
  async function getPollers() {
19375
19993
  if (!pollers) {
19376
- const mod = await Promise.resolve().then(() => (init_cloud_poller_ZIWSADJB(), cloud_poller_ZIWSADJB_exports));
19994
+ const mod = await Promise.resolve().then(() => (init_cloud_poller_SUNA6ZQC(), cloud_poller_SUNA6ZQC_exports));
19377
19995
  pollers = new mod.CloudBoxPollers();
19378
19996
  }
19379
19997
  return pollers;
@@ -19390,7 +20008,13 @@ function createRelayServer(opts) {
19390
20008
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "relay"}`);
19391
20009
  const route = `${req.method ?? "GET"} ${url.pathname}`;
19392
20010
  if (route === "GET /healthz") {
19393
- send(res, 200, { ok: true, boxes: registry.size(), events: events.size() });
20011
+ send(res, 200, {
20012
+ ok: true,
20013
+ boxes: registry.size(),
20014
+ events: events.size(),
20015
+ pid: process.pid,
20016
+ cliEntry: Boolean(process.env.AGENTBOX_CLI_ENTRY)
20017
+ });
19394
20018
  return;
19395
20019
  }
19396
20020
  if (url.pathname.startsWith("/bridge/")) {
@@ -19495,21 +20119,36 @@ function createRelayServer(opts) {
19495
20119
  if (body.method === "git.push" || body.method === "git.fetch") {
19496
20120
  if (body.method === "git.push") {
19497
20121
  const params = body.params;
19498
- const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
19499
- kind: "confirm",
19500
- message: `Allow git push from box ${reg.name}?`,
19501
- detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
19502
- defaultAnswer: "n",
19503
- context: {
19504
- command: "git push",
19505
- cwd: params?.path,
19506
- argv: params?.args
19507
- }
19508
- });
19509
- if (verdict.answer !== "y") {
19510
- send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
20122
+ const worktree = resolveWorktree(reg, params?.path ?? "/workspace");
20123
+ const isAgentboxBranch = worktree?.branch.startsWith("agentbox/") ?? false;
20124
+ const tokenClaimed = typeof params?.hostInitiated === "string";
20125
+ const incomingHash = hashRpcParams(params);
20126
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimed && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, "git.push", incomingHash);
20127
+ if (!isAgentboxBranch && tokenClaimed && !hostInitiatedOk) {
20128
+ send(res, 500, {
20129
+ exitCode: 10,
20130
+ stdout: "",
20131
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
20132
+ });
19511
20133
  return;
19512
20134
  }
20135
+ if (!isAgentboxBranch && !hostInitiatedOk) {
20136
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
20137
+ kind: "confirm",
20138
+ message: `Allow git push from box ${reg.name}?`,
20139
+ detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
20140
+ defaultAnswer: "n",
20141
+ context: {
20142
+ command: "git push",
20143
+ cwd: params?.path,
20144
+ argv: params?.args
20145
+ }
20146
+ });
20147
+ if (verdict.answer !== "y") {
20148
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
20149
+ return;
20150
+ }
20151
+ }
19513
20152
  }
19514
20153
  const result = await handleGitRpc(reg, body.method, body.params);
19515
20154
  const status2 = result.exitCode === 0 ? 200 : 500;
@@ -19542,6 +20181,33 @@ function createRelayServer(opts) {
19542
20181
  send(res, status2, result);
19543
20182
  return;
19544
20183
  }
20184
+ if (body.method.startsWith("gh.pr.")) {
20185
+ const op = body.method.slice("gh.pr.".length);
20186
+ if (!isGhPrOp(op)) {
20187
+ send(res, 400, { error: `unknown gh.pr.* op: ${op}` });
20188
+ return;
20189
+ }
20190
+ const result = await handleGhPrRpc(
20191
+ op,
20192
+ reg,
20193
+ body.params,
20194
+ prompts,
20195
+ subscribers,
20196
+ hostInitiatedTokens
20197
+ );
20198
+ const status2 = result.exitCode === 0 ? 200 : 500;
20199
+ send(res, status2, result);
20200
+ return;
20201
+ }
20202
+ if (body.method === "git.clone" || body.method === "gh.repo.clone") {
20203
+ send(res, 501, {
20204
+ exitCode: 64,
20205
+ stdout: "",
20206
+ 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.
20207
+ `
20208
+ });
20209
+ return;
20210
+ }
19545
20211
  if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
19546
20212
  const params = body.params;
19547
20213
  const kind = body.method.split(".")[1] ?? "workspace";
@@ -19678,6 +20344,7 @@ function createRelayServer(opts) {
19678
20344
  boxName: reg.name,
19679
20345
  prompts,
19680
20346
  subscribers,
20347
+ hostInitiatedTokens,
19681
20348
  log
19682
20349
  });
19683
20350
  await respond(result);
@@ -19690,6 +20357,11 @@ function createRelayServer(opts) {
19690
20357
  });
19691
20358
  }
19692
20359
  } : void 0,
20360
+ // Self-heal a dead preview transport (hetzner SSH `-L` after a
20361
+ // ControlMaster death). The relay strips the `cloud:` prefix
20362
+ // the cloud-provider tags onto BoxRecord.container — what the
20363
+ // backend's `get(sandboxId)` expects is the bare sandbox id.
20364
+ recoverPreviewUrl: reg.backend ? async () => refreshCloudPreviewUrl(reg.backend, reg.boxId, DEFAULT_BOX_RELAY_PORT) : void 0,
19693
20365
  logger: log
19694
20366
  });
19695
20367
  } catch (err) {
@@ -19804,6 +20476,27 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19804
20476
  send(res, 204, null);
19805
20477
  return;
19806
20478
  }
20479
+ if (route === "POST /admin/host-initiated/mint") {
20480
+ const body = await readJsonBody(req);
20481
+ if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.method !== "string" || body.method.length === 0) {
20482
+ send(res, 400, { error: "expected {boxId, method, paramsHash, ttlMs?}" });
20483
+ return;
20484
+ }
20485
+ let paramsHash;
20486
+ if (body.paramsHash === null || body.paramsHash === void 0) {
20487
+ paramsHash = null;
20488
+ } else if (typeof body.paramsHash === "string" && /^[0-9a-f]{64}$/.test(body.paramsHash)) {
20489
+ paramsHash = body.paramsHash;
20490
+ } else {
20491
+ send(res, 400, { error: "paramsHash must be a 64-hex sha256 string or null" });
20492
+ return;
20493
+ }
20494
+ const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
20495
+ const token = hostInitiatedTokens.mint(body.boxId, body.method, paramsHash, ttlMs);
20496
+ log(`host-initiated-mint box=${body.boxId} method=${body.method} paramsBound=${paramsHash !== null}`);
20497
+ send(res, 200, { token });
20498
+ return;
20499
+ }
19807
20500
  if (route === "POST /admin/notices/set") {
19808
20501
  const body = await readJsonBody(req);
19809
20502
  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) {
@@ -19816,6 +20509,17 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19816
20509
  send(res, 200, { id });
19817
20510
  return;
19818
20511
  }
20512
+ if (route === "POST /admin/queue/enqueue") {
20513
+ const body = await readJsonBody(req);
20514
+ if (!body || typeof body.id !== "string" || body.id.length === 0) {
20515
+ send(res, 400, { error: "expected {id}" });
20516
+ return;
20517
+ }
20518
+ log(`queue-enqueue id=${body.id}`);
20519
+ queuePoke?.();
20520
+ send(res, 204, null);
20521
+ return;
20522
+ }
19819
20523
  if (route === "POST /admin/notices/clear") {
19820
20524
  const body = await readJsonBody(req);
19821
20525
  if (!body || typeof body.id !== "string" || body.id.length === 0) {
@@ -19852,6 +20556,9 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19852
20556
  notices,
19853
20557
  hostActions: hostActions ?? void 0,
19854
20558
  url: `http://${host}:${String(opts.port)}`,
20559
+ setQueuePoke: (fn) => {
20560
+ queuePoke = fn;
20561
+ },
19855
20562
  close: async () => {
19856
20563
  if (pollers) await pollers.stopAll();
19857
20564
  await new Promise((resolve2, reject) => {
@@ -19903,7 +20610,71 @@ async function handleGitRpc(reg, method, params) {
19903
20610
  if (typeof a2 === "string") argv.push(a2);
19904
20611
  }
19905
20612
  }
19906
- return runHostCommand(argv);
20613
+ const result = await runHostCommand(argv);
20614
+ if (method === "git.push" && result.exitCode === 0 && !worktree.branch.startsWith("agentbox/")) {
20615
+ await runHostCommand([
20616
+ "git",
20617
+ "-C",
20618
+ worktree.hostMainRepo,
20619
+ "branch",
20620
+ `--set-upstream-to=${remote}/${worktree.branch}`,
20621
+ worktree.branch
20622
+ ]);
20623
+ }
20624
+ return result;
20625
+ }
20626
+ async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiatedTokens) {
20627
+ const mergeBypass = refuseMergeBypass(op);
20628
+ if (mergeBypass) return mergeBypass;
20629
+ const checkoutOptIn = refuseCheckoutByDefault(op);
20630
+ if (checkoutOptIn) return checkoutOptIn;
20631
+ const containerPath = params?.path ?? "/workspace";
20632
+ const worktree = resolveWorktree(reg, containerPath);
20633
+ if (!worktree) {
20634
+ return {
20635
+ exitCode: 64,
20636
+ stdout: "",
20637
+ stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
20638
+ };
20639
+ }
20640
+ const ghReady = await assertGhReady();
20641
+ if (ghReady) return ghReady;
20642
+ const args = Array.isArray(params?.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
20643
+ if (op === "checkout") {
20644
+ const branches = (reg.worktrees ?? []).map((w) => w.branch);
20645
+ const guard = await checkoutGuards(worktree.hostMainRepo, branches);
20646
+ if (guard) return guard;
20647
+ }
20648
+ const tokenClaimedGh = typeof params?.hostInitiated === "string";
20649
+ const incomingHashGh = hashRpcParams(params);
20650
+ const hostInitiatedOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, `gh.pr.${op}`, incomingHashGh);
20651
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && !hostInitiatedOk) {
20652
+ return {
20653
+ exitCode: 10,
20654
+ stdout: "",
20655
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
20656
+ };
20657
+ }
20658
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedOk) {
20659
+ const detail = args.join(" ").slice(0, 200);
20660
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
20661
+ kind: "confirm",
20662
+ message: `Allow gh pr ${op} from box ${reg.name}?`,
20663
+ detail,
20664
+ defaultAnswer: "n",
20665
+ context: {
20666
+ command: `gh pr ${op}`,
20667
+ cwd: containerPath,
20668
+ argv: args
20669
+ }
20670
+ });
20671
+ if (verdict.answer !== "y") {
20672
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
20673
+ }
20674
+ }
20675
+ const finalArgs = injectPrCreateHead(op, worktree.branch, args);
20676
+ if (prCreateNeedsHead(op, finalArgs)) return PR_CREATE_NO_HEAD_REFUSAL;
20677
+ return runHostGh(["pr", op, ...finalArgs], worktree.hostMainRepo);
19907
20678
  }
19908
20679
  async function handleCpRpc(reg, method, params) {
19909
20680
  const entry = process.env.AGENTBOX_CLI_ENTRY;
@@ -19964,7 +20735,7 @@ function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
19964
20735
  resolve2({ exitCode: 64, stdout: "", stderr: "empty command" });
19965
20736
  return;
19966
20737
  }
19967
- const child = (0, import_child_process.spawn)(cmd, rest, {
20738
+ const child = (0, import_child_process2.spawn)(cmd, rest, {
19968
20739
  env: process.env,
19969
20740
  stdio: ["ignore", "pipe", "pipe"]
19970
20741
  });
@@ -20011,6 +20782,8 @@ async function startRelayServer(opts) {
20011
20782
  });
20012
20783
  return handle;
20013
20784
  }
20785
+ var QUEUE_DIR = (0, import_path6.join)(STATE_DIR, "queue");
20786
+ var QUEUE_LOGS_DIR = (0, import_path6.join)(STATE_DIR, "logs");
20014
20787
 
20015
20788
  // src/config.ts
20016
20789
  var import_promises16 = require("fs/promises");
@@ -20321,7 +21094,7 @@ function assertBool(raw, where) {
20321
21094
  if (typeof raw !== "boolean") throw new ConfigError(`${where} must be a boolean`);
20322
21095
  return raw;
20323
21096
  }
20324
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults"]);
21097
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults", "carry"]);
20325
21098
  function validateUnitGraph(tasks, services) {
20326
21099
  const names = /* @__PURE__ */ new Set();
20327
21100
  for (const t of tasks) {
@@ -20441,8 +21214,95 @@ function describeCommand(cmd) {
20441
21214
  return Array.isArray(cmd) ? cmd.join(" ") : cmd;
20442
21215
  }
20443
21216
 
20444
- // src/supervisor.ts
21217
+ // src/codex-scraper.ts
20445
21218
  var import_node_child_process7 = require("child_process");
21219
+ var DEFAULT_INTERVAL_MS = 1e3;
21220
+ var DEFAULT_SESSION = "codex";
21221
+ var PATTERNS = [
21222
+ // Permission / approval prompts (highest priority — these mean codex is blocked).
21223
+ { re: /Hooks need review|Trust all and continue/i, state: "waiting" },
21224
+ { re: /Do you trust the contents of this directory/i, state: "waiting" },
21225
+ { re: /Allow this command\?|Approve this (command|tool)\?|Press y\/n|\[Y\/n\]/m, state: "waiting" },
21226
+ { re: /Waiting for (your |user )?(response|input|approval|permission)/i, state: "waiting" },
21227
+ // Compaction (codex's `/compact` command and auto-compaction).
21228
+ { re: /Compacting (conversation|context)|Summariz(e|ing) (the )?conversation/i, state: "compacting" },
21229
+ // Failure / fatal-error frames.
21230
+ { re: /\bError:|\bFailed:|^Traceback /m, state: "error" },
21231
+ // Active work signals — pinned to specific codex TUI fragments to avoid
21232
+ // matching every line of english that contains "working" or "running"
21233
+ // (e.g. the directory-trust prompt's "Working with untrusted contents"
21234
+ // warning is NOT a working state).
21235
+ {
21236
+ re: /\b(Thinking\.\.\.|Worked for \d|Streaming response|tool call \w|Running command|Generating response|Reasoning\.\.\.|Editing \w)/m,
21237
+ state: "working"
21238
+ },
21239
+ // Idle: codex shows a status line `gpt-5.5 high · /workspace` (or similar
21240
+ // model · cwd footer) at the bottom of the input prompt when ready for
21241
+ // input. Lower priority than every "busy" pattern above so an in-flight
21242
+ // turn that still shows the footer correctly registers as working.
21243
+ { re: /gpt-\d+(\.\d+)?(-\w+)?\s+(low|medium|high|xhigh)\b|OpenAI Codex \(v\d/i, state: "idle" }
21244
+ ];
21245
+ function startCodexScraper(opts) {
21246
+ const sessionName = opts.sessionName ?? DEFAULT_SESSION;
21247
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
21248
+ const capture = opts.capturePane ?? defaultCapturePane;
21249
+ let lastState = null;
21250
+ let lastSessionPresent = false;
21251
+ let stopped = false;
21252
+ const tick = async () => {
21253
+ if (stopped) return;
21254
+ try {
21255
+ const pane = await capture(sessionName);
21256
+ if (pane === null) {
21257
+ lastSessionPresent = false;
21258
+ return;
21259
+ }
21260
+ if (!lastSessionPresent) {
21261
+ opts.reporter.setCodexState("idle");
21262
+ lastState = "idle";
21263
+ lastSessionPresent = true;
21264
+ }
21265
+ const matched = matchState(pane);
21266
+ if (matched !== null && matched !== lastState) {
21267
+ opts.reporter.setCodexState(matched);
21268
+ lastState = matched;
21269
+ }
21270
+ } catch {
21271
+ }
21272
+ };
21273
+ const timer = setInterval(() => void tick(), intervalMs);
21274
+ timer.unref();
21275
+ void tick();
21276
+ return {
21277
+ stop() {
21278
+ stopped = true;
21279
+ clearInterval(timer);
21280
+ }
21281
+ };
21282
+ }
21283
+ function matchState(pane) {
21284
+ for (const { re, state } of PATTERNS) {
21285
+ if (re.test(pane)) return state;
21286
+ }
21287
+ return null;
21288
+ }
21289
+ function defaultCapturePane(sessionName) {
21290
+ return new Promise((resolve2) => {
21291
+ const child = (0, import_node_child_process7.spawn)("tmux", ["capture-pane", "-p", "-t", sessionName], {
21292
+ stdio: ["ignore", "pipe", "ignore"]
21293
+ });
21294
+ let stdout = "";
21295
+ child.stdout.on("data", (b) => stdout += b.toString("utf8"));
21296
+ child.on("error", () => resolve2(null));
21297
+ child.on("close", (code) => {
21298
+ if (code === 0) resolve2(stdout);
21299
+ else resolve2(null);
21300
+ });
21301
+ });
21302
+ }
21303
+
21304
+ // src/supervisor.ts
21305
+ var import_node_child_process8 = require("child_process");
20446
21306
  var import_node_events15 = require("events");
20447
21307
  var import_node_fs7 = require("fs");
20448
21308
  var import_promises18 = require("fs/promises");
@@ -20708,7 +21568,7 @@ var cachedLoginPath;
20708
21568
  function loginShellPath() {
20709
21569
  if (cachedLoginPath !== void 0) return cachedLoginPath;
20710
21570
  try {
20711
- const out = (0, import_node_child_process7.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
21571
+ const out = (0, import_node_child_process8.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
20712
21572
  encoding: "utf8",
20713
21573
  timeout: 5e3
20714
21574
  }).trim();
@@ -20723,7 +21583,7 @@ var ServiceRunner = class extends import_node_events15.EventEmitter {
20723
21583
  super();
20724
21584
  this.spec = spec;
20725
21585
  this.opts = opts;
20726
- this.spawnFn = opts.spawn ?? import_node_child_process7.spawn;
21586
+ this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
20727
21587
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
20728
21588
  this.clearTimer = opts.clearTimer ?? ((h2) => {
20729
21589
  clearTimeout(h2);
@@ -20954,7 +21814,7 @@ var TaskRunner = class extends import_node_events15.EventEmitter {
20954
21814
  super();
20955
21815
  this.spec = spec;
20956
21816
  this.opts = opts;
20957
- this.spawnFn = opts.spawn ?? import_node_child_process7.spawn;
21817
+ this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
20958
21818
  }
20959
21819
  spec;
20960
21820
  opts;
@@ -21502,10 +22362,10 @@ var import_promises19 = require("fs/promises");
21502
22362
  var import_node_path7 = require("path");
21503
22363
 
21504
22364
  // src/status-reporter.ts
21505
- var import_node_child_process9 = require("child_process");
22365
+ var import_node_child_process10 = require("child_process");
21506
22366
 
21507
22367
  // src/tmux.ts
21508
- var import_node_child_process8 = require("child_process");
22368
+ var import_node_child_process9 = require("child_process");
21509
22369
  var import_node_os4 = require("os");
21510
22370
  var MAX_TITLE_LEN = 120;
21511
22371
  function sanitizePaneTitle(raw, ctx) {
@@ -21519,7 +22379,7 @@ function sanitizePaneTitle(raw, ctx) {
21519
22379
  }
21520
22380
  function runTool(cmd, args) {
21521
22381
  return new Promise((resolve2) => {
21522
- const child = (0, import_node_child_process8.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
22382
+ const child = (0, import_node_child_process9.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
21523
22383
  let stdout = "";
21524
22384
  let stderr = "";
21525
22385
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
@@ -21565,8 +22425,12 @@ var StatusReporter = class {
21565
22425
  periodicMs;
21566
22426
  claudeState = "unknown";
21567
22427
  claudeUpdatedAt = null;
22428
+ claudePlan;
22429
+ claudeQuestion;
21568
22430
  codexState = "unknown";
21569
22431
  codexUpdatedAt = null;
22432
+ opencodeState = "unknown";
22433
+ opencodeUpdatedAt = null;
21570
22434
  debounceTimer = null;
21571
22435
  periodicTimer = null;
21572
22436
  onChange = () => this.schedulePush();
@@ -21595,9 +22459,21 @@ var StatusReporter = class {
21595
22459
  this.periodicTimer = null;
21596
22460
  }
21597
22461
  }
21598
- setClaudeState(state) {
22462
+ setClaudeState(state, payload) {
22463
+ const sticky = this.claudeState === "end-plan" || this.claudeState === "question";
22464
+ if (state === "working" && sticky && !payload?.clearPending) return;
21599
22465
  this.claudeState = state;
21600
22466
  this.claudeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
22467
+ if (payload?.clearPending) {
22468
+ this.claudePlan = void 0;
22469
+ this.claudeQuestion = void 0;
22470
+ }
22471
+ if (state === "end-plan" && payload?.plan) {
22472
+ this.claudePlan = payload.plan;
22473
+ }
22474
+ if (state === "question" && payload?.question) {
22475
+ this.claudeQuestion = payload.question;
22476
+ }
21601
22477
  this.schedulePush();
21602
22478
  }
21603
22479
  setCodexState(state) {
@@ -21605,6 +22481,11 @@ var StatusReporter = class {
21605
22481
  this.codexUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
21606
22482
  this.schedulePush();
21607
22483
  }
22484
+ setOpencodeState(state) {
22485
+ this.opencodeState = state;
22486
+ this.opencodeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
22487
+ this.schedulePush();
22488
+ }
21608
22489
  /** Forced immediate push (used on shutdown). */
21609
22490
  flush() {
21610
22491
  if (this.debounceTimer) {
@@ -21654,7 +22535,9 @@ var StatusReporter = class {
21654
22535
  state: this.claudeState,
21655
22536
  updatedAt: this.claudeUpdatedAt,
21656
22537
  sessionRunning: claudeSession2.running,
21657
- ...claudeSession2.title ? { sessionTitle: claudeSession2.title } : {}
22538
+ ...claudeSession2.title ? { sessionTitle: claudeSession2.title } : {},
22539
+ ...this.claudePlan ? { plan: this.claudePlan } : {},
22540
+ ...this.claudeQuestion ? { question: this.claudeQuestion } : {}
21658
22541
  }
21659
22542
  };
21660
22543
  if (codexSession.running || this.codexState !== "unknown") {
@@ -21665,9 +22548,11 @@ var StatusReporter = class {
21665
22548
  ...codexSession.title ? { sessionTitle: codexSession.title } : {}
21666
22549
  };
21667
22550
  }
21668
- if (opencodeSession.running) {
22551
+ if (opencodeSession.running || this.opencodeState !== "unknown") {
21669
22552
  status2.opencode = {
21670
- sessionRunning: true,
22553
+ state: this.opencodeState,
22554
+ updatedAt: this.opencodeUpdatedAt,
22555
+ sessionRunning: opencodeSession.running,
21671
22556
  ...opencodeSession.title ? { sessionTitle: opencodeSession.title } : {}
21672
22557
  };
21673
22558
  }
@@ -21687,7 +22572,7 @@ async function collectPorts(supervisor) {
21687
22572
  }
21688
22573
  function run(cmd, args) {
21689
22574
  return new Promise((resolve2) => {
21690
- const child = (0, import_node_child_process9.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
22575
+ const child = (0, import_node_child_process10.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
21691
22576
  let stdout = "";
21692
22577
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
21693
22578
  child.on("error", () => resolve2({ exitCode: 127, stdout }));
@@ -21844,7 +22729,11 @@ async function handleConnection(sock, opts) {
21844
22729
  if (!CLAUDE_ACTIVITY_STATES.includes(req.state)) {
21845
22730
  writeLine(sock, { ok: false, error: `invalid claude state: ${String(req.state)}` });
21846
22731
  } else {
21847
- opts.reporter?.setClaudeState(req.state);
22732
+ opts.reporter?.setClaudeState(req.state, {
22733
+ plan: req.plan,
22734
+ question: req.question,
22735
+ clearPending: req.clearPending
22736
+ });
21848
22737
  writeLine(sock, { ok: true, data: "ok" });
21849
22738
  }
21850
22739
  sock.end();
@@ -21860,6 +22749,16 @@ async function handleConnection(sock, opts) {
21860
22749
  sock.end();
21861
22750
  return;
21862
22751
  }
22752
+ case "opencode-state": {
22753
+ if (!CLAUDE_ACTIVITY_STATES.includes(req.state)) {
22754
+ writeLine(sock, { ok: false, error: `invalid opencode state: ${String(req.state)}` });
22755
+ } else {
22756
+ opts.reporter?.setOpencodeState(req.state);
22757
+ writeLine(sock, { ok: true, data: "ok" });
22758
+ }
22759
+ sock.end();
22760
+ return;
22761
+ }
21863
22762
  default: {
21864
22763
  writeLine(sock, { ok: false, error: `unknown op` });
21865
22764
  sock.end();
@@ -21913,7 +22812,85 @@ async function* createLineReader(sock) {
21913
22812
  if (buf.length > 0) yield buf;
21914
22813
  }
21915
22814
 
22815
+ // src/box-relay-forwarder.ts
22816
+ var import_node_http3 = require("http");
22817
+ var ALLOWED_PATHS = /* @__PURE__ */ new Set(["/rpc", "/events"]);
22818
+ function startBoxRelayForwarder(opts) {
22819
+ const log = opts.logger ?? (() => {
22820
+ });
22821
+ const upstream = opts.upstream;
22822
+ const upstreamPort = upstream.port.length > 0 ? Number.parseInt(upstream.port, 10) : upstream.protocol === "https:" ? 443 : 80;
22823
+ const server = (0, import_node_http3.createServer)((req, res) => {
22824
+ const path6 = (req.url ?? "").split("?")[0] ?? "";
22825
+ if (req.method !== "POST" || !ALLOWED_PATHS.has(path6)) {
22826
+ res.writeHead(404, { "Content-Type": "text/plain" });
22827
+ res.end("not found");
22828
+ return;
22829
+ }
22830
+ const headers = { ...req.headers };
22831
+ delete headers.host;
22832
+ delete headers.connection;
22833
+ const upstreamReq = (0, import_node_http3.request)(
22834
+ {
22835
+ host: upstream.hostname,
22836
+ port: upstreamPort,
22837
+ method: "POST",
22838
+ path: `${upstream.pathname.replace(/\/$/, "")}${path6}`,
22839
+ headers,
22840
+ // No keep-alive: the relay holds /rpc open for the lifetime of a
22841
+ // host prompt (potentially many seconds). Reusing sockets across
22842
+ // such calls invites mid-stream resets on Node version drift.
22843
+ agent: false
22844
+ },
22845
+ (upstreamRes) => {
22846
+ res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
22847
+ upstreamRes.pipe(res);
22848
+ }
22849
+ );
22850
+ upstreamReq.on("error", (err) => {
22851
+ log(`upstream error on ${path6}: ${err.message}`);
22852
+ if (!res.headersSent) {
22853
+ res.writeHead(502, { "Content-Type": "text/plain" });
22854
+ }
22855
+ res.end();
22856
+ });
22857
+ req.on("error", (err) => {
22858
+ log(`client error on ${path6}: ${err.message}`);
22859
+ upstreamReq.destroy();
22860
+ });
22861
+ req.pipe(upstreamReq);
22862
+ });
22863
+ return new Promise((resolve2, reject) => {
22864
+ const onError = (err) => {
22865
+ reject(err);
22866
+ };
22867
+ server.once("error", onError);
22868
+ server.listen(opts.port, "127.0.0.1", () => {
22869
+ server.removeListener("error", onError);
22870
+ resolve2({
22871
+ url: `http://127.0.0.1:${String(opts.port)}`,
22872
+ close: () => new Promise((res) => {
22873
+ server.close(() => res());
22874
+ })
22875
+ });
22876
+ });
22877
+ });
22878
+ }
22879
+
21916
22880
  // src/commands/daemon.ts
22881
+ function resolveBoxRelayPort() {
22882
+ const raw = process.env.AGENTBOX_BOX_RELAY_PORT;
22883
+ if (raw === void 0 || raw.length === 0) return DEFAULT_BOX_RELAY_PORT;
22884
+ const n2 = Number.parseInt(raw, 10);
22885
+ if (!Number.isFinite(n2) || n2 < 1 || n2 > 65535) {
22886
+ process.stderr.write(
22887
+ `agentbox-ctl: AGENTBOX_BOX_RELAY_PORT=${raw} is not a valid port; falling back to ${String(DEFAULT_BOX_RELAY_PORT)}
22888
+ `
22889
+ );
22890
+ return DEFAULT_BOX_RELAY_PORT;
22891
+ }
22892
+ return n2;
22893
+ }
21917
22894
  var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supervisor in the foreground").option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).option("--config <path>", "path to agentbox.yaml", DEFAULT_CONFIG_PATH).option("--log-dir <path>", "where per-service log files are written", DEFAULT_LOG_DIR).option("--workspace <path>", "cwd for service processes", "/workspace").action(async (opts) => {
21918
22895
  const cfg = await loadConfig(opts.config);
21919
22896
  const sup = new Supervisor({ workspace: opts.workspace, logDir: opts.logDir });
@@ -21925,6 +22902,14 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21925
22902
  sessionName: DEFAULT_CLAUDE_SESSION_NAME
21926
22903
  });
21927
22904
  reporter.start();
22905
+ let codexScraper = null;
22906
+ try {
22907
+ codexScraper = startCodexScraper({ reporter });
22908
+ } catch (err) {
22909
+ const msg = err instanceof Error ? err.message : String(err);
22910
+ process.stderr.write(`agentbox-ctl: codex scraper failed to start: ${msg}
22911
+ `);
22912
+ }
21928
22913
  const server = await startServer({
21929
22914
  socketPath: opts.socket,
21930
22915
  supervisor: sup,
@@ -21938,7 +22923,9 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21938
22923
  `agentbox-ctl: ${String(cfg.services.length)} service(s), ${String(cfg.tasks.length)} task(s) configured
21939
22924
  `
21940
22925
  );
22926
+ const boxRelayPort = resolveBoxRelayPort();
21941
22927
  let inBoxRelay = null;
22928
+ let inBoxForwarder = null;
21942
22929
  if (process.env.AGENTBOX_BOX_KIND === "cloud") {
21943
22930
  const bridgeToken = process.env.AGENTBOX_BRIDGE_TOKEN ?? "";
21944
22931
  const boxId = process.env.AGENTBOX_BOX_ID ?? "";
@@ -21951,7 +22938,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21951
22938
  } else {
21952
22939
  try {
21953
22940
  inBoxRelay = await startRelayServer({
21954
- port: DEFAULT_RELAY_PORT,
22941
+ port: boxRelayPort,
21955
22942
  host: "0.0.0.0",
21956
22943
  mode: "box",
21957
22944
  bridgeToken,
@@ -21966,7 +22953,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21966
22953
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
21967
22954
  });
21968
22955
  process.stdout.write(
21969
- `agentbox-ctl: in-sandbox relay (mode=box) listening on :${String(DEFAULT_RELAY_PORT)}
22956
+ `agentbox-ctl: in-sandbox relay (mode=box) listening on :${String(boxRelayPort)}
21970
22957
  `
21971
22958
  );
21972
22959
  } catch (err) {
@@ -21975,15 +22962,35 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21975
22962
  `);
21976
22963
  }
21977
22964
  }
22965
+ } else {
22966
+ const upstreamUrl = process.env.AGENTBOX_HOST_RELAY_URL ?? "http://host.docker.internal:8787";
22967
+ try {
22968
+ inBoxForwarder = await startBoxRelayForwarder({
22969
+ port: boxRelayPort,
22970
+ upstream: new URL(upstreamUrl),
22971
+ logger: (line) => process.stdout.write(`relay(fwd): ${line}
22972
+ `)
22973
+ });
22974
+ process.stdout.write(
22975
+ `agentbox-ctl: in-box relay forwarder listening on :${String(boxRelayPort)} -> ${upstreamUrl}
22976
+ `
22977
+ );
22978
+ } catch (err) {
22979
+ const msg = err instanceof Error ? err.message : String(err);
22980
+ process.stderr.write(`agentbox-ctl: in-box relay forwarder failed to start: ${msg}
22981
+ `);
22982
+ }
21978
22983
  }
21979
22984
  const shutdown = async (signal) => {
21980
22985
  process.stdout.write(`agentbox-ctl: ${signal} \u2014 shutting down
21981
22986
  `);
22987
+ if (codexScraper) codexScraper.stop();
21982
22988
  reporter.stop();
21983
22989
  reporter.flush();
21984
22990
  server.close();
21985
22991
  await sup.stopAll();
21986
22992
  if (inBoxRelay) await inBoxRelay.close();
22993
+ if (inBoxForwarder) await inBoxForwarder.close();
21987
22994
  process.exit(0);
21988
22995
  };
21989
22996
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
@@ -22030,17 +23037,95 @@ var checkpointCommand = new Command("checkpoint").description("Capture this box
22030
23037
  }
22031
23038
  );
22032
23039
 
23040
+ // src/commands/pr-subcommands.ts
23041
+ var PR_SUBCOMMANDS = [
23042
+ {
23043
+ op: "create",
23044
+ description: "Run `gh pr create` on the host (creates a PR for this box's branch). User is prompted on the host wrapper."
23045
+ },
23046
+ { op: "view", description: "Run `gh pr view` on the host (read-only; no prompt)." },
23047
+ { op: "list", description: "Run `gh pr list` on the host (read-only; no prompt)." },
23048
+ { op: "comment", description: "Run `gh pr comment` on the host (prompted; visible to others)." },
23049
+ { op: "review", description: "Run `gh pr review` on the host (prompted; visible to others)." },
23050
+ {
23051
+ op: "merge",
23052
+ description: "Run `gh pr merge` on the host (prompted; destructive \u2014 AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1)."
23053
+ },
23054
+ {
23055
+ op: "checkout",
23056
+ description: "Run `gh pr checkout` on the host (prompted + clean-tree guard; opt-in via AGENTBOX_GH_PR_CHECKOUT=allow because it switches the host main repo branch)."
23057
+ },
23058
+ { op: "close", description: "Run `gh pr close` on the host (prompted)." },
23059
+ { op: "reopen", description: "Run `gh pr reopen` on the host (prompted)." }
23060
+ ];
23061
+ function buildPrCommand(errorPrefix) {
23062
+ const prCommand = new Command("pr").description(
23063
+ "PR operations via the host `gh` CLI (requires `gh` installed and `gh auth login` on the host)"
23064
+ );
23065
+ for (const spec of PR_SUBCOMMANDS) {
23066
+ prCommand.addCommand(
23067
+ new Command(spec.op).description(spec.description).option("--cwd <path>", "container path identifying which registered worktree to use").addOption(
23068
+ new Option(
23069
+ "--host-initiated-token <token>",
23070
+ "internal: one-time token from the host CLI; skips relay confirm prompt when valid"
23071
+ ).hideHelp()
23072
+ ).allowExcessArguments(true).allowUnknownOption(true).argument(
23073
+ "[args...]",
23074
+ "extra flags forwarded to `gh pr <op>` verbatim (e.g. `--title`, `--body`, `--label`, `--draft`, `--json`)."
23075
+ ).action(async (args, opts) => {
23076
+ const params = { path: opts.cwd ?? process.cwd() };
23077
+ if (args.length > 0) params.args = args;
23078
+ if (opts.hostInitiatedToken) params.hostInitiated = opts.hostInitiatedToken;
23079
+ const code = await postRpcAndExit(`gh.pr.${spec.op}`, params, { errorPrefix });
23080
+ process.exit(code);
23081
+ })
23082
+ );
23083
+ }
23084
+ return prCommand;
23085
+ }
23086
+
23087
+ // src/commands/gh.ts
23088
+ var repoCommand = new Command("repo").description("GitHub repo operations via the host `gh` CLI (host runs `gh repo \u2026` then ships results to the box)").addCommand(
23089
+ new Command("clone").description(
23090
+ "Clone a github repo into the box via host `gh repo clone`. The host clones into a tmpdir with its creds, bundles, and ships the bundle back; the box materialises the working copy and resets origin to the original URL."
23091
+ ).option("--cwd <path>", "container path identifying which registered worktree to use (default: cwd)").option("--branch <name>", "pass --branch <name> to host gh repo clone").option("--depth <n>", "pass --depth <n> to host gh repo clone").argument("<repo>", "github repo: owner/name shorthand or full URL").argument("[dir]", "target directory inside the box (default: derived from repo)").action(
23092
+ async (repo, dir, opts) => {
23093
+ const params = {
23094
+ path: opts.cwd ?? process.cwd(),
23095
+ repo
23096
+ };
23097
+ if (dir) params.targetPath = dir;
23098
+ const extra = [];
23099
+ if (opts.branch) extra.push("--branch", opts.branch);
23100
+ if (opts.depth) extra.push("--depth", opts.depth);
23101
+ if (extra.length > 0) params.args = extra;
23102
+ const code = await postRpcAndExit("gh.repo.clone", params, {
23103
+ errorPrefix: "agentbox-ctl gh repo clone"
23104
+ });
23105
+ process.exit(code);
23106
+ }
23107
+ )
23108
+ );
23109
+ var ghCommand = new Command("gh").description("GitHub CLI operations routed through the relay (host `gh` runs with host creds; box never sees a token)").addCommand(buildPrCommand("agentbox-ctl gh pr")).addCommand(repoCommand);
23110
+
22033
23111
  // src/commands/git.ts
22034
- var import_node_child_process10 = require("child_process");
23112
+ var import_node_child_process11 = require("child_process");
23113
+ function hostInitiatedOption() {
23114
+ return new Option(
23115
+ "--host-initiated-token <token>",
23116
+ "internal: one-time token from the host CLI; skips relay confirm prompt when valid"
23117
+ ).hideHelp();
23118
+ }
22035
23119
  function buildParams(opts, extra) {
22036
23120
  const params = { path: opts.cwd ?? process.cwd() };
22037
23121
  if (opts.remote) params.remote = opts.remote;
22038
23122
  if (extra.length > 0) params.args = extra;
23123
+ if (opts.hostInitiatedToken) params.hostInitiated = opts.hostInitiatedToken;
22039
23124
  return params;
22040
23125
  }
22041
23126
  function runLocalGit(args, cwd) {
22042
23127
  return new Promise((resolve2) => {
22043
- const child = (0, import_node_child_process10.spawn)("git", args, { cwd, stdio: "inherit" });
23128
+ const child = (0, import_node_child_process11.spawn)("git", args, { cwd, stdio: "inherit" });
22044
23129
  child.on("close", (code) => resolve2(code ?? 1));
22045
23130
  child.on("error", (err) => {
22046
23131
  process.stderr.write(`agentbox-ctl git: ${String(err.message ?? err)}
@@ -22050,14 +23135,20 @@ function runLocalGit(args, cwd) {
22050
23135
  });
22051
23136
  }
22052
23137
  var gitCommand = new Command("git").description("Git operations that need host credentials (routed through the agentbox relay)").addCommand(
22053
- new Command("push").description("Run `git push` on the host main repo against this box's branch (user is prompted on the host wrapper to confirm)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git push").action(async (args, opts) => {
23138
+ new Command("push").description("Run `git push` on the host main repo against this box's branch (user is prompted on the host wrapper to confirm)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").addOption(hostInitiatedOption()).allowExcessArguments(true).allowUnknownOption(true).argument(
23139
+ "[args...]",
23140
+ "extra flags appended to the host-built `git push <remote> <branch>` (e.g. `--force-with-lease`, `--tags`). Do NOT re-pass the remote or branch \u2014 they are taken from --remote and the registered worktree; appending them as positionals makes git treat them as refspecs and fail with `refs/remotes/origin/HEAD cannot be resolved to branch`. Use --remote to change the remote."
23141
+ ).action(async (args, opts) => {
22054
23142
  const code = await postRpcAndExit("git.push", buildParams(opts, args), {
22055
23143
  errorPrefix: "agentbox-ctl git"
22056
23144
  });
22057
23145
  process.exit(code);
22058
23146
  })
22059
23147
  ).addCommand(
22060
- new Command("fetch").description("Run `git fetch` on the host main repo (refs land in the shared .git)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git fetch").action(async (args, opts) => {
23148
+ new Command("fetch").description("Run `git fetch` on the host main repo (refs land in the shared .git)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").addOption(hostInitiatedOption()).allowExcessArguments(true).allowUnknownOption(true).argument(
23149
+ "[args...]",
23150
+ "extra flags appended to the host-built `git fetch <remote> <branch>` (e.g. `--prune`, `--tags`). Do NOT re-pass the remote or branch; they come from --remote and the registered worktree (same gotcha as `push`)."
23151
+ ).action(async (args, opts) => {
22061
23152
  const code = await postRpcAndExit("git.fetch", buildParams(opts, args), {
22062
23153
  errorPrefix: "agentbox-ctl git"
22063
23154
  });
@@ -22066,7 +23157,10 @@ var gitCommand = new Command("git").description("Git operations that need host c
22066
23157
  ).addCommand(
22067
23158
  new Command("pull").description(
22068
23159
  "Fetch via the relay (host creds), then merge into the in-container working tree locally"
22069
- ).option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").option("--ff-only", "pass --ff-only to the local merge").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git fetch").action(
23160
+ ).option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").option("--ff-only", "pass --ff-only to the local merge").addOption(hostInitiatedOption()).allowExcessArguments(true).allowUnknownOption(true).argument(
23161
+ "[args...]",
23162
+ "extra flags appended to the host-built `git fetch <remote> <branch>` (e.g. `--prune`). Do NOT re-pass the remote or branch; they come from --remote and the registered worktree (same gotcha as `push`)."
23163
+ ).action(
22070
23164
  async (args, opts) => {
22071
23165
  const fetchCode = await postRpcAndExit("git.fetch", buildParams(opts, args), {
22072
23166
  errorPrefix: "agentbox-ctl git"
@@ -22081,7 +23175,27 @@ var gitCommand = new Command("git").description("Git operations that need host c
22081
23175
  process.exit(mergeCode);
22082
23176
  }
22083
23177
  )
22084
- );
23178
+ ).addCommand(
23179
+ new Command("clone").description(
23180
+ "Clone a github repo into the box. Host runs `git clone` with its creds into a tmpdir, bundles, and ships the bundle back; the box materialises the working copy and resets origin to the original URL."
23181
+ ).option("--cwd <path>", "container path identifying which registered worktree to use (default: cwd)").option("--branch <name>", "pass --branch <name> to host git clone").option("--depth <n>", "pass --depth <n> to host git clone").argument("<url>", "github URL or owner/name shorthand").argument("[dir]", "target directory inside the box (default: derived from url)").action(
23182
+ async (url, dir, opts) => {
23183
+ const params = {
23184
+ path: opts.cwd ?? process.cwd(),
23185
+ url
23186
+ };
23187
+ if (dir) params.targetPath = dir;
23188
+ const extra = [];
23189
+ if (opts.branch) extra.push("--branch", opts.branch);
23190
+ if (opts.depth) extra.push("--depth", opts.depth);
23191
+ if (extra.length > 0) params.args = extra;
23192
+ const code = await postRpcAndExit("git.clone", params, {
23193
+ errorPrefix: "agentbox-ctl git clone"
23194
+ });
23195
+ process.exit(code);
23196
+ }
23197
+ )
23198
+ ).addCommand(buildPrCommand("agentbox-ctl git pr"));
22085
23199
 
22086
23200
  // src/commands/notify.ts
22087
23201
  async function reportState(opts, state) {
@@ -22103,7 +23217,7 @@ var notifyCommand = new Command("notify").description(
22103
23217
  );
22104
23218
 
22105
23219
  // src/commands/open.ts
22106
- var import_node_child_process11 = require("child_process");
23220
+ var import_node_child_process12 = require("child_process");
22107
23221
  var OPEN_TIMEOUT_MS = 3e4;
22108
23222
  function isHttpUrl(value) {
22109
23223
  try {
@@ -22115,7 +23229,7 @@ function isHttpUrl(value) {
22115
23229
  }
22116
23230
  function openInBoxBrowser(url) {
22117
23231
  return new Promise((resolve2) => {
22118
- const child = (0, import_node_child_process11.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
23232
+ const child = (0, import_node_child_process12.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
22119
23233
  const timer = setTimeout(() => {
22120
23234
  child.kill("SIGTERM");
22121
23235
  process.stderr.write(
@@ -22334,9 +23448,11 @@ program2.addCommand(reloadCommand);
22334
23448
  program2.addCommand(claudeSessionCommand);
22335
23449
  program2.addCommand(claudeStateCommand);
22336
23450
  program2.addCommand(codexStateCommand);
23451
+ program2.addCommand(opencodeStateCommand);
22337
23452
  program2.addCommand(waitReadyCommand);
22338
23453
  program2.addCommand(runTaskCommand);
22339
23454
  program2.addCommand(gitCommand);
23455
+ program2.addCommand(ghCommand);
22340
23456
  program2.addCommand(checkpointCommand);
22341
23457
  program2.addCommand(cpCommand);
22342
23458
  program2.addCommand(downloadCommand);