@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
@@ -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
@@ -18414,6 +18568,11 @@ var KEY_REGISTRY = [
18414
18568
  description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
18415
18569
  advanced: true
18416
18570
  },
18571
+ {
18572
+ key: "box.bundleDepth",
18573
+ type: "int",
18574
+ 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/)."
18575
+ },
18417
18576
  {
18418
18577
  key: "claude.sessionName",
18419
18578
  type: "string",
@@ -18521,6 +18680,16 @@ var KEY_REGISTRY = [
18521
18680
  type: "int",
18522
18681
  description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
18523
18682
  },
18683
+ {
18684
+ key: "queue.enabled",
18685
+ type: "bool",
18686
+ description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
18687
+ },
18688
+ {
18689
+ key: "queue.maxConcurrent",
18690
+ type: "int",
18691
+ 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>`."
18692
+ },
18524
18693
  {
18525
18694
  key: "maintenance.pruneProjectConfigs",
18526
18695
  type: "bool",
@@ -18557,7 +18726,8 @@ var PROJECTS_DIR = (0, import_path2.join)(STATE_DIR2, "projects");
18557
18726
  var PROJECT_GC_COUNTER_FILE = (0, import_path3.join)(PROJECTS_DIR, ".gc.json");
18558
18727
 
18559
18728
  // ../relay/dist/index.js
18560
- var DEFAULT_RELAY_PORT = 8787;
18729
+ var import_path6 = require("path");
18730
+ var DEFAULT_BOX_RELAY_PORT = 8788;
18561
18731
  var RELAY_EVENT_RING_SIZE = 1e3;
18562
18732
  var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
18563
18733
  var HostActionQueue = class {
@@ -18870,6 +19040,237 @@ var BoxNotices = class {
18870
19040
  return this.entries.size;
18871
19041
  }
18872
19042
  };
19043
+ var DEFAULT_TTL_MS = 12e4;
19044
+ function hashRpcParams(params) {
19045
+ return (0, import_crypto4.createHash)("sha256").update(canonicalJson(params)).digest("hex");
19046
+ }
19047
+ function canonicalJson(v) {
19048
+ if (v === null) return "null";
19049
+ if (typeof v === "undefined") return "null";
19050
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
19051
+ if (typeof v === "boolean") return v ? "true" : "false";
19052
+ if (typeof v === "string") return JSON.stringify(v);
19053
+ if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
19054
+ if (typeof v === "object") {
19055
+ const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a2], [b]) => a2 < b ? -1 : a2 > b ? 1 : 0);
19056
+ return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
19057
+ }
19058
+ return "null";
19059
+ }
19060
+ var HostInitiatedTokens = class {
19061
+ store = /* @__PURE__ */ new Map();
19062
+ /**
19063
+ * Mint a fresh one-time token scoped to (boxId, method, paramsHash).
19064
+ * `paramsHash` MUST be supplied for any call surface where the box can
19065
+ * influence the eventual RPC params. Pass `null` only when there are no
19066
+ * params (no current call sites use this).
19067
+ */
19068
+ mint(boxId, method, paramsHash, ttlMs = DEFAULT_TTL_MS) {
19069
+ const token = (0, import_crypto4.randomBytes)(32).toString("hex");
19070
+ this.store.set(token, { boxId, method, paramsHash, expiresAt: Date.now() + ttlMs });
19071
+ return token;
19072
+ }
19073
+ /**
19074
+ * Returns true exactly once if `token` is a valid, unexpired token for the
19075
+ * given `(boxId, method)` AND the supplied `incomingParamsHash` matches
19076
+ * the hash bound at mint time. The token is removed on a successful match
19077
+ * (one-shot semantics). All failure modes return false — callers fall back
19078
+ * to the normal prompt path.
19079
+ */
19080
+ consume(token, boxId, method, incomingParamsHash) {
19081
+ if (!token || typeof token !== "string") return false;
19082
+ const record = this.store.get(token);
19083
+ if (!record) return false;
19084
+ if (record.expiresAt < Date.now()) {
19085
+ this.store.delete(token);
19086
+ return false;
19087
+ }
19088
+ if (record.boxId !== boxId || record.method !== method) return false;
19089
+ if (record.paramsHash !== null && record.paramsHash !== incomingParamsHash) {
19090
+ return false;
19091
+ }
19092
+ this.store.delete(token);
19093
+ return true;
19094
+ }
19095
+ /** Drop expired entries. Cheap; safe to call periodically. */
19096
+ gc() {
19097
+ const now = Date.now();
19098
+ for (const [token, record] of this.store) {
19099
+ if (record.expiresAt < now) this.store.delete(token);
19100
+ }
19101
+ }
19102
+ /** Test-only: number of live tokens. */
19103
+ size() {
19104
+ return this.store.size;
19105
+ }
19106
+ };
19107
+ var GH_PR_OPS = [
19108
+ "create",
19109
+ "view",
19110
+ "list",
19111
+ "comment",
19112
+ "review",
19113
+ "merge",
19114
+ "checkout",
19115
+ "close",
19116
+ "reopen"
19117
+ ];
19118
+ function isGhPrOp(value) {
19119
+ return GH_PR_OPS.includes(value);
19120
+ }
19121
+ var GH_PR_READ_ONLY_OPS = /* @__PURE__ */ new Set(["view", "list"]);
19122
+ var GH_RPC_TIMEOUT_MS = 12e4;
19123
+ var GH_READY_CACHE_TTL_MS = 6e4;
19124
+ var ghReadyCache;
19125
+ async function assertGhReady() {
19126
+ const now = Date.now();
19127
+ if (ghReadyCache && ghReadyCache.expiresAt > now) {
19128
+ return ghReadyCache.result;
19129
+ }
19130
+ const result = await probeGh();
19131
+ ghReadyCache = { result, expiresAt: now + GH_READY_CACHE_TTL_MS };
19132
+ return result;
19133
+ }
19134
+ async function probeGh() {
19135
+ const version = await runHostGh(["--version"], process.cwd(), 1e4);
19136
+ if (version.exitCode === 127 || /ENOENT/.test(version.stderr)) {
19137
+ return {
19138
+ exitCode: 127,
19139
+ stdout: "",
19140
+ stderr: "gh not installed on host (https://cli.github.com)\n"
19141
+ };
19142
+ }
19143
+ if (version.exitCode !== 0) {
19144
+ return {
19145
+ exitCode: version.exitCode,
19146
+ stdout: "",
19147
+ stderr: `gh --version failed: ${version.stderr || version.stdout}`.trimEnd() + "\n"
19148
+ };
19149
+ }
19150
+ const auth = await runHostGh(["auth", "status"], process.cwd(), 15e3);
19151
+ if (auth.exitCode !== 0) {
19152
+ return {
19153
+ exitCode: 4,
19154
+ stdout: "",
19155
+ stderr: "gh not authenticated on host (run `gh auth login`)\n"
19156
+ };
19157
+ }
19158
+ return null;
19159
+ }
19160
+ function runHostGh(args, cwd, timeoutMs = GH_RPC_TIMEOUT_MS) {
19161
+ return new Promise((resolve2) => {
19162
+ const child = (0, import_child_process.spawn)("gh", args, {
19163
+ cwd,
19164
+ env: process.env,
19165
+ stdio: ["ignore", "pipe", "pipe"]
19166
+ });
19167
+ let stdout = "";
19168
+ let stderr = "";
19169
+ let settled = false;
19170
+ const finish = (exitCode) => {
19171
+ if (settled) return;
19172
+ settled = true;
19173
+ resolve2({ exitCode, stdout, stderr });
19174
+ };
19175
+ const timer = setTimeout(() => {
19176
+ child.kill("SIGTERM");
19177
+ stderr += `
19178
+ relay: gh command timed out after ${String(timeoutMs)}ms
19179
+ `;
19180
+ finish(124);
19181
+ }, timeoutMs);
19182
+ child.stdout?.on("data", (chunk) => {
19183
+ stdout += chunk.toString("utf8");
19184
+ });
19185
+ child.stderr?.on("data", (chunk) => {
19186
+ stderr += chunk.toString("utf8");
19187
+ });
19188
+ child.on("error", (err) => {
19189
+ clearTimeout(timer);
19190
+ const code = err.code;
19191
+ stderr += String(err.message ?? err);
19192
+ finish(code === "ENOENT" ? 127 : 1);
19193
+ });
19194
+ child.on("close", (code) => {
19195
+ clearTimeout(timer);
19196
+ finish(code ?? -1);
19197
+ });
19198
+ });
19199
+ }
19200
+ async function checkoutGuards(hostMainRepo, registeredBranches) {
19201
+ const status2 = await runGitProbe(["-C", hostMainRepo, "status", "--porcelain"]);
19202
+ if (status2.exitCode !== 0) {
19203
+ return {
19204
+ exitCode: status2.exitCode,
19205
+ stdout: "",
19206
+ stderr: `gh pr checkout: failed to inspect host repo: ${status2.stderr || status2.stdout}`.trimEnd() + "\n"
19207
+ };
19208
+ }
19209
+ if (status2.stdout.trim().length > 0) {
19210
+ return {
19211
+ exitCode: 12,
19212
+ stdout: "",
19213
+ stderr: `gh pr checkout: ${hostMainRepo} has uncommitted changes; refusing to switch branches
19214
+ `
19215
+ };
19216
+ }
19217
+ const head = await runGitProbe(["-C", hostMainRepo, "rev-parse", "--abbrev-ref", "HEAD"]);
19218
+ if (head.exitCode !== 0) {
19219
+ return {
19220
+ exitCode: head.exitCode,
19221
+ stdout: "",
19222
+ stderr: `gh pr checkout: failed to resolve HEAD: ${head.stderr || head.stdout}`.trimEnd() + "\n"
19223
+ };
19224
+ }
19225
+ const currentBranch = head.stdout.trim();
19226
+ if (registeredBranches.includes(currentBranch)) {
19227
+ return {
19228
+ exitCode: 12,
19229
+ stdout: "",
19230
+ stderr: `gh pr checkout: ${hostMainRepo} is on registered box branch ${currentBranch}; refusing (would corrupt the bind-mounted box HEAD)
19231
+ `
19232
+ };
19233
+ }
19234
+ return null;
19235
+ }
19236
+ function runGitProbe(args) {
19237
+ return new Promise((resolve2) => {
19238
+ const child = (0, import_child_process.spawn)("git", args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
19239
+ let stdout = "";
19240
+ let stderr = "";
19241
+ child.stdout?.on("data", (c3) => {
19242
+ stdout += c3.toString("utf8");
19243
+ });
19244
+ child.stderr?.on("data", (c3) => {
19245
+ stderr += c3.toString("utf8");
19246
+ });
19247
+ child.on("error", (err) => {
19248
+ resolve2({ exitCode: 127, stdout, stderr: stderr + String(err.message ?? err) });
19249
+ });
19250
+ child.on("close", (code) => {
19251
+ resolve2({ exitCode: code ?? -1, stdout, stderr });
19252
+ });
19253
+ });
19254
+ }
19255
+ function refuseMergeBypass(op) {
19256
+ if (op !== "merge") return null;
19257
+ if (process.env["AGENTBOX_PROMPT"] !== "off") return null;
19258
+ if (process.env["AGENTBOX_GH_FORCE"] === "1") return null;
19259
+ return {
19260
+ exitCode: 10,
19261
+ stdout: "",
19262
+ stderr: "gh pr merge: AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1 (merge is irreversible)\n"
19263
+ };
19264
+ }
19265
+ function refuseCheckoutByDefault(op) {
19266
+ if (op !== "checkout") return null;
19267
+ if (process.env["AGENTBOX_GH_PR_CHECKOUT"] === "allow") return null;
19268
+ return {
19269
+ exitCode: 13,
19270
+ stdout: "",
19271
+ stderr: "gh pr checkout: disabled by default; set AGENTBOX_GH_PR_CHECKOUT=allow to enable\n"
19272
+ };
19273
+ }
18873
19274
  function sanitizeMnemonic(raw) {
18874
19275
  return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
18875
19276
  }
@@ -18949,6 +19350,18 @@ async function resolveCloudBackend(name) {
18949
19350
  }
18950
19351
  throw new Error(`no host executor for cloud backend '${name}'`);
18951
19352
  }
19353
+ async function refreshCloudPreviewUrl(backendName, boxId, port) {
19354
+ try {
19355
+ const backend = await resolveCloudBackend(backendName);
19356
+ if (!backend.refreshPreviewUrl) return null;
19357
+ const lookup = await lookupCloudBox(boxId);
19358
+ const handle = { sandboxId: lookup.cloudSandboxId };
19359
+ const url = await backend.refreshPreviewUrl(handle, port);
19360
+ return url.url;
19361
+ } catch {
19362
+ return null;
19363
+ }
19364
+ }
18952
19365
  async function executeCloudAction(action, deps) {
18953
19366
  const log = deps.log ?? (() => {
18954
19367
  });
@@ -18968,6 +19381,17 @@ async function executeCloudAction(action, deps) {
18968
19381
  if (action.method === "browser.open.mirror") {
18969
19382
  return runBrowserOpenMirror(action, deps);
18970
19383
  }
19384
+ if (action.method.startsWith("gh.pr.")) {
19385
+ return runGhPrRpc(action, deps);
19386
+ }
19387
+ if (action.method === "git.clone" || action.method === "gh.repo.clone") {
19388
+ return {
19389
+ exitCode: 64,
19390
+ stdout: "",
19391
+ 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.
19392
+ `
19393
+ };
19394
+ }
18971
19395
  return {
18972
19396
  exitCode: 1,
18973
19397
  stdout: "",
@@ -18975,6 +19399,86 @@ async function executeCloudAction(action, deps) {
18975
19399
  `
18976
19400
  };
18977
19401
  }
19402
+ async function runGhPrRpc(action, deps) {
19403
+ const op = action.method.slice("gh.pr.".length);
19404
+ if (!isGhPrOp(op)) {
19405
+ return {
19406
+ exitCode: 64,
19407
+ stdout: "",
19408
+ stderr: `unknown gh.pr.* op: ${op}
19409
+ `
19410
+ };
19411
+ }
19412
+ const mergeBypass = refuseMergeBypass(op);
19413
+ if (mergeBypass) return mergeBypass;
19414
+ const checkoutOptIn = refuseCheckoutByDefault(op);
19415
+ if (checkoutOptIn) return checkoutOptIn;
19416
+ const params = action.params ?? {};
19417
+ const args = Array.isArray(params.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
19418
+ const ghReady = await assertGhReady();
19419
+ if (ghReady) return ghReady;
19420
+ const lookup = await lookupCloudBox(deps.boxId);
19421
+ if (op === "checkout") {
19422
+ const guard = await checkoutGuards(lookup.workspacePath, []);
19423
+ if (guard) return guard;
19424
+ }
19425
+ const tokenClaimedGhCloud = typeof params.hostInitiated === "string";
19426
+ const incomingHashGhCloud = hashRpcParams(params);
19427
+ const hostInitiatedGhOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && (deps.hostInitiatedTokens?.consume(
19428
+ params.hostInitiated,
19429
+ deps.boxId,
19430
+ `gh.pr.${op}`,
19431
+ incomingHashGhCloud
19432
+ ) ?? false);
19433
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && !hostInitiatedGhOk) {
19434
+ return {
19435
+ exitCode: 10,
19436
+ stdout: "",
19437
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19438
+ };
19439
+ }
19440
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedGhOk && deps.prompts && deps.subscribers) {
19441
+ const detail = args.join(" ").slice(0, 200);
19442
+ const ctx = {
19443
+ kind: "confirm",
19444
+ message: `Allow gh pr ${op} from cloud box ${deps.boxName ?? deps.boxId}?`,
19445
+ detail,
19446
+ defaultAnswer: "n",
19447
+ context: {
19448
+ command: `gh pr ${op}`,
19449
+ cwd: params.path,
19450
+ argv: args
19451
+ }
19452
+ };
19453
+ const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
19454
+ if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
19455
+ const noSubMode = (process.env["AGENTBOX_GH_NO_SUB"] ?? "deny").toLowerCase();
19456
+ if (noSubMode === "deny") {
19457
+ return {
19458
+ exitCode: 10,
19459
+ stdout: "",
19460
+ stderr: "denied automatically \u2014 no attached wrapper to confirm. Attach `agentbox claude` (or similar) and retry, or set AGENTBOX_GH_NO_SUB=allow.\n"
19461
+ };
19462
+ }
19463
+ if (noSubMode === "allow") {
19464
+ deps.log?.(`gh.pr.${op} auto-approved (no subscribers, AGENTBOX_GH_NO_SUB=allow)`);
19465
+ } else {
19466
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx, {
19467
+ ttlMs: 5 * 60 * 1e3
19468
+ });
19469
+ if (verdict.answer !== "y") {
19470
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19471
+ }
19472
+ }
19473
+ } else {
19474
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx);
19475
+ if (verdict.answer !== "y") {
19476
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19477
+ }
19478
+ }
19479
+ }
19480
+ return runHostGh(["pr", op, ...args], lookup.workspacePath);
19481
+ }
18978
19482
  async function runBrowserOpenMirror(action, deps) {
18979
19483
  const params = action.params ?? {};
18980
19484
  const url = typeof params.url === "string" ? params.url.trim() : "";
@@ -19001,8 +19505,8 @@ async function runBrowserOpenMirror(action, deps) {
19001
19505
  { ttlMs: TTL_MS }
19002
19506
  );
19003
19507
  if (verdict.answer === "y" && !verdict.cancelled) {
19004
- const { spawn: spawn32 } = await import("child_process");
19005
- const child = spawn32("open", [url], { stdio: "ignore", detached: true });
19508
+ const { spawn: spawn52 } = await import("child_process");
19509
+ const child = spawn52("open", [url], { stdio: "ignore", detached: true });
19006
19510
  child.unref();
19007
19511
  }
19008
19512
  } catch (err) {
@@ -19173,7 +19677,23 @@ async function runGitRpc(action, deps) {
19173
19677
  stderr: `failed to resolve branch in sandbox ${containerPath}: ${branchProbe.stderr || branch}`
19174
19678
  };
19175
19679
  }
19176
- if (action.method === "git.push" && deps.prompts && deps.subscribers) {
19680
+ const isAgentboxBranch = branch.startsWith("agentbox/");
19681
+ const tokenClaimedGit = typeof params.hostInitiated === "string";
19682
+ const incomingHashGit = hashRpcParams(params);
19683
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimedGit && (deps.hostInitiatedTokens?.consume(
19684
+ params.hostInitiated,
19685
+ deps.boxId,
19686
+ "git.push",
19687
+ incomingHashGit
19688
+ ) ?? false);
19689
+ if (action.method === "git.push" && !isAgentboxBranch && tokenClaimedGit && !hostInitiatedOk) {
19690
+ return {
19691
+ exitCode: 10,
19692
+ stdout: "",
19693
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
19694
+ };
19695
+ }
19696
+ if (action.method === "git.push" && !isAgentboxBranch && !hostInitiatedOk && deps.prompts && deps.subscribers) {
19177
19697
  const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
19178
19698
  if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
19179
19699
  const noSubMode = (process.env["AGENTBOX_GIT_PUSH_NO_SUB"] ?? "deny").toLowerCase();
@@ -19248,10 +19768,45 @@ async function runGitRpc(action, deps) {
19248
19768
  for (const a2 of params.args) if (typeof a2 === "string") argv.push(a2);
19249
19769
  }
19250
19770
  const push = await execa("git", argv, { reject: false });
19771
+ let pushStderr = push.stderr ?? "";
19772
+ if ((push.exitCode ?? 1) === 0 && !branch.startsWith("agentbox/")) {
19773
+ try {
19774
+ const sha = await execa(
19775
+ "git",
19776
+ ["-C", lookup.workspacePath, "rev-parse", branch],
19777
+ { reject: false }
19778
+ );
19779
+ const shaText = (sha.stdout ?? "").trim();
19780
+ if (sha.exitCode === 0 && shaText.length > 0) {
19781
+ const updateRef = await backend.exec(
19782
+ handle,
19783
+ `git -C ${shellQuote(containerPath)} update-ref refs/remotes/${remote2}/${branch} ${shellQuote(shaText)}`
19784
+ );
19785
+ if (updateRef.exitCode !== 0) {
19786
+ pushStderr += `
19787
+ relay: post-push in-box update-ref refs/remotes/${remote2}/${branch} failed: ${updateRef.stderr || updateRef.stdout}`;
19788
+ }
19789
+ const setUpstream = await backend.exec(
19790
+ handle,
19791
+ `git -C ${shellQuote(containerPath)} branch --set-upstream-to=${remote2}/${branch} ${shellQuote(branch)}`
19792
+ );
19793
+ if (setUpstream.exitCode !== 0) {
19794
+ pushStderr += `
19795
+ relay: post-push in-box --set-upstream-to=${remote2}/${branch} failed: ${setUpstream.stderr || setUpstream.stdout}`;
19796
+ }
19797
+ } else {
19798
+ pushStderr += `
19799
+ relay: post-push rev-parse ${branch} failed on host; skipping in-box origin/upstream sync`;
19800
+ }
19801
+ } catch (err) {
19802
+ pushStderr += `
19803
+ relay: post-push in-box origin/upstream sync threw: ${err instanceof Error ? err.message : String(err)}`;
19804
+ }
19805
+ }
19251
19806
  return {
19252
19807
  exitCode: push.exitCode ?? 1,
19253
19808
  stdout: push.stdout ?? "",
19254
- stderr: push.stderr ?? ""
19809
+ stderr: pushStderr
19255
19810
  };
19256
19811
  }
19257
19812
  const remote = params.remote ?? "origin";
@@ -19363,6 +19918,8 @@ function createRelayServer(opts) {
19363
19918
  const prompts = new PendingPrompts();
19364
19919
  const subscribers = new PromptSubscribers();
19365
19920
  const notices = new BoxNotices(subscribers);
19921
+ const hostInitiatedTokens = new HostInitiatedTokens();
19922
+ let queuePoke = null;
19366
19923
  const host = opts.host ?? "0.0.0.0";
19367
19924
  const mode = opts.mode ?? "host";
19368
19925
  const hostActions = mode === "box" ? new HostActionQueue() : null;
@@ -19373,7 +19930,7 @@ function createRelayServer(opts) {
19373
19930
  let pollers = null;
19374
19931
  async function getPollers() {
19375
19932
  if (!pollers) {
19376
- const mod = await Promise.resolve().then(() => (init_cloud_poller_ZIWSADJB(), cloud_poller_ZIWSADJB_exports));
19933
+ const mod = await Promise.resolve().then(() => (init_cloud_poller_SUNA6ZQC(), cloud_poller_SUNA6ZQC_exports));
19377
19934
  pollers = new mod.CloudBoxPollers();
19378
19935
  }
19379
19936
  return pollers;
@@ -19495,21 +20052,36 @@ function createRelayServer(opts) {
19495
20052
  if (body.method === "git.push" || body.method === "git.fetch") {
19496
20053
  if (body.method === "git.push") {
19497
20054
  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" });
20055
+ const worktree = resolveWorktree(reg, params?.path ?? "/workspace");
20056
+ const isAgentboxBranch = worktree?.branch.startsWith("agentbox/") ?? false;
20057
+ const tokenClaimed = typeof params?.hostInitiated === "string";
20058
+ const incomingHash = hashRpcParams(params);
20059
+ const hostInitiatedOk = !isAgentboxBranch && tokenClaimed && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, "git.push", incomingHash);
20060
+ if (!isAgentboxBranch && tokenClaimed && !hostInitiatedOk) {
20061
+ send(res, 500, {
20062
+ exitCode: 10,
20063
+ stdout: "",
20064
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
20065
+ });
19511
20066
  return;
19512
20067
  }
20068
+ if (!isAgentboxBranch && !hostInitiatedOk) {
20069
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
20070
+ kind: "confirm",
20071
+ message: `Allow git push from box ${reg.name}?`,
20072
+ detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
20073
+ defaultAnswer: "n",
20074
+ context: {
20075
+ command: "git push",
20076
+ cwd: params?.path,
20077
+ argv: params?.args
20078
+ }
20079
+ });
20080
+ if (verdict.answer !== "y") {
20081
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
20082
+ return;
20083
+ }
20084
+ }
19513
20085
  }
19514
20086
  const result = await handleGitRpc(reg, body.method, body.params);
19515
20087
  const status2 = result.exitCode === 0 ? 200 : 500;
@@ -19542,6 +20114,33 @@ function createRelayServer(opts) {
19542
20114
  send(res, status2, result);
19543
20115
  return;
19544
20116
  }
20117
+ if (body.method.startsWith("gh.pr.")) {
20118
+ const op = body.method.slice("gh.pr.".length);
20119
+ if (!isGhPrOp(op)) {
20120
+ send(res, 400, { error: `unknown gh.pr.* op: ${op}` });
20121
+ return;
20122
+ }
20123
+ const result = await handleGhPrRpc(
20124
+ op,
20125
+ reg,
20126
+ body.params,
20127
+ prompts,
20128
+ subscribers,
20129
+ hostInitiatedTokens
20130
+ );
20131
+ const status2 = result.exitCode === 0 ? 200 : 500;
20132
+ send(res, status2, result);
20133
+ return;
20134
+ }
20135
+ if (body.method === "git.clone" || body.method === "gh.repo.clone") {
20136
+ send(res, 501, {
20137
+ exitCode: 64,
20138
+ stdout: "",
20139
+ 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.
20140
+ `
20141
+ });
20142
+ return;
20143
+ }
19545
20144
  if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
