@madarco/agentbox 0.4.1 → 0.5.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 (38) hide show
  1. package/dist/{chunk-WR5FFGE5.js → chunk-6VTAPD4H.js} +123 -112
  2. package/dist/chunk-6VTAPD4H.js.map +1 -0
  3. package/dist/{chunk-J35IH7W5.js → chunk-7J5AJLWG.js} +61 -23
  4. package/dist/chunk-7J5AJLWG.js.map +1 -0
  5. package/dist/{chunk-FQD6ZWYW.js → chunk-FJNIFTWK.js} +66 -65
  6. package/dist/chunk-FJNIFTWK.js.map +1 -0
  7. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  8. package/dist/chunk-HPZMD5DE.js.map +1 -0
  9. package/dist/{chunk-NSIECUCS.js → chunk-PXUBE5KS.js} +365 -258
  10. package/dist/chunk-PXUBE5KS.js.map +1 -0
  11. package/dist/{chunk-SOMIKEN2.js → chunk-RFC5F5HR.js} +272 -214
  12. package/dist/chunk-RFC5F5HR.js.map +1 -0
  13. package/dist/create-AHZ3GVEZ-TGEDL7UX.js +15 -0
  14. package/dist/index.js +2757 -1854
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-LFOL6YFM-TCHDX3J5.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-Z4BVJODD-HEC4TMUZ.js +19 -0
  19. package/package.json +5 -4
  20. package/runtime/docker/Dockerfile.box +47 -19
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +39 -50
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +219 -148
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +42 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +26 -15
  25. package/runtime/relay/bin.cjs +288 -12
  26. package/share/agentbox-setup/SKILL.md +39 -50
  27. package/dist/chunk-FQD6ZWYW.js.map +0 -1
  28. package/dist/chunk-IDR4HVIC.js.map +0 -1
  29. package/dist/chunk-J35IH7W5.js.map +0 -1
  30. package/dist/chunk-NSIECUCS.js.map +0 -1
  31. package/dist/chunk-SOMIKEN2.js.map +0 -1
  32. package/dist/chunk-WR5FFGE5.js.map +0 -1
  33. package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
  34. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  35. /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-AHZ3GVEZ-TGEDL7UX.js.map} +0 -0
  36. /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-LFOL6YFM-TCHDX3J5.js.map} +0 -0
  37. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  38. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-Z4BVJODD-HEC4TMUZ.js.map} +0 -0
