@madarco/agentbox 0.4.1 → 0.6.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 (40) hide show
  1. package/dist/{chunk-J35IH7W5.js → chunk-BBZMA2K6.js} +61 -23
  2. package/dist/chunk-BBZMA2K6.js.map +1 -0
  3. package/dist/{chunk-SOMIKEN2.js → chunk-HHMWQNLF.js} +272 -214
  4. package/dist/chunk-HHMWQNLF.js.map +1 -0
  5. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  6. package/dist/chunk-HPZMD5DE.js.map +1 -0
  7. package/dist/{chunk-NSIECUCS.js → chunk-HTTKML3C.js} +705 -289
  8. package/dist/chunk-HTTKML3C.js.map +1 -0
  9. package/dist/{chunk-WR5FFGE5.js → chunk-KJNZP6I3.js} +218 -128
  10. package/dist/chunk-KJNZP6I3.js.map +1 -0
  11. package/dist/{chunk-FQD6ZWYW.js → chunk-M7I247BK.js} +68 -65
  12. package/dist/chunk-M7I247BK.js.map +1 -0
  13. package/dist/create-6PWXI6HO-OWAMHBAK.js +15 -0
  14. package/dist/index.js +2394 -1283
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-EMXR46DI-DUVBXNTV.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-SZXOJE3D-N7OODCHW.js +19 -0
  19. package/package.json +3 -2
  20. package/runtime/docker/Dockerfile.box +65 -25
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +52 -55
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +272 -160
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +52 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
  25. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
  26. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +21 -15
  27. package/runtime/relay/bin.cjs +407 -12
  28. package/share/agentbox-setup/SKILL.md +52 -55
  29. package/dist/chunk-FQD6ZWYW.js.map +0 -1
  30. package/dist/chunk-IDR4HVIC.js.map +0 -1
  31. package/dist/chunk-J35IH7W5.js.map +0 -1
  32. package/dist/chunk-NSIECUCS.js.map +0 -1
  33. package/dist/chunk-SOMIKEN2.js.map +0 -1
  34. package/dist/chunk-WR5FFGE5.js.map +0 -1
  35. package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
  36. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  37. /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
  38. /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
  39. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  40. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
@@ -10381,6 +10381,171 @@ var RELAY_EVENT_RING_SIZE = 1e3;
10381
10381
  var import_node_child_process = require("child_process");
10382
10382
  var import_node_http = require("http");
10383
10383
 