19546
20145
  const params = body.params;
19547
20146
  const kind = body.method.split(".")[1] ?? "workspace";
@@ -19678,6 +20277,7 @@ function createRelayServer(opts) {
19678
20277
  boxName: reg.name,
19679
20278
  prompts,
19680
20279
  subscribers,
20280
+ hostInitiatedTokens,
19681
20281
  log
19682
20282
  });
19683
20283
  await respond(result);
@@ -19690,6 +20290,11 @@ function createRelayServer(opts) {
19690
20290
  });
19691
20291
  }
19692
20292
  } : void 0,
20293
+ // Self-heal a dead preview transport (hetzner SSH `-L` after a
20294
+ // ControlMaster death). The relay strips the `cloud:` prefix
20295
+ // the cloud-provider tags onto BoxRecord.container — what the
20296
+ // backend's `get(sandboxId)` expects is the bare sandbox id.
20297
+ recoverPreviewUrl: reg.backend ? async () => refreshCloudPreviewUrl(reg.backend, reg.boxId, DEFAULT_BOX_RELAY_PORT) : void 0,
19693
20298
  logger: log
19694
20299
  });
19695
20300
  } catch (err) {
@@ -19804,6 +20409,27 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19804
20409
  send(res, 204, null);
19805
20410
  return;
19806
20411
  }