@@ -10381,6 +10381,110 @@ 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/prompts.ts
10385
+ var import_node_crypto = require("crypto");
10386
+ var PendingPrompts = class {
10387
+ entries = /* @__PURE__ */ new Map();
10388
+ add(boxId, ev) {
10389
+ return new Promise((resolve2) => {
10390
+ this.entries.set(ev.id, {
10391
+ ev,
10392
+ boxId,
10393
+ resolve: resolve2,
10394
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
10395
+ });
10396
+ });
10397
+ }
10398
+ /**
10399
+ * Idempotent: returns true if a pending entry was found + resolved, false
10400
+ * otherwise. The /admin/prompts/answer handler uses the bool to decide
10401
+ * 204 vs 404 — the wrapper treats both as "we're done."
10402
+ */
10403
+ resolve(id, answer, cancelled) {
10404
+ const entry = this.entries.get(id);
10405
+ if (!entry) return false;
10406
+ this.entries.delete(id);
10407
+ entry.resolve({ answer, cancelled });
10408
+ return true;
10409
+ }
10410
+ /**
10411
+ * Snapshot of all pending prompts for a given box; used to flush the
10412
+ * backlog to a newly-attached SSE subscriber.
10413
+ */
10414
+ forBox(boxId) {
10415
+ const out = [];
10416
+ for (const entry of this.entries.values()) {
10417
+ if (entry.boxId === boxId) out.push(entry.ev);
10418
+ }
10419
+ return out;
10420
+ }
10421
+ /** boxId that owns a pending prompt id, or null when unknown. */
10422
+ boxFor(id) {
10423
+ const entry = this.entries.get(id);
10424
+ return entry ? entry.boxId : null;
10425
+ }
10426
+ size() {
10427
+ return this.entries.size;
10428
+ }
10429
+ };
10430
+ var PromptSubscribers = class {
10431
+ byBox = /* @__PURE__ */ new Map();
10432
+ add(boxId, res) {
10433
+ let set = this.byBox.get(boxId);
10434
+ if (!set) {
10435
+ set = /* @__PURE__ */ new Set();
10436
+ this.byBox.set(boxId, set);
10437
+ }
10438
+ set.add(res);
10439
+ }
10440
+ remove(boxId, res) {
10441
+ const set = this.byBox.get(boxId);
10442
+ if (!set) return;
10443
+ set.delete(res);
10444
+ if (set.size === 0) this.byBox.delete(boxId);
10445
+ }
10446
+ forBox(boxId) {
10447
+ const set = this.byBox.get(boxId);
10448
+ return set ? Array.from(set) : [];
10449
+ }
10450
+ /**
10451
+ * Fire-and-forget broadcast. SSE writes that fail (closed socket) are
10452
+ * swallowed — the `res.on('close')` handler in the server route already
10453
+ * deregisters the dead subscriber.
10454
+ */
10455
+ broadcast(boxId, event, data) {
10456
+ const set = this.byBox.get(boxId);
10457
+ if (!set) return;
10458
+ const payload = `event: ${event}
10459
+ data: ${JSON.stringify(data)}
10460
+
10461
+ `;
10462
+ for (const res of set) {
10463
+ try {
10464
+ res.write(payload);
10465
+ } catch {
10466
+ }
10467
+ }
10468
+ }
10469
+ };
10470
+ async function askPrompt(prompts, subscribers, boxId, params) {
10471
+ if (process.env.AGENTBOX_PROMPT === "off") {
10472
+ return { answer: "y" };
10473
+ }
10474
+ const ev = { id: (0, import_node_crypto.randomUUID)(), ...params };
10475
+ const promise = prompts.add(boxId, ev);
10476
+ subscribers.broadcast(boxId, "prompt-ask", ev);
10477
+ return promise;
10478
+ }
10479
+ function isPromptAnswerBody(v) {
10480
+ if (!v || typeof v !== "object") return false;
10481
+ const o = v;
10482
+ if (typeof o.id !== "string" || o.id.length === 0) return false;
10483
+ if (o.answer !== "y" && o.answer !== "n") return false;
10484
+ if (o.cancelled !== void 0 && typeof o.cancelled !== "boolean") return false;
10485
+ return true;
10486
+ }
10487
+
10384
10488
  // src/registry.ts