10384
+ // src/notices.ts
10385
+ var import_node_crypto = require("crypto");
10386
+ var DEFAULT_NOTICE_TTL_MS = 66e4;
10387
+ var BoxNotices = class {
10388
+ constructor(subscribers) {
10389
+ this.subscribers = subscribers;
10390
+ }
10391
+ subscribers;
10392
+ /** keyed by notice id. */
10393
+ entries = /* @__PURE__ */ new Map();
10394
+ /**
10395
+ * Register a notice for `boxId` and broadcast `notice-set`. At most one
10396
+ * notice per (box, kind) is kept — a fresh `set` for the same kind
10397
+ * replaces the previous one (and cancels its TTL timer so a stale timer
10398
+ * can't later fire a `notice-clear` racing the replacement). Returns the
10399
+ * generated notice id.
10400
+ */
10401
+ set(boxId, kind, message, ttlMs) {
10402
+ for (const [id, entry] of this.entries) {
10403
+ if (entry.boxId === boxId && entry.ev.kind === kind) {
10404
+ clearTimeout(entry.timer);
10405
+ this.entries.delete(id);
10406
+ }
10407
+ }
10408
+ const ev = { id: (0, import_node_crypto.randomUUID)(), kind, message };
10409
+ const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;
10410
+ const timer = setTimeout(() => {
10411
+ if (this.entries.delete(ev.id)) {
10412
+ this.subscribers.broadcast(boxId, "notice-clear", { id: ev.id });
10413
+ }
10414
+ }, ttl);
10415
+ if (typeof timer.unref === "function") timer.unref();
10416
+ this.entries.set(ev.id, { ev, boxId, timer });
10417
+ this.subscribers.broadcast(boxId, "notice-set", ev);
10418
+ return ev.id;
10419
+ }
10420
+ /**
10421
+ * Clear a notice by id. Idempotent: returns false when no such notice
10422
+ * exists (already cleared / expired). Broadcasts `notice-clear` on a hit.
10423
+ */
10424
+ clear(id) {
10425
+ const entry = this.entries.get(id);
10426
+ if (!entry) return false;
10427
+ clearTimeout(entry.timer);
10428
+ this.entries.delete(id);
10429
+ this.subscribers.broadcast(entry.boxId, "notice-clear", { id });
10430
+ return true;
10431
+ }
10432
+ /** Snapshot of active notices for a box; replayed to a new SSE subscriber. */
10433
+ forBox(boxId) {
10434
+ const out = [];
10435
+ for (const entry of this.entries.values()) {
10436
+ if (entry.boxId === boxId) out.push(entry.ev);
10437
+ }
10438
+ return out;
10439
+ }
10440
+ size() {
10441
+ return this.entries.size;
10442
+ }
10443
+ };
10444
+
10445
+ // src/prompts.ts
10446
+ var import_node_crypto2 = require("crypto");
10447
+ var PendingPrompts = class {
10448
+ entries = /* @__PURE__ */ new Map();
10449
+ add(boxId, ev) {
10450
+ return new Promise((resolve2) => {
10451
+ this.entries.set(ev.id, {
10452
+ ev,
10453
+ boxId,
10454
+ resolve: resolve2,
10455
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
10456
+ });
10457
+ });
10458
+ }
10459
+ /**
10460
+ * Idempotent: returns true if a pending entry was found + resolved, false
10461
+ * otherwise. The /admin/prompts/answer handler uses the bool to decide
10462
+ * 204 vs 404 — the wrapper treats both as "we're done."
10463
+ */
10464
+ resolve(id, answer, cancelled) {
10465
+ const entry = this.entries.get(id);
10466
+ if (!entry) return false;
10467
+ this.entries.delete(id);
10468
+ entry.resolve({ answer, cancelled });
10469
+ return true;
10470
+ }
10471
+ /**
10472
+ * Snapshot of all pending prompts for a given box; used to flush the
10473
+ * backlog to a newly-attached SSE subscriber.
10474
+ */
10475
+ forBox(boxId) {
10476
+ const out = [];
10477
+ for (const entry of this.entries.values()) {
10478
+ if (entry.boxId === boxId) out.push(entry.ev);
10479
+ }
10480
+ return out;
10481
+ }
10482
+ /** boxId that owns a pending prompt id, or null when unknown. */
10483
+ boxFor(id) {
10484
+ const entry = this.entries.get(id);
10485
+ return entry ? entry.boxId : null;
10486
+ }
10487
+ size() {
10488
+ return this.entries.size;
10489
+ }
10490
+ };
10491
+ var PromptSubscribers = class {
10492
+ byBox = /* @__PURE__ */ new Map();
10493
+ add(boxId, res) {
10494
+ let set = this.byBox.get(boxId);
10495
+ if (!set) {
10496
+ set = /* @__PURE__ */ new Set();
10497
+ this.byBox.set(boxId, set);
10498
+ }
10499
+ set.add(res);
10500
+ }
10501
+ remove(boxId, res) {
10502
+ const set = this.byBox.get(boxId);
10503
+ if (!set) return;
10504
+ set.delete(res);
10505
+ if (set.size === 0) this.byBox.delete(boxId);
10506
+ }
10507
+ forBox(boxId) {
10508
+ const set = this.byBox.get(boxId);
10509
+ return set ? Array.from(set) : [];
10510
+ }
10511
+ /**
10512
+ * Fire-and-forget broadcast. SSE writes that fail (closed socket) are
10513
+ * swallowed — the `res.on('close')` handler in the server route already
10514
+ * deregisters the dead subscriber.
10515
+ */
10516
+ broadcast(boxId, event, data) {
10517
+ const set = this.byBox.get(boxId);
10518
+ if (!set) return;
10519
+ const payload = `event: ${event}
10520
+ data: ${JSON.stringify(data)}
10521
+
10522
+ `;
10523
+ for (const res of set) {
10524
+ try {
10525
+ res.write(payload);
10526
+ } catch {
10527
+ }
10528
+ }
10529
+ }
10530
+ };
10531
+ async function askPrompt(prompts, subscribers, boxId, params) {
10532
+ if (process.env.AGENTBOX_PROMPT === "off") {
10533
+ return { answer: "y" };
10534
+ }
10535
+ const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
10536
+ const promise = prompts.add(boxId, ev);
10537
+ subscribers.broadcast(boxId, "prompt-ask", ev);
10538
+ return promise;
10539
+ }
10540
+ function isPromptAnswerBody(v) {
10541
+ if (!v || typeof v !== "object") return false;
10542
+ const o = v;
10543
+ if (typeof o.id !== "string" || o.id.length === 0) return false;
10544
+ if (o.answer !== "y" && o.answer !== "n") return false;
10545
+ if (o.cancelled !== void 0 && typeof o.cancelled !== "boolean") return false;
10546
+ return true;
10547
+ }
10548
+
10384
10549
  // src/registry.ts