20412
+ if (route === "POST /admin/host-initiated/mint") {
20413
+ const body = await readJsonBody(req);
20414
+ if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.method !== "string" || body.method.length === 0) {
20415
+ send(res, 400, { error: "expected {boxId, method, paramsHash, ttlMs?}" });
20416
+ return;
20417
+ }
20418
+ let paramsHash;
20419
+ if (body.paramsHash === null || body.paramsHash === void 0) {
20420
+ paramsHash = null;
20421
+ } else if (typeof body.paramsHash === "string" && /^[0-9a-f]{64}$/.test(body.paramsHash)) {
20422
+ paramsHash = body.paramsHash;
20423
+ } else {
20424
+ send(res, 400, { error: "paramsHash must be a 64-hex sha256 string or null" });
20425
+ return;
20426
+ }
20427
+ const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
20428
+ const token = hostInitiatedTokens.mint(body.boxId, body.method, paramsHash, ttlMs);
20429
+ log(`host-initiated-mint box=${body.boxId} method=${body.method} paramsBound=${paramsHash !== null}`);
20430
+ send(res, 200, { token });
20431
+ return;
20432
+ }
19807
20433
  if (route === "POST /admin/notices/set") {
19808
20434
  const body = await readJsonBody(req);
19809
20435
  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 +20442,17 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19816
20442
  send(res, 200, { id });
19817
20443
  return;
19818
20444
  }