10385
10489
  var BoxRegistry = class {
10386
10490
  map = /* @__PURE__ */ new Map();
@@ -10447,8 +10551,16 @@ var EventBuffer = class {
10447
10551
  var import_promises = require("fs/promises");
10448
10552
  var import_node_os = require("os");
10449
10553
  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");
10554
+ function sanitizeMnemonic(raw) {
10555
+ return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
10556
+ }
10557
+ function boxRunDirFor(boxId, name, projectIndex) {
10558
+ const mnemonic = sanitizeMnemonic(name);
10559
+ const segment = typeof projectIndex === "number" && Number.isFinite(projectIndex) && projectIndex > 0 ? `${boxId}-${String(projectIndex)}-${mnemonic}` : `${boxId}-${mnemonic}`;
10560
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentbox", "boxes", segment);
10561
+ }
10562
+ function boxStatusPathFor(boxId, name, projectIndex) {
10563
+ return (0, import_node_path.join)(boxRunDirFor(boxId, name, projectIndex), "status.json");
10452
10564
  }
10453
10565
  function isValidBoxStatus(payload) {
10454
10566
  if (typeof payload !== "object" || payload === null) return false;
@@ -10460,13 +10572,19 @@ var BoxStatusStore = class {
10460
10572
  get(boxId) {
10461
10573
  return this.map.get(boxId);
10462
10574
  }
10463
- /** Update the in-memory entry and best-effort persist it to disk. */
10464
- async set(boxId, status) {
10575
+ /**
10576
+ * Update the in-memory entry and best-effort persist it to disk. `name` is
10577
+ * the box's user-facing name (from the registry); `projectIndex` is the
10578
+ * 1-based per-project `N`. Together they form the on-disk dir
10579
+ * `~/.agentbox/boxes/<id>-<n>-<mnemonic>/status.json` (or
10580
+ * `<id>-<mnemonic>/` if N is absent — legacy boxes).
10581
+ */
10582
+ async set(boxId, name, projectIndex, status) {
10465
10583
  this.map.set(boxId, status);
10466
- const target = boxStatusPathFor(boxId);
10584
+ const target = boxStatusPathFor(boxId, name, projectIndex);
10467
10585
  const tmp = `${target}.${String(process.pid)}.tmp`;
10468
10586
  try {
10469
- await (0, import_promises.mkdir)((0, import_node_path.join)((0, import_node_os.homedir)(), ".agentbox", "boxes", boxId), { recursive: true });
10587
+ await (0, import_promises.mkdir)(boxRunDirFor(boxId, name, projectIndex), { recursive: true });
10470
10588
  await (0, import_promises.writeFile)(tmp, JSON.stringify(status), "utf8");
10471
10589
  await (0, import_promises.rename)(tmp, target);
10472
10590
  } catch {
@@ -10485,6 +10603,9 @@ var BOX_STATUS_EVENT = "box-status";
10485
10603
  var MAX_BODY_BYTES = 1024 * 1024;
10486
10604
  var GIT_RPC_TIMEOUT_MS = 12e4;
10487
10605
  var CHECKPOINT_RPC_TIMEOUT_MS = 6e5;
10606
+ var DOWNLOAD_RPC_TIMEOUT_MS = 6e5;
10607
+ var CP_RPC_TIMEOUT_MS = 3e5;
10608
+ var SSE_HEARTBEAT_MS = 15e3;
10488
10609
  function send(res, status, body, contentType = "application/json") {
10489
10610
  const text = body == null ? "" : typeof body === "string" ? body : JSON.stringify(body);
10490
10611
  res.statusCode = status;
@@ -10540,6 +10661,8 @@ function createRelayServer(opts) {
10540
10661
  const registry = new BoxRegistry();
10541
10662
  const events = new EventBuffer();
10542
10663
  const statusStore = new BoxStatusStore();
10664
+ const prompts = new PendingPrompts();
10665
+ const subscribers = new PromptSubscribers();
10543
10666
  const host = opts.host ?? "0.0.0.0";
10544
10667
  const server = (0, import_node_http.createServer)((req, res) => {
10545
10668
  handle(req, res).catch((err) => {
@@ -10575,7 +10698,7 @@ function createRelayServer(opts) {
10575
10698
  send(res, 400, { error: "invalid box-status payload" });
10576
10699
  return;
10577
10700
  }
10578
- await statusStore.set(reg.boxId, body.payload);
10701
+ await statusStore.set(reg.boxId, reg.name, reg.projectIndex, body.payload);
10579
10702
  log(`box-status box=${reg.boxId}`);
10580
10703
  send(res, 202, { ok: true });
10581
10704
  return;
@@ -10599,12 +10722,78 @@ function createRelayServer(opts) {
10599
10722
  return;
10600
10723
  }
10601
10724
  log(`rpc box=${reg.boxId} method=${body.method}`);
10602
- if (body.method === "git.pull" || body.method === "git.push") {
10725
+ if (body.method === "git.push" || body.method === "git.fetch") {
10726
+ if (body.method === "git.push") {
10727
+ const params = body.params;
10728
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
10729
+ kind: "confirm",
10730
+ message: `Allow git push from box ${reg.name}?`,
10731
+ detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
10732
+ defaultAnswer: "n",
10733
+ context: {
10734
+ command: "git push",
10735
+ cwd: params?.path,
10736
+ argv: params?.args
10737
+ }
10738
+ });
10739
+ if (verdict.answer !== "y") {
10740
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
10741
+ return;
10742
+ }
10743
+ }
10603
10744
  const result = await handleGitRpc(reg, body.method, body.params);
10604
10745
  const status = result.exitCode === 0 ? 200 : 500;
10605
10746
  send(res, status, result);
10606
10747
  return;
10607
10748
  }
10749
+ if (body.method === "cp.toHost" || body.method === "cp.fromHost") {
10750
+ const params = body.params;
10751
+ if (!params || typeof params.boxPath !== "string" || typeof params.hostPath !== "string") {
10752
+ send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
10753
+ return;
10754
+ }
10755
+ const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
10756
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
10757
+ kind: "confirm",
10758
+ message: `Allow cp (${direction}) on ${reg.name}?`,
10759
+ detail: body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`,
10760
+ defaultAnswer: "n",
10761
+ context: {
10762
+ command: body.method,
10763
+ argv: [params.boxPath, params.hostPath]
10764
+ }
10765
+ });
10766
+ if (verdict.answer !== "y") {
10767
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
10768
+ return;
10769
+ }
10770
+ const result = await handleCpRpc(reg, body.method, params);
10771
+ const status = result.exitCode === 0 ? 200 : 500;
10772
+ send(res, status, result);
10773
+ return;
10774
+ }
10775
+ if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
10776
+ const params = body.params;
10777
+ const kind = body.method.split(".")[1] ?? "workspace";
10778
+ const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
10779
+ kind: "confirm",
10780
+ message: `Allow download (${kind}) from ${reg.name}?`,
10781
+ detail: params?.hostPath ?? "(default host location)",
10782
+ defaultAnswer: "n",
10783
+ context: {
10784
+ command: body.method,
10785
+ argv: params?.hostPath ? [params.hostPath] : []
10786
+ }
10787
+ });
10788
+ if (verdict.answer !== "y") {
10789
+ send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
10790
+ return;
10791
+ }
10792
+ const result = await handleDownloadRpc(reg, kind);
10793
+ const status = result.exitCode === 0 ? 200 : 500;
10794
+ send(res, status, result);
10795
+ return;
10796
+ }
10608
10797
  if (body.method === "checkpoint.create") {
10609
10798
  const result = await handleCheckpointRpc(
10610
10799
  reg,
@@ -10629,6 +10818,7 @@ function createRelayServer(opts) {
10629
10818
  return;
10630
10819
  }
10631
10820
  const worktrees = sanitizeWorktrees(body.worktrees);
10821
+ const projectIndex = typeof body.projectIndex === "number" && Number.isFinite(body.projectIndex) && body.projectIndex > 0 ? Math.trunc(body.projectIndex) : void 0;
10632
10822
  const reg = {
10633
10823
  boxId: body.boxId,
10634
10824
  token: body.token,
@@ -10636,6 +10826,7 @@ function createRelayServer(opts) {
10636
10826
  registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
10637
10827
  containerName: typeof body.containerName === "string" && body.containerName.length > 0 ? body.containerName : void 0,
10638
10828
  createdAt: typeof body.createdAt === "string" && body.createdAt.length > 0 ? body.createdAt : void 0,
10829
+ projectIndex,
10639
10830
  worktrees
10640
10831
  };
10641
10832
  registry.register(reg);
@@ -10681,11 +10872,66 @@ function createRelayServer(opts) {
10681
10872
  registeredAt: r.registeredAt,
10682
10873
  containerName: r.containerName,
10683
10874
  createdAt: r.createdAt,
10875
+ projectIndex: r.projectIndex,
10684
10876
  worktrees: r.worktrees ?? []
10685
10877
  }));
10686
10878
  send(res, 200, { boxes: redacted });
10687
10879
  return;
10688
10880
  }
10881
+ if (route === "GET /admin/prompts/stream") {
10882
+ const boxId = url.searchParams.get("boxId") ?? "";
10883
+ if (boxId.length === 0) {
10884
+ send(res, 400, { error: "missing boxId query param" });
10885
+ return;
10886
+ }
10887
+ res.statusCode = 200;
10888
+ res.setHeader("Content-Type", "text/event-stream");
10889
+ res.setHeader("Cache-Control", "no-cache");
10890
+ res.setHeader("Connection", "keep-alive");
10891
+ res.setHeader("X-Accel-Buffering", "no");
10892
+ if (typeof res.flushHeaders === "function") res.flushHeaders();
10893
+ res.write(": connected\n\n");
10894
+ subscribers.add(boxId, res);
10895
+ for (const ev of prompts.forBox(boxId)) {
10896
+ res.write(`event: prompt-ask
10897
+ data: ${JSON.stringify(ev)}
10898
+
10899
+ `);
10900
+ }
10901
+ const heartbeat = setInterval(() => {
10902
+ try {
10903
+ res.write(`event: ping
10904
+ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
10905
+
10906
+ `);
10907
+ } catch {
10908
+ }
10909
+ }, SSE_HEARTBEAT_MS);
10910
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
10911
+ res.on("close", () => {
10912
+ clearInterval(heartbeat);
10913
+ subscribers.remove(boxId, res);
10914
+ });
10915
+ return;
10916
+ }
10917
+ if (route === "POST /admin/prompts/answer") {
10918
+ const body = await readJsonBody(req);
10919
+ if (!isPromptAnswerBody(body)) {
10920
+ send(res, 400, { error: 'expected {id, answer:"y"|"n", cancelled?}' });
10921
+ return;
10922
+ }
10923
+ const targetBox = prompts.boxFor(body.id);
10924
+ const hit = prompts.resolve(body.id, body.answer, body.cancelled);
10925
+ if (!hit) {
10926
+ send(res, 404, { error: "no pending prompt with that id" });
10927
+ return;
10928
+ }
10929
+ if (targetBox) {
10930
+ subscribers.broadcast(targetBox, "prompt-resolved", { id: body.id });
10931
+ }
10932
+ send(res, 204, null);
10933
+ return;
10934
+ }
10689
10935
  send(res, 404, { error: "not found", route });
10690
10936
  }
10691
10937
  function authBox(req, res, reg) {
@@ -10706,6 +10952,8 @@ function createRelayServer(opts) {
10706
10952
  registry,
10707
10953
  events,
10708
10954
  statusStore,
10955
+ prompts,
10956
+ subscribers,
10709
10957
  url: `http://${host}:${String(opts.port)}`,
10710
10958
  close: () => new Promise((resolve2, reject) => {
10711
10959
  server.close((err) => {
@@ -10719,10 +10967,10 @@ function sanitizeWorktrees(input) {
10719
10967
  if (!Array.isArray(input)) return void 0;
10720
10968
  const out = [];
10721
10969
  for (const w of input) {
10722
- if (w && typeof w.containerPath === "string" && typeof w.hostWorktreeDir === "string" && typeof w.branch === "string") {
10970
+ if (w && typeof w.containerPath === "string" && typeof w.hostMainRepo === "string" && typeof w.branch === "string") {
10723
10971
  out.push({
10724
10972
  containerPath: w.containerPath,
10725
- hostWorktreeDir: w.hostWorktreeDir,
10973
+ hostMainRepo: w.hostMainRepo,
10726
10974
  branch: w.branch
10727
10975
  });
10728
10976
  }
@@ -10747,9 +10995,9 @@ async function handleGitRpc(reg, method, params) {
10747
10995
  stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
10748
10996
  };
10749
10997
  }
10750
- const op = method === "git.pull" ? "pull" : "push";
10998
+ const op = method === "git.push" ? "push" : "fetch";
10751
10999
  const remote = params?.remote ?? "origin";
10752
- const argv = ["git", "-C", worktree.hostWorktreeDir, op, remote];
11000
+ const argv = ["git", "-C", worktree.hostMainRepo, op, remote, worktree.branch];
10753
11001
  if (Array.isArray(params?.args)) {
10754
11002
  for (const a of params.args) {
10755
11003
  if (typeof a === "string") argv.push(a);
@@ -10757,6 +11005,33 @@ async function handleGitRpc(reg, method, params) {
10757
11005
  }
10758
11006
  return runHostCommand(argv);
10759
11007
  }
11008
+ async function handleCpRpc(reg, method, params) {
11009
+ const entry = process.env.AGENTBOX_CLI_ENTRY;
11010
+ if (!entry) {
11011
+ return {
11012
+ exitCode: 64,
11013
+ stdout: "",
11014
+ stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run cp host-side"
11015
+ };
11016
+ }
11017
+ const boxRef = `${reg.name}:${params.boxPath}`;
11018
+ const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath] : [process.execPath, entry, "cp", params.hostPath, boxRef];
11019
+ return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
11020
+ }
11021
+ async function handleDownloadRpc(reg, kind) {
11022
+ const entry = process.env.AGENTBOX_CLI_ENTRY;
11023
+ if (!entry) {
11024
+ return {
11025
+ exitCode: 64,
11026
+ stdout: "",
11027
+ stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run download host-side"
11028
+ };
11029
+ }
11030
+ const argv = [process.execPath, entry, "download"];
11031
+ if (kind !== "workspace") argv.push(kind);
11032
+ argv.push(reg.name, "-y");
11033
+ return runHostCommand(argv, DOWNLOAD_RPC_TIMEOUT_MS);
11034
+ }
10760
11035
  async function handleCheckpointRpc(reg, params) {
10761
11036
  const entry = process.env.AGENTBOX_CLI_ENTRY;
10762
11037
  if (!entry) {
@@ -10770,6 +11045,7 @@ async function handleCheckpointRpc(reg, params) {
10770
11045
  if (params?.name) argv.push("--name", params.name);
10771
11046
  if (params?.merged === true) argv.push("--merged");
10772
11047
  if (params?.setDefault === true) argv.push("--set-default");
11048
+ if (params?.replace === true) argv.push("--replace");
10773
11049
  return runHostCommand(argv, CHECKPOINT_RPC_TIMEOUT_MS);
10774
11050
  }
10775
11051
  function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
@@ -5,7 +5,21 @@ description: Generate an agentbox.yaml for the current AgentBox workspace. Invok
5
5
 
6
6
  # /agentbox-setup
7
7
 
8
- Goal: produce a `/workspace/agentbox.yaml` that captures this project's services, tasks, and box defaults so the in-box supervisor (`agentbox-ctl`) can boot the workspace deterministically.
8
+ ## Box layout (what you're configuring against)
9
+
10
+ Your user i `vscode` and you can use passwordless sudo to run commands as root.
11
+
12
+ `/workspace` is the box's plain writable filesystem — a per-box git worktree on a fresh `agentbox/<box-name>` branch (or a tar-piped copy of the host workspace for non-git projects). Anything you install or build into `/workspace` (incl. `node_modules`, `.next`, `target`, `.venv`) lives in the **container's writable layer** and is captured wholesale by `agentbox checkpoint` (`docker commit`) — so a setup task that runs the install once becomes a warm-start asset for every future box in the project. Everything is wiped on `agentbox destroy`.
13
+
14
+ Three bind mounts wire the box back to the host:
15
+
16
+ - **Host main repo's `.git/`** — bind-mounted RW at its identical absolute host path. In-box commits land on the host's branch refs (visible to `git log` on the host immediately); the box itself carries no SSH/git creds, so `git push` goes through the host relay (`agentbox-ctl git push`). The host's **working tree is never written to** — only refs/objects under `.git/`.
17
+ - **`~/.claude`** — a Docker named volume (`agentbox-claude-config`, shared across boxes by default) seeded from the host's `~/.claude` on each create so auth, skills, and plugins persist without leaking the host's home dir.
18
+ - **`agentbox.yaml`** — read by `agentbox-ctl` from `/workspace`. Tasks and services declared here are what the supervisor will run.
19
+
20
+ ## Goal
21
+
22
+ Produce a `/workspace/agentbox.yaml` that captures this project's services, tasks, and box defaults so the in-box supervisor (`agentbox-ctl`) can boot the workspace deterministically.
9
23
 
10
24
  `agentbox.yaml` is **declarative**. The supervisor reads it on box start, but you don't have to restart the box: after you write the file, `agentbox-ctl reload` (run from inside the box) makes the already-running supervisor re-read it and immediately run the declared tasks and autostart the services. See step 8.
11
25
 
@@ -23,10 +37,12 @@ Look at `/workspace`:
23
37
  ## 2. Pick services and tasks
24
38
 
25
39
  - **Services** = long-running. Web servers, watchers, queue workers, databases. `restart: on-failure` by default.
26
- - **Tasks** = one-shot. `pnpm install`, DB migrations, codegen, fixture loaders. Wire dependent services with `needs:` so they wait for the task to finish successfully.
40
+ - **Tasks** = one-shot. `pnpm install`, DB migrations, codegen, fixture loaders, install apt packages. Wire dependent services with `needs:` so they wait for the task to finish successfully.
27
41
  - Names: must match `[A-Za-z0-9_-]+`. Task names and service names share a namespace — no collisions.
28
42
  - No cycles in `needs:`.
29
- - **Always generate a dependency-install task** and make it the root of the `needs:` graph (every service that needs deps gets `needs: [install, …]`). The box runs on Linux; the host's `node_modules` (and `.next`, `target`, `.venv`) are macOS-native and now live in the box's writable upper layer, so they must be rebuilt **inside** the box. The task must be **idempotent and self-healing**: `agentbox-ctl` re-runs pending tasks on every box stop/start (the daemon dies with the container and is relaunched), so a plain `rm -rf node_modules && install` would wipe + reinstall on every start. Guard the rebuild with a marker file *inside* `node_modules` (the `.agentbox-installed` convention AgentBox uses internally): rebuild only when the marker is absent (fresh box, or a stale host-leaked `node_modules`), and be a fast no-op once it exists. Detect the package manager from the lockfile — never hardcode `pnpm`. See the worked example below.
43
+ - **Always generate a dependency-install task** and make it the root of the `needs:` graph (every service that needs deps gets `needs: [install, …]`). Future boxes start from a snapshot of the final filesystem so they won't need this, but updates or moving to a cloud provider might need to rebuild the container from scratch. The filesystem can be then later captured by `agentbox-ctl checkpoint --set-default`. The task must be **idempotent and self-healing**: `agentbox-ctl` re-runs pending tasks on every box stop/start (the daemon dies with the container and is relaunched), so a plain `rm -rf node_modules && install` would wipe + reinstall on every start. Guard the rebuild with a marker file *inside* `node_modules` (the `.agentbox-installed` convention AgentBox uses internally): rebuild only when the marker is absent (fresh box), and be a fast no-op once it exists. Detect the package manager from the lockfile — never hardcode `pnpm`. See the worked example below.
44
+ - **Add a comment to the beginning** of the file to explain what you did and what issues you encountered, so that future run might use this information in case the project evolves and you need to update the agentbox.yaml file.
45
+ -
30
46
 
31
47
  ## 3. Wire readiness probes (services only)
32
48
 
@@ -61,7 +77,7 @@ Per service:
61
77
 
62
78
  Sets per-project defaults for `agentbox create`/`claude`/`code`/`shell` — same shape as `~/.agentbox/config.yaml`. CLI flags still override. Common keys:
63
79
 
64
- - `box.hostSnapshot` (bool) — frozen APFS clone of the *host* workspace as overlay lower (renamed from `box.snapshot`).
80
+ - `box.hostSnapshot` (bool) — APFS-clone the *host* workspace into a per-box scratch dir before seeding `/workspace` (stabilizes the tar-pipe source).
65
81
  - `box.defaultCheckpoint` (string) — checkpoint new boxes start from (normally you set this via `agentbox-ctl checkpoint --set-default` at the end of setup — see section 9, not by hand).
66
82
  - `box.withPlaywright` (bool) — install `@playwright/cli` globally inside the box.
67
83
  - `box.vnc` (bool) — run Xvnc + noVNC on container port 6080.
@@ -76,6 +92,11 @@ Full key list (run on the host): `agentbox config list --keys`.
76
92
 
77
93
  ```yaml
78
94
  # yaml-language-server: $schema=https://agentbox.dev/schema/agentbox.schema.json
95
+ # This agentbox.yaml setup this Next.js project, and includes:
96
+ # - a postgres database because it's used in the project
97
+ # - an inngest server for queues
98
+ # - a fix to move .turbo/cache folder to the workspace to avoid a permission error during setup
99
+ # - ...
79
100
  defaults:
80
101
  box:
81
102
  withPlaywright: true
@@ -83,30 +104,23 @@ defaults:
83
104
  ide: cursor
84
105
 
85
106
  tasks:
86
- # Idempotent install. node_modules lives in the box's writable upper layer
87
- # (per-box, isolated, captured by `agentbox open --upper`). The host's
88
- # node_modules is macOS-native, so force a clean Linux build the first time
89
- # and self-heal a stale one but skip on every subsequent box start
90
- # (agentbox-ctl re-runs pending tasks after stop/start). Adjust the
91
- # lockfile detection to the project's package manager.
107
+ # Idempotent install. /workspace is the container's writable filesystem, so
108
+ # node_modules persists across pause/stop/start and is captured by
109
+ # `agentbox checkpoint`. The host's node_modules is macOS-native and is
110
+ # never copied in, so force a clean Linux build the first time but skip
111
+ # on every subsequent box start (agentbox-ctl re-runs pending tasks after
112
+ # stop/start). Adjust the lockfile detection to the project's package
113
+ # manager.
92
114
  install:
93
115
  command: |
94
116
  set -e
95
117
  MARKER=node_modules/.agentbox-installed
96
118
  [ -f "$MARKER" ] && { echo "deps installed (marker present) — skip"; exit 0; }
119
+ apt-get update && apt-get install -y postgresql-client
97
120
  rm -rf node_modules
98
121
  if [ -f pnpm-lock.yaml ]; then
99
122
  corepack enable >/dev/null 2>&1 || true
100
123
  pnpm install --frozen-lockfile || pnpm install
101
- elif [ -f yarn.lock ]; then
102
- corepack enable >/dev/null 2>&1 || true
103
- yarn install --frozen-lockfile || yarn install
104
- elif [ -f bun.lockb ] || [ -f bun.lock ]; then
105
- bun install
106
- elif [ -f package-lock.json ]; then
107
- npm ci || npm install
108
- else
109
- npm install
110
124
  fi
111
125
  touch "$MARKER"
112
126
 
@@ -149,44 +163,19 @@ services:
149
163
  1. Write the file to `/workspace/agentbox.yaml`.
150
164
  2. **Apply it live**: from inside the box run `agentbox-ctl reload`. The already-running supervisor re-reads the config and immediately runs the declared tasks and autostarts the services — no box restart needed. It prints the `added` / `removed` / `changed` diff. If it errors because the daemon isn't running, the config is still valid: the next `agentbox start` (or `agentbox create` in this workspace) picks it up automatically.
151
165
  3. Confirm with `agentbox-ctl status`: tasks should be `running` or `done`, autostart services `starting` or `ready`. If something failed, tail it with `agentbox-ctl logs <service>` and fix the config, then `agentbox-ctl reload` again.
152
-
153
- 4. **Then do section 9** — once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --set-default` so future boxes start ready. This is the real final step; don't stop at hand-off.
166
+ 4. Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --set-default` so future boxes start ready.
154
167
 
155
168
  5. Tell the user:
156
169
 
157
170
  > I wrote `/workspace/agentbox.yaml` and ran `agentbox-ctl reload` so the supervisor is already running the declared tasks/services. To land the file on the host:
158
171
  > - I've created a checkpoint of the warm box state so future boxes start ready in seconds, no reinstall.
159
172
  > - commit it inside the box (`git add agentbox.yaml && git commit -m 'add agentbox config'`) — the box's `.git/` is bind-mounted, so the commit shows up on the host immediately; or
160
- > - on the host, tell the user to run `agentbox pull config` to update their original host workspace.
161
-
162
-
163
- ## 9. Checkpoint the warm state (do this at the very end)
164
-
165
- Once `agentbox-ctl status` shows the install task `done` and services `ready` — i.e. the box is fully warmed up — capture a **checkpoint** so future boxes in this project start from this exact state instead of cold.
173
+ > - on the host, tell the user to run `agentbox download config` to update their original host workspace.
166
174
 
167
- A checkpoint snapshots the box's writable layer, which includes everything you just did:
168
-
169
- - `node_modules` (and `.next`, `target`, `.venv`, build caches) — the expensive Linux-native install,
170
- - `.env` / `.env.*` / `secrets.toml` and any other gitignored config files present in `/workspace`,
171
- - any other files written during setup.
172
-
173
- A new box created from the checkpoint reuses all of it (the install task's marker is already present, so it no-ops), while git-tracked code still comes fresh from the host's current HEAD. Result: new boxes are ready in seconds with no reinstall and no missing env vars.
174
-
175
- **Run this from inside the box, as the final step:**
176
-
177
- ```sh
178
- agentbox-ctl checkpoint --set-default
179
- ```
180
-
181
- This routes through the host relay (no host shell needed), captures `<box-name>-<n>`, and writes `box.defaultCheckpoint` into the project's config so **every future `agentbox create` / `claude` in this project starts warm automatically**. Pass `--name <name>` for a stable name, or omit `--set-default` to capture without changing the default. Re-run it whenever the warm state meaningfully changes (e.g. after a dependency bump you want every new box to inherit).
182
-
183
- Then tell the user, e.g.:
184
-
185
- > I checkpointed the warm box state (node_modules, env files, build caches) and set it as this project's default — `agentbox create` will now spin up new boxes from it in seconds, no reinstall.
186
-
187
- ## 10. Known issues
175
+ ## 9. Known issues
188
176
 
189
177
  - For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
190
- - **Turbo/Nx/Jest and other git-worktree-aware tools may try to write their cache to a read-only host path.** The box runs `/workspace` as a git worktree with the host repo's `.git/` bind-mounted at its absolute host path; tools that derive paths from git (Turbo's cache root = the worktree's git *common dir*) resolve outside `/workspace` and hit `EROFS` on the Mac path. Pin the cache into the writable overlay via the task/service `env:`, e.g. `TURBO_CACHE_DIR: /workspace/.turbo/cache` (or pass `--cache-dir`).
178
+
191
179
  - The `install` task is intentionally a no-op once `node_modules/.agentbox-installed` exists. Do **not** remove the marker guard to "force a fresh install" — that reinstalls on every box start. To force a one-off rebuild, delete `node_modules` (or just the marker) then run `agentbox-ctl reload`.
192
- - `.pnpm-store` default location is read only, so you need to point it to a writable location in the box's writable workspace eg: `PNPM_STORE: /workspace/.pnpm-store` and add `.pnpm-store/` to the `.gitignore` if the workspace root is git-tracked.
180
+
181
+ - Host-only CLI wrappers (portless, etc.) must be bypassed, eg some projects wrap the dev server with a host-side proxy (here: `portless projectname next dev --turbopack`). Override the service command: to call the underlying tool directly (`next dev --turbopack`)