10385
10550
  var BoxRegistry = class {
10386
10551
  map = /* @__PURE__ */ new Map();
@@ -10447,8 +10612,16 @@ var EventBuffer = class {
10447
10612
  var import_promises = require("fs/promises");
10448
10613
  var import_node_os = require("os");
10449
10614
  var import_node_path = require("path");
10450
- function boxStatusPathFor(boxId) {
10451
- return (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentbox", "boxes", boxId, "status.json");
10615
+ function sanitizeMnemonic(raw) {
10616
+ return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
10617
+ }
10618
+ function boxRunDirFor(boxId, name, projectIndex) {
10619
+ const mnemonic = sanitizeMnemonic(name);
10620
+ const segment = typeof projectIndex === "number" && Number.isFinite(projectIndex) && projectIndex > 0 ? `${boxId}-${String(projectIndex)}-${mnemonic}` : `${boxId}-${mnemonic}`;
10621
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentbox", "boxes", segment);
10622
+ }
10623
+ function boxStatusPathFor(boxId, name, projectIndex) {
10624
+ return (0, import_node_path.join)(boxRunDirFor(boxId, name, projectIndex), "status.json");
10452
10625
  }
10453
10626
  function isValidBoxStatus(payload) {
10454
10627
  if (typeof payload !== "object" || payload === null) return false;
@@ -10460,13 +10633,19 @@ var BoxStatusStore = class {
10460
10633
  get(boxId) {
10461
10634
  return this.map.get(boxId);
10462
10635
  }
10463
- /** Update the in-memory entry and best-effort persist it to disk. */
10464
- async set(boxId, status) {
10636
+ /**
10637
+ * Update the in-memory entry and best-effort persist it to disk. `name` is
10638
+ * the box's user-facing name (from the registry); `projectIndex` is the
10639
+ * 1-based per-project `N`. Together they form the on-disk dir
10640
+ * `~/.agentbox/boxes/<id>-<n>-<mnemonic>/status.json` (or
10641
+ * `<id>-<mnemonic>/` if N is absent — legacy boxes).
10642
+ */
10643
+ async set(boxId, name, projectIndex, status) {
10465
10644
  this.map.set(boxId, status);
10466
- const target = boxStatusPathFor(boxId);
10645
+ const target = boxStatusPathFor(boxId, name, projectIndex);
10467
10646
  const tmp = `${target}.${String(process.pid)}.tmp`;
10468
10647
  try {
10469
- await (0, import_promises.mkdir)((0, import_node_path.join)((0, import_node_os.homedir)(), ".agentbox", "boxes", boxId), { recursive: true });
10648
+ await (0, import_promises.mkdir)(boxRunDirFor(boxId, name, projectIndex), { recursive: true });
10470
10649
  await (0, import_promises.writeFile)(tmp, JSON.stringify(status), "utf8");
10471
10650
  await (0, import_promises.rename)(tmp, target);
10472
10651
  } catch {
@@ -10485,6 +10664,10 @@ var BOX_STATUS_EVENT = "box-status";
10485
10664
  var MAX_BODY_BYTES = 1024 * 1024;
10486
10665
  var GIT_RPC_TIMEOUT_MS = 12e4;
10487
10666
  var CHECKPOINT_RPC_TIMEOUT_MS = 6e5;
10667
+ var DOWNLOAD_RPC_TIMEOUT_MS = 6e5;
10668
+ var CP_RPC_TIMEOUT_MS = 3e5;
10669
+ var BROWSER_OPEN_RPC_TIMEOUT_MS = 15e3;
10670
+ var SSE_HEARTBEAT_MS = 15e3;
10488
10671
  function send(res, status, body, contentType = "application/json") {
10489
10672
  const text = body == null ? "" : typeof body === "string" ? body : JSON.stringify(body);
10490
10673
  res.statusCode = status;
@@ -10540,6 +10723,9 @@ function createRelayServer(opts) {
10540
10723
  const registry = new BoxRegistry();
10541
10724
  const events = new EventBuffer();
10542
10725
  const statusStore = new BoxStatusStore();
10726
+ const prompts = new PendingPrompts();
10727
+ const subscribers = new PromptSubscribers();
10728
+ const notices = new BoxNotices(subscribers);
10543
10729
  const host = opts.host ?? "0.0.0.0";
10544
10730
  const server = (0, import_node_http.createServer)((req, res) => {
10545
10731
  handle(req, res).catch((err) => {
@@ -10575,7 +10761,7 @@ function createRelayServer(opts) {
10575
10761
  send(res, 400, { error: "invalid box-status payload" });
10576
10762
  return;
10577
10763
  }
10578
- await statusStore.set(reg.boxId, body.payload);
10764
+ await statusStore.set(reg.boxId, reg.name, reg.projectIndex, body.payload);
10579
10765
  log(`box-status box=${reg.boxId}`);
10580
10766
  send(res, 202, { ok: true });
10581
10767
  return;
@@ -10599,12 +10785,78 @@ function createRelayServer(opts) {
10599
10785
  return;
10600
10786
  }
10601
10787
  log(`rpc box=${reg.boxId} method=${body.method}`);
10602
- if (body.method === "git.pull" || body.method === "git.push") {
10788
+ if (body.method === "git.push" || body.method === "git.fetch") {
10789
+ if (body.method === "git.push") {
10790
+ const params = body.params;
10791
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
10792
+ kind: "confirm",
10793
+ message: `Allow git push from box ${reg.name}?`,
10794
+ detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
10795
+ defaultAnswer: "n",
10796
+ context: {
10797
+ command: "git push",
10798
+ cwd: params?.path,
10799
+ argv: params?.args
10800
+ }
10801
+ });
10802
+ if (verdict.answer !== "y") {
10803
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
10804
+ return;
10805
+ }
10806
+ }
10603
10807
  const result = await handleGitRpc(reg, body.method, body.params);
10604
10808
  const status = result.exitCode === 0 ? 200 : 500;
10605
10809
  send(res, status, result);
10606
10810
  return;
10607
10811
  }
10812
+ if (body.method === "cp.toHost" || body.method === "cp.fromHost") {
10813
+ const params = body.params;
10814
+ if (!params || typeof params.boxPath !== "string" || typeof params.hostPath !== "string") {
10815
+ send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
10816
+ return;
10817
+ }
10818
+ const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
10819
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
10820
+ kind: "confirm",
10821
+ message: `Allow cp (${direction}) on ${reg.name}?`,
10822
+ detail: body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`,
10823
+ defaultAnswer: "n",
10824
+ context: {
10825
+ command: body.method,
10826
+ argv: [params.boxPath, params.hostPath]
10827
+ }
10828
+ });
10829
+ if (verdict.answer !== "y") {
10830
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
10831
+ return;
10832
+ }
10833
+ const result = await handleCpRpc(reg, body.method, params);
10834
+ const status = result.exitCode === 0 ? 200 : 500;
10835
+ send(res, status, result);
10836
+ return;
10837
+ }
10838
+ if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
10839
+ const params = body.params;
10840
+ const kind = body.method.split(".")[1] ?? "workspace";
10841
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
10842
+ kind: "confirm",
10843
+ message: `Allow download (${kind}) from ${reg.name}?`,
10844
+ detail: params?.hostPath ?? "(default host location)",
10845
+ defaultAnswer: "n",
10846
+ context: {
10847
+ command: body.method,
10848
+ argv: params?.hostPath ? [params.hostPath] : []
10849
+ }
10850
+ });
10851
+ if (verdict.answer !== "y") {
10852
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
10853
+ return;
10854
+ }
10855
+ const result = await handleDownloadRpc(reg, kind);
10856
+ const status = result.exitCode === 0 ? 200 : 500;
10857
+ send(res, status, result);
10858
+ return;
10859
+ }
10608
10860
  if (body.method === "checkpoint.create") {
10609
10861
  const result = await handleCheckpointRpc(
10610
10862
  reg,
@@ -10614,6 +10866,23 @@ function createRelayServer(opts) {
10614
10866
  send(res, status, result);
10615
10867
  return;
10616
10868
  }
10869
+ if (body.method === "browser.open") {
10870
+ const params = body.params;
10871
+ const url2 = typeof params?.url === "string" ? params.url.trim() : "";
10872
+ if (!isOpenableUrl(url2)) {
10873
+ send(res, 400, {
10874
+ exitCode: 64,
10875
+ stdout: "",
10876
+ stderr: "browser.open: only http/https URLs are allowed\n"
10877
+ });
10878
+ return;
10879
+ }
10880
+ events.append({ boxId: reg.boxId, type: "browser-open", payload: { url: url2 } });
10881
+ const result = await runHostCommand(["open", url2], BROWSER_OPEN_RPC_TIMEOUT_MS);
10882
+ const status = result.exitCode === 0 ? 200 : 500;
10883
+ send(res, status, result);
10884
+ return;
10885
+ }
10617
10886
  events.append({
10618
10887
  boxId: reg.boxId,
10619
10888
  type: "rpc-unknown",
@@ -10629,6 +10898,7 @@ function createRelayServer(opts) {
10629
10898
  return;
10630
10899
  }
10631
10900
  const worktrees = sanitizeWorktrees(body.worktrees);
10901
+ const projectIndex = typeof body.projectIndex === "number" && Number.isFinite(body.projectIndex) && body.projectIndex > 0 ? Math.trunc(body.projectIndex) : void 0;
10632
10902
  const reg = {
10633
10903
  boxId: body.boxId,
10634
10904
  token: body.token,
@@ -10636,6 +10906,7 @@ function createRelayServer(opts) {
10636
10906
  registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
10637
10907
  containerName: typeof body.containerName === "string" && body.containerName.length > 0 ? body.containerName : void 0,
10638
10908
  createdAt: typeof body.createdAt === "string" && body.createdAt.length > 0 ? body.createdAt : void 0,
10909
+ projectIndex,
10639
10910
  worktrees
10640
10911
  };
10641
10912
  registry.register(reg);
@@ -10681,11 +10952,95 @@ function createRelayServer(opts) {
10681
10952
  registeredAt: r.registeredAt,
10682
10953
  containerName: r.containerName,
10683
10954
  createdAt: r.createdAt,
10955
+ projectIndex: r.projectIndex,
10684
10956
  worktrees: r.worktrees ?? []
10685
10957
  }));
10686
10958
  send(res, 200, { boxes: redacted });
10687
10959
  return;
10688
10960
  }
10961
+ if (route === "GET /admin/prompts/stream") {
10962
+ const boxId = url.searchParams.get("boxId") ?? "";
10963
+ if (boxId.length === 0) {
10964
+ send(res, 400, { error: "missing boxId query param" });
10965
+ return;
10966
+ }
10967
+ res.statusCode = 200;
10968
+ res.setHeader("Content-Type", "text/event-stream");
10969
+ res.setHeader("Cache-Control", "no-cache");
10970
+ res.setHeader("Connection", "keep-alive");
10971
+ res.setHeader("X-Accel-Buffering", "no");
10972
+ if (typeof res.flushHeaders === "function") res.flushHeaders();
10973
+ res.write(": connected\n\n");
10974
+ subscribers.add(boxId, res);
10975
+ for (const ev of prompts.forBox(boxId)) {
10976
+ res.write(`event: prompt-ask
10977
+ data: ${JSON.stringify(ev)}
10978
+
10979
+ `);
10980
+ }
10981
+ for (const ev of notices.forBox(boxId)) {
10982
+ res.write(`event: notice-set
10983
+ data: ${JSON.stringify(ev)}
10984
+
10985
+ `);
10986
+ }
10987
+ const heartbeat = setInterval(() => {
10988
+ try {
10989
+ res.write(`event: ping
10990
+ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
10991
+
10992
+ `);
10993
+ } catch {
10994
+ }
10995
+ }, SSE_HEARTBEAT_MS);
10996
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
10997
+ res.on("close", () => {
10998
+ clearInterval(heartbeat);
10999
+ subscribers.remove(boxId, res);
11000
+ });
11001
+ return;
11002
+ }
11003
+ if (route === "POST /admin/prompts/answer") {
11004
+ const body = await readJsonBody(req);
11005
+ if (!isPromptAnswerBody(body)) {
11006
+ send(res, 400, { error: 'expected {id, answer:"y"|"n", cancelled?}' });
11007
+ return;
11008
+ }
11009
+ const targetBox = prompts.boxFor(body.id);
11010
+ const hit = prompts.resolve(body.id, body.answer, body.cancelled);
11011
+ if (!hit) {
11012
+ send(res, 404, { error: "no pending prompt with that id" });
11013
+ return;
11014
+ }
11015
+ if (targetBox) {
11016
+ subscribers.broadcast(targetBox, "prompt-resolved", { id: body.id });
11017
+ }
11018
+ send(res, 204, null);
11019
+ return;
11020
+ }
11021
+ if (route === "POST /admin/notices/set") {
11022
+ const body = await readJsonBody(req);
11023
+ 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) {
11024
+ send(res, 400, { error: "expected {boxId, kind, message}" });
11025
+ return;
11026
+ }
11027
+ const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
11028
+ const id = notices.set(body.boxId, body.kind, body.message, ttlMs);
11029
+ log(`notice-set box=${body.boxId} kind=${body.kind} id=${id}`);
11030
+ send(res, 200, { id });
11031
+ return;
11032
+ }
11033
+ if (route === "POST /admin/notices/clear") {
11034
+ const body = await readJsonBody(req);
11035
+ if (!body || typeof body.id !== "string" || body.id.length === 0) {
11036
+ send(res, 400, { error: "expected {boxId, id}" });
11037
+ return;
11038
+ }
11039
+ notices.clear(body.id);
11040
+ log(`notice-clear id=${body.id}`);
11041
+ send(res, 204, null);
11042
+ return;
11043
+ }
10689
11044
  send(res, 404, { error: "not found", route });
10690
11045
  }
10691
11046
  function authBox(req, res, reg) {
@@ -10706,6 +11061,9 @@ function createRelayServer(opts) {
10706
11061
  registry,
10707
11062
  events,
10708
11063
  statusStore,
11064
+ prompts,
11065
+ subscribers,
11066
+ notices,
10709
11067
  url: `http://${host}:${String(opts.port)}`,
10710
11068
  close: () => new Promise((resolve2, reject) => {
10711
11069
  server.close((err) => {
@@ -10719,10 +11077,10 @@ function sanitizeWorktrees(input) {
10719
11077
  if (!Array.isArray(input)) return void 0;
10720
11078
  const out = [];
10721
11079
  for (const w of input) {
10722
- if (w && typeof w.containerPath === "string" && typeof w.hostWorktreeDir === "string" && typeof w.branch === "string") {
11080
+ if (w && typeof w.containerPath === "string" && typeof w.hostMainRepo === "string" && typeof w.branch === "string") {
10723
11081
  out.push({
10724
11082
  containerPath: w.containerPath,
10725
- hostWorktreeDir: w.hostWorktreeDir,
11083
+ hostMainRepo: w.hostMainRepo,
10726
11084
  branch: w.branch
10727
11085
  });
10728
11086
  }
@@ -10747,9 +11105,9 @@ async function handleGitRpc(reg, method, params) {
10747
11105
  stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
10748
11106
  };
10749
11107
  }
10750
- const op = method === "git.pull" ? "pull" : "push";
11108
+ const op = method === "git.push" ? "push" : "fetch";
10751
11109
  const remote = params?.remote ?? "origin";
10752
- const argv = ["git", "-C", worktree.hostWorktreeDir, op, remote];
11110
+ const argv = ["git", "-C", worktree.hostMainRepo, op, remote, worktree.branch];
10753
11111
  if (Array.isArray(params?.args)) {
10754
11112
  for (const a of params.args) {
10755
11113
  if (typeof a === "string") argv.push(a);
@@ -10757,6 +11115,33 @@ async function handleGitRpc(reg, method, params) {
10757
11115
  }
10758
11116
  return runHostCommand(argv);
10759
11117
  }
11118
+ async function handleCpRpc(reg, method, params) {
11119
+ const entry = process.env.AGENTBOX_CLI_ENTRY;
11120
+ if (!entry) {
11121
+ return {
11122
+ exitCode: 64,
11123
+ stdout: "",
11124
+ stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run cp host-side"
11125
+ };
11126
+ }
11127
+ const boxRef = `${reg.name}:${params.boxPath}`;
11128
+ const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath] : [process.execPath, entry, "cp", params.hostPath, boxRef];
11129
+ return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
11130
+ }
11131
+ async function handleDownloadRpc(reg, kind) {
11132
+ const entry = process.env.AGENTBOX_CLI_ENTRY;
11133
+ if (!entry) {
11134
+ return {
11135
+ exitCode: 64,
11136
+ stdout: "",
11137
+ stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run download host-side"
11138
+ };
11139
+ }
11140
+ const argv = [process.execPath, entry, "download"];
11141
+ if (kind !== "workspace") argv.push(kind);
11142
+ argv.push(reg.name, "-y");
11143
+ return runHostCommand(argv, DOWNLOAD_RPC_TIMEOUT_MS);
11144
+ }
10760
11145
  async function handleCheckpointRpc(reg, params) {
10761
11146
  const entry = process.env.AGENTBOX_CLI_ENTRY;
10762
11147
  if (!entry) {
@@ -10770,8 +11155,18 @@ async function handleCheckpointRpc(reg, params) {
10770
11155
  if (params?.name) argv.push("--name", params.name);
10771
11156
  if (params?.merged === true) argv.push("--merged");
10772
11157
  if (params?.setDefault === true) argv.push("--set-default");
11158
+ if (params?.replace === true) argv.push("--replace");
10773
11159
  return runHostCommand(argv, CHECKPOINT_RPC_TIMEOUT_MS);
10774
11160
  }
11161
+ function isOpenableUrl(value) {
11162
+ let url;
11163
+ try {
11164
+ url = new URL(value);
11165
+ } catch {
11166
+ return false;
11167
+ }
11168
+ return url.protocol === "http:" || url.protocol === "https:";
11169
+ }
10775
11170
  function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
10776
11171
  return new Promise((resolve2) => {
10777
11172
  const [cmd, ...rest] = argv;