20445
+ if (route === "POST /admin/queue/enqueue") {
20446
+ const body = await readJsonBody(req);
20447
+ if (!body || typeof body.id !== "string" || body.id.length === 0) {
20448
+ send(res, 400, { error: "expected {id}" });
20449
+ return;
20450
+ }
20451
+ log(`queue-enqueue id=${body.id}`);
20452
+ queuePoke?.();
20453
+ send(res, 204, null);
20454
+ return;
20455
+ }
19819
20456
  if (route === "POST /admin/notices/clear") {
19820
20457
  const body = await readJsonBody(req);
19821
20458
  if (!body || typeof body.id !== "string" || body.id.length === 0) {
@@ -19852,6 +20489,9 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
19852
20489
  notices,
19853
20490
  hostActions: hostActions ?? void 0,
19854
20491
  url: `http://${host}:${String(opts.port)}`,
20492
+ setQueuePoke: (fn) => {
20493
+ queuePoke = fn;
20494
+ },
19855
20495
  close: async () => {
19856
20496
  if (pollers) await pollers.stopAll();
19857
20497
  await new Promise((resolve2, reject) => {
@@ -19903,7 +20543,69 @@ async function handleGitRpc(reg, method, params) {
19903
20543
  if (typeof a2 === "string") argv.push(a2);
19904
20544
  }
19905
20545
  }
19906
- return runHostCommand(argv);
20546
+ const result = await runHostCommand(argv);
20547
+ if (method === "git.push" && result.exitCode === 0 && !worktree.branch.startsWith("agentbox/")) {
20548
+ await runHostCommand([
20549
+ "git",
20550
+ "-C",
20551
+ worktree.hostMainRepo,
20552
+ "branch",
20553
+ `--set-upstream-to=${remote}/${worktree.branch}`,
20554
+ worktree.branch
20555
+ ]);
20556
+ }
20557
+ return result;
20558
+ }
20559
+ async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiatedTokens) {
20560
+ const mergeBypass = refuseMergeBypass(op);
20561
+ if (mergeBypass) return mergeBypass;
20562
+ const checkoutOptIn = refuseCheckoutByDefault(op);
20563
+ if (checkoutOptIn) return checkoutOptIn;
20564
+ const containerPath = params?.path ?? "/workspace";
20565
+ const worktree = resolveWorktree(reg, containerPath);
20566
+ if (!worktree) {
20567
+ return {
20568
+ exitCode: 64,
20569
+ stdout: "",
20570
+ stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
20571
+ };
20572
+ }
20573
+ const ghReady = await assertGhReady();
20574
+ if (ghReady) return ghReady;
20575
+ const args = Array.isArray(params?.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
20576
+ if (op === "checkout") {
20577
+ const branches = (reg.worktrees ?? []).map((w) => w.branch);
20578
+ const guard = await checkoutGuards(worktree.hostMainRepo, branches);
20579
+ if (guard) return guard;
20580
+ }
20581
+ const tokenClaimedGh = typeof params?.hostInitiated === "string";
20582
+ const incomingHashGh = hashRpcParams(params);
20583
+ const hostInitiatedOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, `gh.pr.${op}`, incomingHashGh);
20584
+ if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && !hostInitiatedOk) {
20585
+ return {
20586
+ exitCode: 10,
20587
+ stdout: "",
20588
+ stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
20589
+ };
20590
+ }
20591
+ if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedOk) {
20592
+ const detail = args.join(" ").slice(0, 200);
20593
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
20594
+ kind: "confirm",
20595
+ message: `Allow gh pr ${op} from box ${reg.name}?`,
20596
+ detail,
20597
+ defaultAnswer: "n",
20598
+ context: {
20599
+ command: `gh pr ${op}`,
20600
+ cwd: containerPath,
20601
+ argv: args
20602
+ }
20603
+ });
20604
+ if (verdict.answer !== "y") {
20605
+ return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
20606
+ }
20607
+ }
20608
+ return runHostGh(["pr", op, ...args], worktree.hostMainRepo);
19907
20609
  }
19908
20610
  async function handleCpRpc(reg, method, params) {
19909
20611
  const entry = process.env.AGENTBOX_CLI_ENTRY;
@@ -19964,7 +20666,7 @@ function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
19964
20666
  resolve2({ exitCode: 64, stdout: "", stderr: "empty command" });
19965
20667
  return;
19966
20668
  }
19967
- const child = (0, import_child_process.spawn)(cmd, rest, {
20669
+ const child = (0, import_child_process2.spawn)(cmd, rest, {
19968
20670
  env: process.env,
19969
20671
  stdio: ["ignore", "pipe", "pipe"]
19970
20672
  });
@@ -20011,6 +20713,8 @@ async function startRelayServer(opts) {
20011
20713
  });
20012
20714
  return handle;
20013
20715
  }
20716
+ var QUEUE_DIR = (0, import_path6.join)(STATE_DIR, "queue");
20717
+ var QUEUE_LOGS_DIR = (0, import_path6.join)(STATE_DIR, "logs");
20014
20718
 
20015
20719
  // src/config.ts
20016
20720
  var import_promises16 = require("fs/promises");
@@ -20321,7 +21025,7 @@ function assertBool(raw, where) {
20321
21025
  if (typeof raw !== "boolean") throw new ConfigError(`${where} must be a boolean`);
20322
21026
  return raw;
20323
21027
  }
20324
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults"]);
21028
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults", "carry"]);
20325
21029
  function validateUnitGraph(tasks, services) {
20326
21030
  const names = /* @__PURE__ */ new Set();
20327
21031
  for (const t of tasks) {
@@ -20441,8 +21145,95 @@ function describeCommand(cmd) {
20441
21145
  return Array.isArray(cmd) ? cmd.join(" ") : cmd;
20442
21146
  }
20443
21147
 
20444
- // src/supervisor.ts
21148
+ // src/codex-scraper.ts
20445
21149
  var import_node_child_process7 = require("child_process");
21150
+ var DEFAULT_INTERVAL_MS = 1e3;
21151
+ var DEFAULT_SESSION = "codex";
21152
+ var PATTERNS = [
21153
+ // Permission / approval prompts (highest priority — these mean codex is blocked).
21154
+ { re: /Hooks need review|Trust all and continue/i, state: "waiting" },
21155
+ { re: /Do you trust the contents of this directory/i, state: "waiting" },
21156
+ { re: /Allow this command\?|Approve this (command|tool)\?|Press y\/n|\[Y\/n\]/m, state: "waiting" },
21157
+ { re: /Waiting for (your |user )?(response|input|approval|permission)/i, state: "waiting" },
21158
+ // Compaction (codex's `/compact` command and auto-compaction).
21159
+ { re: /Compacting (conversation|context)|Summariz(e|ing) (the )?conversation/i, state: "compacting" },
21160
+ // Failure / fatal-error frames.
21161
+ { re: /\bError:|\bFailed:|^Traceback /m, state: "error" },
21162
+ // Active work signals — pinned to specific codex TUI fragments to avoid
21163
+ // matching every line of english that contains "working" or "running"
21164
+ // (e.g. the directory-trust prompt's "Working with untrusted contents"
21165
+ // warning is NOT a working state).
21166
+ {
21167
+ re: /\b(Thinking\.\.\.|Worked for \d|Streaming response|tool call \w|Running command|Generating response|Reasoning\.\.\.|Editing \w)/m,
21168
+ state: "working"
21169
+ },
21170
+ // Idle: codex shows a status line `gpt-5.5 high · /workspace` (or similar
21171
+ // model · cwd footer) at the bottom of the input prompt when ready for
21172
+ // input. Lower priority than every "busy" pattern above so an in-flight
21173
+ // turn that still shows the footer correctly registers as working.
21174
+ { re: /gpt-\d+(\.\d+)?(-\w+)?\s+(low|medium|high|xhigh)\b|OpenAI Codex \(v\d/i, state: "idle" }
21175
+ ];
21176
+ function startCodexScraper(opts) {
21177
+ const sessionName = opts.sessionName ?? DEFAULT_SESSION;
21178
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
21179
+ const capture = opts.capturePane ?? defaultCapturePane;
21180
+ let lastState = null;
21181
+ let lastSessionPresent = false;
21182
+ let stopped = false;
21183
+ const tick = async () => {
21184
+ if (stopped) return;
21185
+ try {
21186
+ const pane = await capture(sessionName);
21187
+ if (pane === null) {
21188
+ lastSessionPresent = false;
21189
+ return;
21190
+ }
21191
+ if (!lastSessionPresent) {
21192
+ opts.reporter.setCodexState("idle");
21193
+ lastState = "idle";
21194
+ lastSessionPresent = true;
21195
+ }
21196
+ const matched = matchState(pane);
21197
+ if (matched !== null && matched !== lastState) {
21198
+ opts.reporter.setCodexState(matched);
21199
+ lastState = matched;
21200
+ }
21201
+ } catch {
21202
+ }
21203
+ };
21204
+ const timer = setInterval(() => void tick(), intervalMs);
21205
+ timer.unref();
21206
+ void tick();
21207
+ return {
21208
+ stop() {
21209
+ stopped = true;
21210
+ clearInterval(timer);
21211
+ }
21212
+ };
21213
+ }
21214
+ function matchState(pane) {
21215
+ for (const { re, state } of PATTERNS) {
21216
+ if (re.test(pane)) return state;
21217
+ }
21218
+ return null;
21219
+ }
21220
+ function defaultCapturePane(sessionName) {
21221
+ return new Promise((resolve2) => {
21222
+ const child = (0, import_node_child_process7.spawn)("tmux", ["capture-pane", "-p", "-t", sessionName], {
21223
+ stdio: ["ignore", "pipe", "ignore"]
21224
+ });
21225
+ let stdout = "";
21226
+ child.stdout.on("data", (b) => stdout += b.toString("utf8"));
21227
+ child.on("error", () => resolve2(null));
21228
+ child.on("close", (code) => {
21229
+ if (code === 0) resolve2(stdout);
21230
+ else resolve2(null);
21231
+ });
21232
+ });
21233
+ }
21234
+
21235
+ // src/supervisor.ts
21236
+ var import_node_child_process8 = require("child_process");
20446
21237
  var import_node_events15 = require("events");
20447
21238
  var import_node_fs7 = require("fs");
20448
21239
  var import_promises18 = require("fs/promises");
@@ -20708,7 +21499,7 @@ var cachedLoginPath;
20708
21499
  function loginShellPath() {
20709
21500
  if (cachedLoginPath !== void 0) return cachedLoginPath;
20710
21501
  try {
20711
- const out = (0, import_node_child_process7.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
21502
+ const out = (0, import_node_child_process8.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
20712
21503
  encoding: "utf8",
20713
21504
  timeout: 5e3
20714
21505
  }).trim();
@@ -20723,7 +21514,7 @@ var ServiceRunner = class extends import_node_events15.EventEmitter {
20723
21514
  super();
20724
21515
  this.spec = spec;
20725
21516
  this.opts = opts;
20726
- this.spawnFn = opts.spawn ?? import_node_child_process7.spawn;
21517
+ this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
20727
21518
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
20728
21519
  this.clearTimer = opts.clearTimer ?? ((h2) => {
20729
21520
  clearTimeout(h2);
@@ -20954,7 +21745,7 @@ var TaskRunner = class extends import_node_events15.EventEmitter {
20954
21745
  super();
20955
21746
  this.spec = spec;
20956
21747
  this.opts = opts;
20957
- this.spawnFn = opts.spawn ?? import_node_child_process7.spawn;
21748
+ this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
20958
21749
  }
20959
21750
  spec;
20960
21751
  opts;
@@ -21502,10 +22293,10 @@ var import_promises19 = require("fs/promises");
21502
22293
  var import_node_path7 = require("path");
21503
22294
 
21504
22295
  // src/status-reporter.ts
21505
- var import_node_child_process9 = require("child_process");
22296
+ var import_node_child_process10 = require("child_process");
21506
22297
 
21507
22298
  // src/tmux.ts
21508
- var import_node_child_process8 = require("child_process");
22299
+ var import_node_child_process9 = require("child_process");
21509
22300
  var import_node_os4 = require("os");
21510
22301
  var MAX_TITLE_LEN = 120;
21511
22302
  function sanitizePaneTitle(raw, ctx) {
@@ -21519,7 +22310,7 @@ function sanitizePaneTitle(raw, ctx) {
21519
22310
  }
21520
22311
  function runTool(cmd, args) {
21521
22312
  return new Promise((resolve2) => {
21522
- const child = (0, import_node_child_process8.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
22313
+ const child = (0, import_node_child_process9.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
21523
22314
  let stdout = "";
21524
22315
  let stderr = "";
21525
22316
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
@@ -21565,8 +22356,12 @@ var StatusReporter = class {
21565
22356
  periodicMs;
21566
22357
  claudeState = "unknown";
21567
22358
  claudeUpdatedAt = null;
22359
+ claudePlan;
22360
+ claudeQuestion;
21568
22361
  codexState = "unknown";
21569
22362
  codexUpdatedAt = null;
22363
+ opencodeState = "unknown";
22364
+ opencodeUpdatedAt = null;
21570
22365
  debounceTimer = null;
21571
22366
  periodicTimer = null;
21572
22367
  onChange = () => this.schedulePush();
@@ -21595,9 +22390,21 @@ var StatusReporter = class {
21595
22390
  this.periodicTimer = null;
21596
22391
  }
21597
22392
  }
21598
- setClaudeState(state) {
22393
+ setClaudeState(state, payload) {
22394
+ const sticky = this.claudeState === "end-plan" || this.claudeState === "question";
22395
+ if (state === "working" && sticky && !payload?.clearPending) return;
21599
22396
  this.claudeState = state;
21600
22397
  this.claudeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
22398
+ if (payload?.clearPending) {
22399
+ this.claudePlan = void 0;
22400
+ this.claudeQuestion = void 0;
22401
+ }
22402
+ if (state === "end-plan" && payload?.plan) {
22403
+ this.claudePlan = payload.plan;
22404
+ }
22405
+ if (state === "question" && payload?.question) {
22406
+ this.claudeQuestion = payload.question;
22407
+ }
21601
22408
  this.schedulePush();
21602
22409
  }
21603
22410
  setCodexState(state) {
@@ -21605,6 +22412,11 @@ var StatusReporter = class {
21605
22412
  this.codexUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
21606
22413
  this.schedulePush();
21607
22414
  }
22415
+ setOpencodeState(state) {
22416
+ this.opencodeState = state;
22417
+ this.opencodeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
22418
+ this.schedulePush();
22419
+ }
21608
22420
  /** Forced immediate push (used on shutdown). */
21609
22421
  flush() {
21610
22422
  if (this.debounceTimer) {
@@ -21654,7 +22466,9 @@ var StatusReporter = class {
21654
22466
  state: this.claudeState,
21655
22467
  updatedAt: this.claudeUpdatedAt,
21656
22468
  sessionRunning: claudeSession2.running,
21657
- ...claudeSession2.title ? { sessionTitle: claudeSession2.title } : {}
22469
+ ...claudeSession2.title ? { sessionTitle: claudeSession2.title } : {},
22470
+ ...this.claudePlan ? { plan: this.claudePlan } : {},
22471
+ ...this.claudeQuestion ? { question: this.claudeQuestion } : {}
21658
22472
  }
21659
22473
  };
21660
22474
  if (codexSession.running || this.codexState !== "unknown") {
@@ -21665,9 +22479,11 @@ var StatusReporter = class {
21665
22479
  ...codexSession.title ? { sessionTitle: codexSession.title } : {}
21666
22480
  };
21667
22481
  }
21668
- if (opencodeSession.running) {
22482
+ if (opencodeSession.running || this.opencodeState !== "unknown") {
21669
22483
  status2.opencode = {
21670
- sessionRunning: true,
22484
+ state: this.opencodeState,
22485
+ updatedAt: this.opencodeUpdatedAt,
22486
+ sessionRunning: opencodeSession.running,
21671
22487
  ...opencodeSession.title ? { sessionTitle: opencodeSession.title } : {}
21672
22488
  };
21673
22489
  }
@@ -21687,7 +22503,7 @@ async function collectPorts(supervisor) {
21687
22503
  }
21688
22504
  function run(cmd, args) {
21689
22505
  return new Promise((resolve2) => {
21690
- const child = (0, import_node_child_process9.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
22506
+ const child = (0, import_node_child_process10.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
21691
22507
  let stdout = "";
21692
22508
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
21693
22509
  child.on("error", () => resolve2({ exitCode: 127, stdout }));
@@ -21844,7 +22660,11 @@ async function handleConnection(sock, opts) {
21844
22660
  if (!CLAUDE_ACTIVITY_STATES.includes(req.state)) {
21845
22661
  writeLine(sock, { ok: false, error: `invalid claude state: ${String(req.state)}` });
21846
22662
  } else {
21847
- opts.reporter?.setClaudeState(req.state);
22663
+ opts.reporter?.setClaudeState(req.state, {
22664
+ plan: req.plan,
22665
+ question: req.question,
22666
+ clearPending: req.clearPending
22667
+ });
21848
22668
  writeLine(sock, { ok: true, data: "ok" });
21849
22669
  }
21850
22670
  sock.end();
@@ -21860,6 +22680,16 @@ async function handleConnection(sock, opts) {
21860
22680
  sock.end();
21861
22681
  return;
21862
22682
  }
22683
+ case "opencode-state": {
22684
+ if (!CLAUDE_ACTIVITY_STATES.includes(req.state)) {
22685
+ writeLine(sock, { ok: false, error: `invalid opencode state: ${String(req.state)}` });
22686
+ } else {
22687
+ opts.reporter?.setOpencodeState(req.state);
22688
+ writeLine(sock, { ok: true, data: "ok" });
22689
+ }
22690
+ sock.end();
22691
+ return;
22692
+ }
21863
22693
  default: {
21864
22694
  writeLine(sock, { ok: false, error: `unknown op` });
21865
22695
  sock.end();
@@ -21913,7 +22743,85 @@ async function* createLineReader(sock) {
21913
22743
  if (buf.length > 0) yield buf;
21914
22744
  }
21915
22745
 
22746
+ // src/box-relay-forwarder.ts
22747
+ var import_node_http3 = require("http");
22748
+ var ALLOWED_PATHS = /* @__PURE__ */ new Set(["/rpc", "/events"]);
22749
+ function startBoxRelayForwarder(opts) {
22750
+ const log = opts.logger ?? (() => {
22751
+ });
22752
+ const upstream = opts.upstream;
22753
+ const upstreamPort = upstream.port.length > 0 ? Number.parseInt(upstream.port, 10) : upstream.protocol === "https:" ? 443 : 80;
22754
+ const server = (0, import_node_http3.createServer)((req, res) => {
22755
+ const path6 = (req.url ?? "").split("?")[0] ?? "";
22756
+ if (req.method !== "POST" || !ALLOWED_PATHS.has(path6)) {
22757
+ res.writeHead(404, { "Content-Type": "text/plain" });
22758
+ res.end("not found");
22759
+ return;
22760
+ }
22761
+ const headers = { ...req.headers };
22762
+ delete headers.host;
22763
+ delete headers.connection;
22764
+ const upstreamReq = (0, import_node_http3.request)(
22765
+ {
22766
+ host: upstream.hostname,
22767
+ port: upstreamPort,
22768
+ method: "POST",
22769
+ path: `${upstream.pathname.replace(/\/$/, "")}${path6}`,
22770
+ headers,
22771
+ // No keep-alive: the relay holds /rpc open for the lifetime of a
22772
+ // host prompt (potentially many seconds). Reusing sockets across
22773
+ // such calls invites mid-stream resets on Node version drift.
22774
+ agent: false
22775
+ },
22776
+ (upstreamRes) => {
22777
+ res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
22778
+ upstreamRes.pipe(res);
22779
+ }
22780
+ );
22781
+ upstreamReq.on("error", (err) => {
22782
+ log(`upstream error on ${path6}: ${err.message}`);
22783
+ if (!res.headersSent) {
22784
+ res.writeHead(502, { "Content-Type": "text/plain" });
22785
+ }
22786
+ res.end();
22787
+ });
22788
+ req.on("error", (err) => {
22789
+ log(`client error on ${path6}: ${err.message}`);
22790
+ upstreamReq.destroy();
22791
+ });
22792
+ req.pipe(upstreamReq);
22793
+ });
22794
+ return new Promise((resolve2, reject) => {
22795
+ const onError = (err) => {
22796
+ reject(err);
22797
+ };
22798
+ server.once("error", onError);
22799
+ server.listen(opts.port, "127.0.0.1", () => {
22800
+ server.removeListener("error", onError);
22801
+ resolve2({
22802
+ url: `http://127.0.0.1:${String(opts.port)}`,
22803
+ close: () => new Promise((res) => {
22804
+ server.close(() => res());
22805
+ })
22806
+ });
22807
+ });
22808
+ });
22809
+ }
22810
+
21916
22811
  // src/commands/daemon.ts
22812
+ function resolveBoxRelayPort() {
22813
+ const raw = process.env.AGENTBOX_BOX_RELAY_PORT;
22814
+ if (raw === void 0 || raw.length === 0) return DEFAULT_BOX_RELAY_PORT;
22815
+ const n2 = Number.parseInt(raw, 10);
22816
+ if (!Number.isFinite(n2) || n2 < 1 || n2 > 65535) {
22817
+ process.stderr.write(
22818
+ `agentbox-ctl: AGENTBOX_BOX_RELAY_PORT=${raw} is not a valid port; falling back to ${String(DEFAULT_BOX_RELAY_PORT)}
22819
+ `
22820
+ );
22821
+ return DEFAULT_BOX_RELAY_PORT;
22822
+ }
22823
+ return n2;
22824
+ }
21917
22825
  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
22826
  const cfg = await loadConfig(opts.config);
21919
22827
  const sup = new Supervisor({ workspace: opts.workspace, logDir: opts.logDir });
@@ -21925,6 +22833,14 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21925
22833
  sessionName: DEFAULT_CLAUDE_SESSION_NAME
21926
22834
  });
21927
22835
  reporter.start();
22836
+ let codexScraper = null;
22837
+ try {
22838
+ codexScraper = startCodexScraper({ reporter });
22839
+ } catch (err) {
22840
+ const msg = err instanceof Error ? err.message : String(err);
22841
+ process.stderr.write(`agentbox-ctl: codex scraper failed to start: ${msg}
22842
+ `);
22843
+ }
21928
22844
  const server = await startServer({
21929
22845
  socketPath: opts.socket,
21930
22846
  supervisor: sup,
@@ -21938,7 +22854,9 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21938
22854
  `agentbox-ctl: ${String(cfg.services.length)} service(s), ${String(cfg.tasks.length)} task(s) configured
21939
22855
  `
21940
22856
  );
22857
+ const boxRelayPort = resolveBoxRelayPort();
21941
22858
  let inBoxRelay = null;
22859
+ let inBoxForwarder = null;
21942
22860
  if (process.env.AGENTBOX_BOX_KIND === "cloud") {
21943
22861
  const bridgeToken = process.env.AGENTBOX_BRIDGE_TOKEN ?? "";
21944
22862
  const boxId = process.env.AGENTBOX_BOX_ID ?? "";
@@ -21951,7 +22869,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21951
22869
  } else {
21952
22870
  try {
21953
22871
  inBoxRelay = await startRelayServer({
21954
- port: DEFAULT_RELAY_PORT,
22872
+ port: boxRelayPort,
21955
22873
  host: "0.0.0.0",
21956
22874
  mode: "box",
21957
22875
  bridgeToken,
@@ -21966,7 +22884,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21966
22884
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
21967
22885
  });
21968
22886
  process.stdout.write(
21969
- `agentbox-ctl: in-sandbox relay (mode=box) listening on :${String(DEFAULT_RELAY_PORT)}
22887
+ `agentbox-ctl: in-sandbox relay (mode=box) listening on :${String(boxRelayPort)}
21970
22888
  `
21971
22889
  );
21972
22890
  } catch (err) {
@@ -21975,15 +22893,35 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
21975
22893
  `);
21976
22894
  }
21977
22895
  }
22896
+ } else {
22897
+ const upstreamUrl = process.env.AGENTBOX_HOST_RELAY_URL ?? "http://host.docker.internal:8787";
22898
+ try {
22899
+ inBoxForwarder = await startBoxRelayForwarder({
22900
+ port: boxRelayPort,
22901
+ upstream: new URL(upstreamUrl),
22902
+ logger: (line) => process.stdout.write(`relay(fwd): ${line}
22903
+ `)
22904
+ });
22905
+ process.stdout.write(
22906
+ `agentbox-ctl: in-box relay forwarder listening on :${String(boxRelayPort)} -> ${upstreamUrl}
22907
+ `
22908
+ );
22909
+ } catch (err) {
22910
+ const msg = err instanceof Error ? err.message : String(err);
22911
+ process.stderr.write(`agentbox-ctl: in-box relay forwarder failed to start: ${msg}
22912
+ `);
22913
+ }
21978
22914
  }
21979
22915
  const shutdown = async (signal) => {
21980
22916
  process.stdout.write(`agentbox-ctl: ${signal} \u2014 shutting down
21981
22917
  `);
22918
+ if (codexScraper) codexScraper.stop();
21982
22919
  reporter.stop();
21983
22920
  reporter.flush();
21984
22921
  server.close();
21985
22922
  await sup.stopAll();
21986
22923
  if (inBoxRelay) await inBoxRelay.close();
22924
+ if (inBoxForwarder) await inBoxForwarder.close();
21987
22925
  process.exit(0);
21988
22926
  };
21989
22927
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
@@ -22030,17 +22968,95 @@ var checkpointCommand = new Command("checkpoint").description("Capture this box
22030
22968
  }
22031
22969
  );
22032
22970
 
22971
+ // src/commands/pr-subcommands.ts
22972
+ var PR_SUBCOMMANDS = [
22973
+ {
22974
+ op: "create",
22975
+ description: "Run `gh pr create` on the host (creates a PR for this box's branch). User is prompted on the host wrapper."
22976
+ },
22977
+ { op: "view", description: "Run `gh pr view` on the host (read-only; no prompt)." },
22978
+ { op: "list", description: "Run `gh pr list` on the host (read-only; no prompt)." },
22979
+ { op: "comment", description: "Run `gh pr comment` on the host (prompted; visible to others)." },
22980
+ { op: "review", description: "Run `gh pr review` on the host (prompted; visible to others)." },
22981
+ {
22982
+ op: "merge",
22983
+ description: "Run `gh pr merge` on the host (prompted; destructive \u2014 AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1)."
22984
+ },
22985
+ {
22986
+ op: "checkout",
22987
+ 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)."
22988
+ },
22989
+ { op: "close", description: "Run `gh pr close` on the host (prompted)." },
22990
+ { op: "reopen", description: "Run `gh pr reopen` on the host (prompted)." }
22991
+ ];
22992
+ function buildPrCommand(errorPrefix) {
22993
+ const prCommand = new Command("pr").description(
22994
+ "PR operations via the host `gh` CLI (requires `gh` installed and `gh auth login` on the host)"
22995
+ );
22996
+ for (const spec of PR_SUBCOMMANDS) {
22997
+ prCommand.addCommand(
22998
+ new Command(spec.op).description(spec.description).option("--cwd <path>", "container path identifying which registered worktree to use").addOption(
22999
+ new Option(
23000
+ "--host-initiated-token <token>",
23001
+ "internal: one-time token from the host CLI; skips relay confirm prompt when valid"
23002
+ ).hideHelp()
23003
+ ).allowExcessArguments(true).allowUnknownOption(true).argument(
23004
+ "[args...]",
23005
+ "extra flags forwarded to `gh pr <op>` verbatim (e.g. `--title`, `--body`, `--label`, `--draft`, `--json`)."
23006
+ ).action(async (args, opts) => {
23007
+ const params = { path: opts.cwd ?? process.cwd() };
23008
+ if (args.length > 0) params.args = args;
23009
+ if (opts.hostInitiatedToken) params.hostInitiated = opts.hostInitiatedToken;
23010
+ const code = await postRpcAndExit(`gh.pr.${spec.op}`, params, { errorPrefix });
23011
+ process.exit(code);
23012
+ })
23013
+ );
23014
+ }
23015
+ return prCommand;
23016
+ }
23017
+
23018
+ // src/commands/gh.ts
23019
+ 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(
23020
+ new Command("clone").description(
23021
+ "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."
23022
+ ).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(
23023
+ async (repo, dir, opts) => {
23024
+ const params = {
23025
+ path: opts.cwd ?? process.cwd(),
23026
+ repo
23027
+ };
23028
+ if (dir) params.targetPath = dir;
23029
+ const extra = [];
23030
+ if (opts.branch) extra.push("--branch", opts.branch);
23031
+ if (opts.depth) extra.push("--depth", opts.depth);
23032
+ if (extra.length > 0) params.args = extra;
23033
+ const code = await postRpcAndExit("gh.repo.clone", params, {
23034
+ errorPrefix: "agentbox-ctl gh repo clone"
23035
+ });
23036
+ process.exit(code);
23037
+ }
23038
+ )
23039
+ );
23040
+ 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);
23041
+
22033
23042
  // src/commands/git.ts
22034
- var import_node_child_process10 = require("child_process");
23043
+ var import_node_child_process11 = require("child_process");
23044
+ function hostInitiatedOption() {
23045
+ return new Option(
23046
+ "--host-initiated-token <token>",
23047
+ "internal: one-time token from the host CLI; skips relay confirm prompt when valid"
23048
+ ).hideHelp();
23049
+ }
22035
23050
  function buildParams(opts, extra) {
22036
23051
  const params = { path: opts.cwd ?? process.cwd() };
22037
23052
  if (opts.remote) params.remote = opts.remote;
22038
23053
  if (extra.length > 0) params.args = extra;
23054
+ if (opts.hostInitiatedToken) params.hostInitiated = opts.hostInitiatedToken;
22039
23055
  return params;
22040
23056
  }
22041
23057
  function runLocalGit(args, cwd) {
22042
23058
  return new Promise((resolve2) => {
22043
- const child = (0, import_node_child_process10.spawn)("git", args, { cwd, stdio: "inherit" });
23059
+ const child = (0, import_node_child_process11.spawn)("git", args, { cwd, stdio: "inherit" });
22044
23060
  child.on("close", (code) => resolve2(code ?? 1));
22045
23061
  child.on("error", (err) => {
22046
23062
  process.stderr.write(`agentbox-ctl git: ${String(err.message ?? err)}
@@ -22050,14 +23066,20 @@ function runLocalGit(args, cwd) {
22050
23066
  });
22051
23067
  }
22052
23068
  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) => {
23069
+ 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(
23070
+ "[args...]",
23071
+ "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."
23072
+ ).action(async (args, opts) => {
22054
23073
  const code = await postRpcAndExit("git.push", buildParams(opts, args), {
22055
23074
  errorPrefix: "agentbox-ctl git"
22056
23075
  });
22057
23076
  process.exit(code);
22058
23077
  })
22059
23078
  ).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) => {
23079
+ 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(
23080
+ "[args...]",
23081
+ "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`)."
23082
+ ).action(async (args, opts) => {
22061
23083
  const code = await postRpcAndExit("git.fetch", buildParams(opts, args), {
22062
23084
  errorPrefix: "agentbox-ctl git"
22063
23085
  });
@@ -22066,7 +23088,10 @@ var gitCommand = new Command("git").description("Git operations that need host c
22066
23088
  ).addCommand(
22067
23089
  new Command("pull").description(
22068
23090
  "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(
23091
+ ).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(
23092
+ "[args...]",
23093
+ "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`)."
23094
+ ).action(
22070
23095
  async (args, opts) => {
22071
23096
  const fetchCode = await postRpcAndExit("git.fetch", buildParams(opts, args), {
22072
23097
  errorPrefix: "agentbox-ctl git"
@@ -22081,7 +23106,27 @@ var gitCommand = new Command("git").description("Git operations that need host c
22081
23106
  process.exit(mergeCode);
22082
23107
  }
22083
23108
  )
22084
- );
23109
+ ).addCommand(
23110
+ new Command("clone").description(
23111
+ "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."
23112
+ ).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(
23113
+ async (url, dir, opts) => {
23114
+ const params = {
23115
+ path: opts.cwd ?? process.cwd(),
23116
+ url
23117
+ };
23118
+ if (dir) params.targetPath = dir;
23119
+ const extra = [];
23120
+ if (opts.branch) extra.push("--branch", opts.branch);
23121
+ if (opts.depth) extra.push("--depth", opts.depth);
23122
+ if (extra.length > 0) params.args = extra;
23123
+ const code = await postRpcAndExit("git.clone", params, {
23124
+ errorPrefix: "agentbox-ctl git clone"
23125
+ });
23126
+ process.exit(code);
23127
+ }
23128
+ )
23129
+ ).addCommand(buildPrCommand("agentbox-ctl git pr"));
22085
23130
 
22086
23131
  // src/commands/notify.ts
22087
23132
  async function reportState(opts, state) {
@@ -22103,7 +23148,7 @@ var notifyCommand = new Command("notify").description(
22103
23148
  );
22104
23149
 
22105
23150
  // src/commands/open.ts
22106
- var import_node_child_process11 = require("child_process");
23151
+ var import_node_child_process12 = require("child_process");
22107
23152
  var OPEN_TIMEOUT_MS = 3e4;
22108
23153
  function isHttpUrl(value) {
22109
23154
  try {
@@ -22115,7 +23160,7 @@ function isHttpUrl(value) {
22115
23160
  }
22116
23161
  function openInBoxBrowser(url) {
22117
23162
  return new Promise((resolve2) => {
22118
- const child = (0, import_node_child_process11.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
23163
+ const child = (0, import_node_child_process12.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
22119
23164
  const timer = setTimeout(() => {
22120
23165
  child.kill("SIGTERM");
22121
23166
  process.stderr.write(
@@ -22334,9 +23379,11 @@ program2.addCommand(reloadCommand);
22334
23379
  program2.addCommand(claudeSessionCommand);
22335
23380
  program2.addCommand(claudeStateCommand);
22336
23381
  program2.addCommand(codexStateCommand);
23382
+ program2.addCommand(opencodeStateCommand);
22337
23383
  program2.addCommand(waitReadyCommand);
22338
23384
  program2.addCommand(runTaskCommand);
22339
23385
  program2.addCommand(gitCommand);
23386
+ program2.addCommand(ghCommand);
22340
23387
  program2.addCommand(checkpointCommand);
22341
23388
  program2.addCommand(cpCommand);
22342
23389
  program2.addCommand(downloadCommand);