@madarco/agentbox 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{_cloud-attach-T727ZPRV.js → _cloud-attach-ZXBCNWJX.js} +4 -4
  2. package/dist/{chunk-67N47KUS.js → chunk-BXQMIEHC.js} +106 -31
  3. package/dist/chunk-BXQMIEHC.js.map +1 -0
  4. package/dist/{chunk-FODMEHD3.js → chunk-GU5LW4B5.js} +341 -25
  5. package/dist/chunk-GU5LW4B5.js.map +1 -0
  6. package/dist/{chunk-BGK32PZE.js → chunk-KL36BRN4.js} +2 -2
  7. package/dist/chunk-KL36BRN4.js.map +1 -0
  8. package/dist/chunk-MTVI44DW.js +662 -0
  9. package/dist/chunk-MTVI44DW.js.map +1 -0
  10. package/dist/{chunk-6OZDFNBF.js → chunk-NCJP5MTN.js} +201 -44
  11. package/dist/chunk-NCJP5MTN.js.map +1 -0
  12. package/dist/{dist-LOZBWMBF.js → dist-32EZBYG4.js} +9 -3
  13. package/dist/{dist-L4LCG5SJ.js → dist-CX5CGVEB.js} +4 -4
  14. package/dist/{dist-ZODPD2I6.js → dist-GDHP34ZK.js} +8 -10
  15. package/dist/dist-GDHP34ZK.js.map +1 -0
  16. package/dist/dist-XML54CNB.js +849 -0
  17. package/dist/dist-XML54CNB.js.map +1 -0
  18. package/dist/index.js +636 -340
  19. package/dist/index.js.map +1 -1
  20. package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js → prepared-state-CL4CWXQA-H5THETIM.js} +2 -2
  21. package/package.json +7 -5
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +98 -29
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
  24. package/runtime/hetzner/agentbox-vnc-start +15 -1
  25. package/runtime/hetzner/ctl.cjs +98 -29
  26. package/runtime/relay/bin.cjs +229 -37
  27. package/runtime/vercel/agentbox-checkpoint-cleanup +52 -0
  28. package/runtime/vercel/agentbox-codex-hooks.json +68 -0
  29. package/runtime/vercel/agentbox-open +28 -0
  30. package/runtime/vercel/agentbox-setup-skill.md +196 -0
  31. package/runtime/vercel/agentbox-vnc-start +91 -0
  32. package/runtime/vercel/claude-managed-settings.json +115 -0
  33. package/runtime/vercel/ctl.cjs +23466 -0
  34. package/runtime/vercel/custom-system-CLAUDE.md +50 -0
  35. package/runtime/vercel/gh-shim +263 -0
  36. package/runtime/vercel/git-shim +131 -0
  37. package/runtime/vercel/scripts/provision.sh +274 -0
  38. package/dist/chunk-67N47KUS.js.map +0 -1
  39. package/dist/chunk-6OZDFNBF.js.map +0 -1
  40. package/dist/chunk-BGK32PZE.js.map +0 -1
  41. package/dist/chunk-FODMEHD3.js.map +0 -1
  42. package/dist/dist-ZODPD2I6.js.map +0 -1
  43. /package/dist/{_cloud-attach-T727ZPRV.js.map → _cloud-attach-ZXBCNWJX.js.map} +0 -0
  44. /package/dist/{dist-LOZBWMBF.js.map → dist-32EZBYG4.js.map} +0 -0
  45. /package/dist/{dist-L4LCG5SJ.js.map → dist-CX5CGVEB.js.map} +0 -0
  46. /package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js.map → prepared-state-CL4CWXQA-H5THETIM.js.map} +0 -0
@@ -18044,6 +18044,23 @@ function isGhPrOp(value) {
18044
18044
  return GH_PR_OPS.includes(value);
18045
18045
  }
18046
18046
  var GH_PR_READ_ONLY_OPS = /* @__PURE__ */ new Set(["view", "list"]);
18047
+ function injectPrCreateHead(op, branch, args) {
18048
+ if (op !== "create") return args;
18049
+ if (!branch || branch === "HEAD") return args;
18050
+ if (hasHeadArg(args)) return args;
18051
+ return ["--head", branch, ...args];
18052
+ }
18053
+ function hasHeadArg(args) {
18054
+ return args.some((a2) => a2 === "--head" || a2.startsWith("--head=") || a2.startsWith("-H"));
18055
+ }
18056
+ function prCreateNeedsHead(op, args) {
18057
+ return op === "create" && !hasHeadArg(args);
18058
+ }
18059
+ var PR_CREATE_NO_HEAD_REFUSAL = {
18060
+ exitCode: 65,
18061
+ stdout: "",
18062
+ stderr: "gh pr create: refusing to run without --head \u2014 could not resolve this box's branch, and falling back to the host repo's checked-out branch would open a PR for the wrong branch. Ensure the box branch is pushed, or pass --head <branch> explicitly.\n"
18063
+ };
18047
18064
  var GH_RPC_TIMEOUT_MS = 12e4;
18048
18065
  var GH_READY_CACHE_TTL_MS = 6e4;
18049
18066
  var ghReadyCache;
@@ -18383,36 +18400,31 @@ function isPromptAnswerBody(v) {
18383
18400
  async function resolveCloudBackend(name) {
18384
18401
  if (name === "daytona") {
18385
18402
  const pkg = "@agentbox/sandbox-daytona";
18386
- try {
18387
- const mod = await import(pkg);
18388
- return mod.daytonaBackend;
18389
- } catch (err) {
18390
- const msg = err instanceof Error ? err.message : String(err);
18391
- if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18392
- throw new Error(
18393
- `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18394
- );
18395
- }
18396
- throw err;
18397
- }
18403
+ return loadCloudBackend(pkg, async () => (await import(pkg)).daytonaBackend);
18398
18404
  }
18399
18405
  if (name === "hetzner") {
18400
18406
  const pkg = "@agentbox/sandbox-hetzner";
18401
- try {
18402
- const mod = await import(pkg);
18403
- return mod.hetznerBackend;
18404
- } catch (err) {
18405
- const msg = err instanceof Error ? err.message : String(err);
18406
- if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18407
- throw new Error(
18408
- `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18409
- );
18410
- }
18411
- throw err;
18412
- }
18407
+ return loadCloudBackend(pkg, async () => (await import(pkg)).hetznerBackend);
18408
+ }
18409
+ if (name === "vercel") {
18410
+ const pkg = "@agentbox/sandbox-vercel";
18411
+ return loadCloudBackend(pkg, async () => (await import(pkg)).vercelBackend);
18413
18412
  }
18414
18413
  throw new Error(`no host executor for cloud backend '${name}'`);
18415
18414
  }
18415
+ async function loadCloudBackend(pkg, load2) {
18416
+ try {
18417
+ return await load2();
18418
+ } catch (err) {
18419
+ const msg = err instanceof Error ? err.message : String(err);
18420
+ if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
18421
+ throw new Error(
18422
+ `relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
18423
+ );
18424
+ }
18425
+ throw err;
18426
+ }
18427
+ }
18416
18428
  async function refreshCloudPreviewUrl(backendName, boxId, port) {
18417
18429
  try {
18418
18430
  const backend = await resolveCloudBackend(backendName);
@@ -18540,7 +18552,20 @@ async function runGhPrRpc(action, deps) {
18540
18552
  }
18541
18553
  }
18542
18554
  }
18543
- return runHostGh(["pr", op, ...args], lookup.workspacePath);
18555
+ let finalArgs = args;
18556
+ if (op === "create" && !args.some((a2) => a2 === "--head" || a2.startsWith("--head="))) {
18557
+ const backend = await resolveCloudBackend(deps.backendName);
18558
+ const handle = { sandboxId: lookup.cloudSandboxId };
18559
+ const containerPath = params.path ?? "/workspace";
18560
+ const branchProbe = await backend.exec(
18561
+ handle,
18562
+ `git -C ${shellQuote(containerPath)} rev-parse --abbrev-ref HEAD`
18563
+ );
18564
+ const branch = branchProbe.exitCode === 0 ? (branchProbe.stdout ?? "").trim() : "";
18565
+ finalArgs = injectPrCreateHead(op, branch, args);
18566
+ }
18567
+ if (prCreateNeedsHead(op, finalArgs)) return PR_CREATE_NO_HEAD_REFUSAL;
18568
+ return runHostGh(["pr", op, ...finalArgs], lookup.workspacePath);
18544
18569
  }
18545
18570
  async function runBrowserOpenMirror(action, deps) {
18546
18571
  const params = action.params ?? {};
@@ -19270,7 +19295,13 @@ function createRelayServer(opts) {
19270
19295
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "relay"}`);
19271
19296
  const route = `${req.method ?? "GET"} ${url.pathname}`;
19272
19297
  if (route === "GET /healthz") {
19273
- send(res, 200, { ok: true, boxes: registry.size(), events: events.size() });
19298
+ send(res, 200, {
19299
+ ok: true,
19300
+ boxes: registry.size(),
19301
+ events: events.size(),
19302
+ pid: process.pid,
19303
+ cliEntry: Boolean(process.env.AGENTBOX_CLI_ENTRY)
19304
+ });
19274
19305
  return;
19275
19306
  }
19276
19307
  if (url.pathname.startsWith("/bridge/")) {
@@ -19928,7 +19959,9 @@ async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiate
19928
19959
  return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19929
19960
  }
19930
19961
  }
19931
- return runHostGh(["pr", op, ...args], worktree.hostMainRepo);
19962
+ const finalArgs = injectPrCreateHead(op, worktree.branch, args);
19963
+ if (prCreateNeedsHead(op, finalArgs)) return PR_CREATE_NO_HEAD_REFUSAL;
19964
+ return runHostGh(["pr", op, ...finalArgs], worktree.hostMainRepo);
19932
19965
  }
19933
19966
  async function handleCpRpc(reg, method, params) {
19934
19967
  const entry = process.env.AGENTBOX_CLI_ENTRY;
@@ -20056,6 +20089,7 @@ var BUILT_IN_DEFAULTS = {
20056
20089
  defaultCheckpointDocker: "",
20057
20090
  defaultCheckpointDaytona: "",
20058
20091
  defaultCheckpointHetzner: "",
20092
+ defaultCheckpointVercel: "",
20059
20093
  withPlaywright: false,
20060
20094
  withEnv: false,
20061
20095
  vnc: true,
@@ -20068,7 +20102,10 @@ var BUILT_IN_DEFAULTS = {
20068
20102
  cpus: 0,
20069
20103
  pidsLimit: 0,
20070
20104
  disk: "",
20071
- bundleDepth: void 0
20105
+ bundleDepth: void 0,
20106
+ vercelVcpus: 2,
20107
+ vercelTimeoutMs: 27e5,
20108
+ vercelNetworkPolicy: ""
20072
20109
  },
20073
20110
  checkpoint: {
20074
20111
  maxLayers: 3
@@ -20119,7 +20156,12 @@ var BUILT_IN_DEFAULTS = {
20119
20156
  },
20120
20157
  queue: {
20121
20158
  enabled: true,
20122
- maxConcurrent: 5
20159
+ maxConcurrent: 5,
20160
+ maxWorking: 0,
20161
+ idleGraceSeconds: 15
20162
+ },
20163
+ cloud: {
20164
+ useCurrentBranch: false
20123
20165
  },
20124
20166
  maintenance: {
20125
20167
  pruneProjectConfigs: true,
@@ -20130,8 +20172,8 @@ var KEY_REGISTRY = [
20130
20172
  {
20131
20173
  key: "box.provider",
20132
20174
  type: "enum",
20133
- enumValues: ["docker", "daytona", "hetzner"],
20134
- description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, or Hetzner Cloud VPSes."
20175
+ enumValues: ["docker", "daytona", "hetzner", "vercel"],
20176
+ description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or Vercel Sandboxes."
20135
20177
  },
20136
20178
  {
20137
20179
  key: "box.hostSnapshot",
@@ -20161,6 +20203,12 @@ var KEY_REGISTRY = [
20161
20203
  description: "Per-provider override of `box.defaultCheckpoint` for hetzner. Wins over the global when set; set via `agentbox checkpoint set-default --provider hetzner`.",
20162
20204
  advanced: true
20163
20205
  },
20206
+ {
20207
+ key: "box.defaultCheckpointVercel",
20208
+ type: "string",
20209
+ description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
20210
+ advanced: true
20211
+ },
20164
20212
  {
20165
20213
  key: "checkpoint.maxLayers",
20166
20214
  type: "int",
@@ -20234,6 +20282,21 @@ var KEY_REGISTRY = [
20234
20282
  type: "int",
20235
20283
  description: "Cap git bundle history shipped to cloud sandboxes (daytona, hetzner). 0 = full history. Unset = adaptive default (last 200 commits; re-bundle at 100 if the bundle exceeds 20 MB). Ignored for docker (which bind-mounts .git/)."
20236
20284
  },
20285
+ {
20286
+ key: "box.vercelVcpus",
20287
+ type: "int",
20288
+ description: "vCPUs for new --provider vercel boxes (Vercel couples RAM at 2048 MB/vCPU). Default 2. Vercel only accepts specific counts (e.g. 1, 2, 4, 8) \u2014 an unsupported value fails create with a 400. Vercel-only; ignored by other providers."
20289
+ },
20290
+ {
20291
+ key: "box.vercelTimeoutMs",
20292
+ type: "int",
20293
+ description: "Max session length (ms) for new --provider vercel boxes before the VM auto-snapshots; persistent mode auto-resumes on the next call. Default 2700000 (45 min, the Hobby ceiling). Vercel-only."
20294
+ },
20295
+ {
20296
+ key: "box.vercelNetworkPolicy",
20297
+ type: "string",
20298
+ description: "Egress lock for new --provider vercel boxes: 'allow-all' (default, unset), 'deny-all', or a comma-separated domain allowlist (e.g. 'github.com,*.npmjs.org') that denies everything else. Vercel-only; ignored by other providers."
20299
+ },
20237
20300
  {
20238
20301
  key: "claude.sessionName",
20239
20302
  type: "string",
@@ -20351,6 +20414,21 @@ var KEY_REGISTRY = [
20351
20414
  type: "int",
20352
20415
  description: "Max number of simultaneously-running boxes (across providers) before background `-i` jobs queue up instead of starting immediately. Per-invocation override: `--max-running <n>`."
20353
20416
  },
20417
+ {
20418
+ key: "queue.maxWorking",
20419
+ type: "int",
20420
+ description: "Max agents actively working/thinking (quota-consuming) at once before background `-i` jobs queue. 0 = disabled (use the queue.maxConcurrent running-box gate). Counts all boxes, foreground + queued. Per-invocation override: `--max-working <n>`."
20421
+ },
20422
+ {
20423
+ key: "queue.idleGraceSeconds",
20424
+ type: "int",
20425
+ description: "Seconds an agent must stay non-working before it frees its working slot (debounce against brief idle flaps between turns). Only used when queue.maxWorking > 0."
20426
+ },
20427
+ {
20428
+ key: "cloud.useCurrentBranch",
20429
+ type: "bool",
20430
+ description: "On cloud providers (daytona/hetzner), start new boxes on the host's current branch instead of forking a new agentbox/<box-name> branch. Overridden by an explicit --use-branch / --from-branch."
20431
+ },
20354
20432
  {
20355
20433
  key: "maintenance.pruneProjectConfigs",
20356
20434
  type: "bool",
@@ -20701,7 +20779,9 @@ async function loadQueueConfig() {
20701
20779
  const q = global3.queue ?? {};
20702
20780
  return {
20703
20781
  enabled: q.enabled ?? d.enabled,
20704
- maxConcurrent: q.maxConcurrent ?? d.maxConcurrent
20782
+ maxConcurrent: q.maxConcurrent ?? d.maxConcurrent,
20783
+ maxWorking: q.maxWorking ?? d.maxWorking,
20784
+ idleGraceMs: (q.idleGraceSeconds ?? d.idleGraceSeconds) * 1e3
20705
20785
  };
20706
20786
  }
20707
20787
  async function writeJob(job) {
@@ -20747,6 +20827,70 @@ function selectNextRunnable(jobs, runningCount) {
20747
20827
  }
20748
20828
  return null;
20749
20829
  }
20830
+ var WORKING_AGENT_STATES = [
20831
+ "working",
20832
+ "idle",
20833
+ "waiting",
20834
+ "end-plan",
20835
+ "question",
20836
+ "compacting",
20837
+ "error",
20838
+ "unknown"
20839
+ ];
20840
+ function isWorkingAgentState(v) {
20841
+ return typeof v === "string" && WORKING_AGENT_STATES.includes(v);
20842
+ }
20843
+ var STARTUP_GRACE_MS = 9e4;
20844
+ function occupiesWorkingSlot(e, idleGraceMs) {
20845
+ if (e.agentState === "working" || e.agentState === "compacting") return true;
20846
+ if ((e.agentState === null || e.agentState === "unknown") && e.sinceCreateMs !== null && e.sinceCreateMs < STARTUP_GRACE_MS) {
20847
+ return true;
20848
+ }
20849
+ if ((e.agentState === "idle" || e.agentState === "waiting" || e.agentState === "end-plan" || e.agentState === "question") && e.sinceUpdateMs !== null && e.sinceUpdateMs < idleGraceMs) {
20850
+ return true;
20851
+ }
20852
+ return false;
20853
+ }
20854
+ function countWorkingSlots(entries, idleGraceMs) {
20855
+ return entries.reduce((n2, e) => occupiesWorkingSlot(e, idleGraceMs) ? n2 + 1 : n2, 0);
20856
+ }
20857
+ function selectNextRunnableByWorking(jobs, workingCount, globalMaxWorking) {
20858
+ for (const j of jobs) {
20859
+ if (j.status !== "queued") continue;
20860
+ const ceil = typeof j.maxWorking === "number" && j.maxWorking > 0 ? j.maxWorking : globalMaxWorking;
20861
+ if (workingCount < ceil) return j;
20862
+ }
20863
+ return null;
20864
+ }
20865
+ function readActiveAgent(snap) {
20866
+ if (!snap || typeof snap !== "object") return { state: null, updatedAt: null };
20867
+ const candidates = [];
20868
+ for (const key of ["claude", "codex", "opencode"]) {
20869
+ const sub = snap[key];
20870
+ if (!sub || typeof sub !== "object") continue;
20871
+ const o2 = sub;
20872
+ if (!isWorkingAgentState(o2.state)) continue;
20873
+ candidates.push({
20874
+ state: o2.state,
20875
+ updatedAt: typeof o2.updatedAt === "string" ? o2.updatedAt : null
20876
+ });
20877
+ }
20878
+ if (candidates.length === 0) return { state: null, updatedAt: null };
20879
+ const active = candidates.find((c3) => c3.state === "working" || c3.state === "compacting");
20880
+ if (active) return active;
20881
+ candidates.sort((a2, b) => parseTime(b.updatedAt) - parseTime(a2.updatedAt));
20882
+ return candidates[0];
20883
+ }
20884
+ function parseTime(iso) {
20885
+ if (!iso) return 0;
20886
+ const t = Date.parse(iso);
20887
+ return Number.isNaN(t) ? 0 : t;
20888
+ }
20889
+ function msSince2(iso) {
20890
+ if (!iso) return null;
20891
+ const t = Date.parse(iso);
20892
+ return Number.isNaN(t) ? null : Date.now() - t;
20893
+ }
20750
20894
  var DEFAULT_INTERVAL_MS2 = 2e3;
20751
20895
  function startQueueLoop(deps) {
20752
20896
  const loadConfig = deps.loadConfig ?? loadQueueConfig;
@@ -20754,8 +20898,10 @@ function startQueueLoop(deps) {
20754
20898
  const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
20755
20899
  const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
20756
20900
  const { log, onStatusChange } = deps;
20901
+ const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
20757
20902
  let ticking = false;
20758
20903
  let stopped = false;
20904
+ let warnedNoWorkingDeps = false;
20759
20905
  let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
20760
20906
  log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
20761
20907
  });
@@ -20768,10 +20914,26 @@ function startQueueLoop(deps) {
20768
20914
  const jobs = await loadQueue();
20769
20915
  const hasQueued = jobs.some((j) => j.status === "queued");
20770
20916
  if (!hasQueued) return;
20917
+ let gateByWorking = cfg.maxWorking > 0;
20918
+ if (gateByWorking && !countWorking) {
20919
+ gateByWorking = false;
20920
+ if (!warnedNoWorkingDeps) {
20921
+ warnedNoWorkingDeps = true;
20922
+ log("queue: maxWorking set but registry/statusStore not wired; using running-box gate");
20923
+ }
20924
+ }
20771
20925
  while (!stopped) {
20772
- const running = await countRunning();
20773
- const fresh = await loadQueue();
20774
- const next = selectNextRunnable(fresh, running);
20926
+ let occupancy;
20927
+ let next;
20928
+ if (gateByWorking && countWorking) {
20929
+ occupancy = await countWorking(cfg.idleGraceMs);
20930
+ const fresh = await loadQueue();
20931
+ next = selectNextRunnableByWorking(fresh, occupancy, cfg.maxWorking);
20932
+ } else {
20933
+ occupancy = await countRunning();
20934
+ const fresh = await loadQueue();
20935
+ next = selectNextRunnable(fresh, occupancy);
20936
+ }
20775
20937
  if (!next) return;
20776
20938
  const current = await readJob(next.id);
20777
20939
  if (!current || current.status !== "queued") continue;
@@ -20788,8 +20950,9 @@ function startQueueLoop(deps) {
20788
20950
  const withPid = { ...updated, pid };
20789
20951
  await writeJob(withPid);
20790
20952
  onStatusChange?.(withPid);
20953
+ const ceil = gateByWorking ? typeof updated.maxWorking === "number" && updated.maxWorking > 0 ? updated.maxWorking : cfg.maxWorking : updated.maxConcurrent;
20791
20954
  log(
20792
- `queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; running ${String(running + 1)}/${String(updated.maxConcurrent)}`
20955
+ `queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; ${gateByWorking ? "working" : "running"} ${String(occupancy + 1)}/${String(ceil)}`
20793
20956
  );
20794
20957
  } else {
20795
20958
  log(`queue: started job ${updated.id} (${updated.agent}); pid unknown`);
@@ -20858,6 +21021,33 @@ function processAlive(pid) {
20858
21021
  return false;
20859
21022
  }
20860
21023
  }
21024
+ async function defaultCountWorkingBoxes(registry, statusStore, idleGraceMs) {
21025
+ const boxes = registry.list();
21026
+ const registeredIds = new Set(boxes.map((b) => b.boxId));
21027
+ const entries = boxes.map((b) => {
21028
+ const active = readActiveAgent(statusStore.get(b.boxId));
21029
+ return {
21030
+ key: b.boxId,
21031
+ agentState: active.state,
21032
+ sinceUpdateMs: msSince2(active.updatedAt),
21033
+ sinceCreateMs: msSince2(b.createdAt)
21034
+ };
21035
+ });
21036
+ let count2 = countWorkingSlots(entries, idleGraceMs);
21037
+ let jobs;
21038
+ try {
21039
+ jobs = await loadQueue();
21040
+ } catch {
21041
+ return count2;
21042
+ }
21043
+ for (const j of jobs) {
21044
+ if (j.status !== "running") continue;
21045
+ if (j.boxId && registeredIds.has(j.boxId)) continue;
21046
+ if (typeof j.pid === "number" && !processAlive(j.pid)) continue;
21047
+ count2 += 1;
21048
+ }
21049
+ return count2;
21050
+ }
20861
21051
  var RUNNING_COUNT_CACHE_MS = 3e3;
20862
21052
  var runningCountCache = null;
20863
21053
  async function defaultCountRunningBoxes() {
@@ -20974,7 +21164,9 @@ program2.command("serve").description("Run the HTTP relay in the foreground").op
20974
21164
  });
20975
21165
  const queue = startQueueLoop({
20976
21166
  log: (line) => process.stdout.write(`agentbox-relay: ${line}
20977
- `)
21167
+ `),
21168
+ registry: handle.registry,
21169
+ statusStore: handle.statusStore
20978
21170
  });
20979
21171
  handle.setQueuePoke(() => {
20980
21172
  queue.poke?.();
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-`docker commit` cleanup: strip ephemeral / disposable state so the
3
+ # captured checkpoint image is closer to "warm project state, nothing else".
4
+ #
5
+ # Invoked by the host via `docker exec --user root <container>
6
+ # /usr/local/bin/agentbox-checkpoint-cleanup` right before
7
+ # `docker commit`. Best-effort: every step is allowed to fail (a checkpoint
8
+ # capture should never block on cleanup hiccups).
9
+ #
10
+ # What we DELIBERATELY keep:
11
+ # - /workspace the actual point of the checkpoint
12
+ # - /home/vscode/.npm warm npm cache (next install is fast)
13
+ # - /home/vscode/.cache pnpm/yarn/Cargo/etc. caches
14
+ # - /var/lib/docker in-box dockerd's data root
15
+ # - /home/vscode/.claude the named volume is bind-mounted; image
16
+ # layer never sees it anyway
17
+ set +e
18
+
19
+ # apt: drop downloaded .deb cache and the package index. The index is ~50MB
20
+ # and gets refreshed on the next `apt-get update`; the .deb cache is reusable
21
+ # only if we don't change versions, which we usually do.
22
+ apt-get clean 2>/dev/null
23
+ rm -rf /var/lib/apt/lists/* 2>/dev/null
24
+
25
+ # Throwaway scratch dirs. Preserve /tmp/claude-* — that is the live in-box
26
+ # Claude Code session's working tree (its per-task stdout/stderr files). The
27
+ # agent that triggered this checkpoint *is* that session; deleting its task
28
+ # output mid-run makes its harness see ENOENT, treat the command as failed,
29
+ # and retry the checkpoint (observed: 5 duplicate auto-named checkpoints).
30
+ # Stale claude-* dirs baked into the image are tiny and Claude Code prunes
31
+ # them itself on the next session start.
32
+ find /tmp /var/tmp -mindepth 1 -maxdepth 1 ! -name 'claude-*' -exec rm -rf {} + 2>/dev/null
33
+
34
+ # Logs: truncate (don't delete) so the original file modes / ownerships stay
35
+ # intact for the next run. Targets common rotated archives too.
36
+ find /var/log -type f \( -name '*.log' -o -name '*.gz' -o -name '*.1' \) \
37
+ -exec truncate -s0 {} + 2>/dev/null
38
+ find /var/log/agentbox -type f -exec truncate -s0 {} + 2>/dev/null
39
+
40
+ # Bash history (root + vscode). Re-assert vscode ownership: `: >` run as root
41
+ # (re)creates the file root-owned 0644 when it didn't exist, which the uid-1000
42
+ # vscode user cannot append to, silently dropping all shell history.
43
+ : > /root/.bash_history 2>/dev/null
44
+ : > /home/vscode/.bash_history 2>/dev/null
45
+ chown vscode:vscode /home/vscode/.bash_history 2>/dev/null
46
+ chmod 600 /home/vscode/.bash_history 2>/dev/null
47
+
48
+ # Anthropic's installer writes a transient marker; redundant once the binary
49
+ # is in place. Safe to wipe.
50
+ rm -rf /home/vscode/.claude-installer 2>/dev/null
51
+
52
+ exit 0
@@ -0,0 +1,68 @@
1
+ {
2
+ "$comment": "Codex 0.134.0 expects `~/.codex/hooks.json` to be `{ hooks: { Event: [...] } }` (matching the `HooksFile` Rust struct), NOT a top-level event map. The `hooks` feature flag must also be enabled (`codex --enable hooks`) and hook trust must be either persisted via the in-TUI dialog or bypassed at launch (`--dangerously-bypass-hook-trust`). startCodexSession() does both. In practice the hook firing on the JSON-config path is still unreliable in 0.134.0 (TUI mode skips them on at least some startup paths) — the real mechanism that lights up state in production is the tmux-pane scraper in packages/ctl/src/codex-scraper.ts. These hooks remain as a defense-in-depth seed so any future codex build that fixes the firing also lights up state without further work.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ { "type": "command", "command": "agentbox-ctl codex-state idle >/dev/null 2>&1 &", "timeout": 3 }
8
+ ]
9
+ }
10
+ ],
11
+ "UserPromptSubmit": [
12
+ {
13
+ "hooks": [
14
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
15
+ ]
16
+ }
17
+ ],
18
+ "PreToolUse": [
19
+ {
20
+ "hooks": [
21
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
22
+ ]
23
+ }
24
+ ],
25
+ "PermissionRequest": [
26
+ {
27
+ "hooks": [
28
+ { "type": "command", "command": "agentbox-ctl codex-state waiting >/dev/null 2>&1 &", "timeout": 3 }
29
+ ]
30
+ }
31
+ ],
32
+ "PreCompact": [
33
+ {
34
+ "hooks": [
35
+ { "type": "command", "command": "agentbox-ctl codex-state compacting >/dev/null 2>&1 &", "timeout": 3 }
36
+ ]
37
+ }
38
+ ],
39
+ "PostCompact": [
40
+ {
41
+ "hooks": [
42
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
43
+ ]
44
+ }
45
+ ],
46
+ "SubagentStart": [
47
+ {
48
+ "hooks": [
49
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
50
+ ]
51
+ }
52
+ ],
53
+ "SubagentStop": [
54
+ {
55
+ "hooks": [
56
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
57
+ ]
58
+ }
59
+ ],
60
+ "Stop": [
61
+ {
62
+ "hooks": [
63
+ { "type": "command", "command": "agentbox-ctl codex-state idle >/dev/null 2>&1 &", "timeout": 3 }
64
+ ]
65
+ }
66
+ ]
67
+ }
68
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Routes in-box URL opens to `agentbox-ctl open`, which opens the link in the
3
+ # box's own Chromium (agent-browser, visible via `agentbox screen`) and asks
4
+ # the host user — in the footer/dashboard — whether to also open it on the
5
+ # host. This script is installed at /usr/local/bin (earlier in PATH than
6
+ # xdg-utils' /usr/bin/xdg-open, which it is also symlinked over) and is the
7
+ # box's $BROWSER, so `xdg-open`, Claude Code's OAuth flow, `gh`,
8
+ # `git web--browse`, python's webbrowser, etc. all land here.
9
+ #
10
+ # Only http(s) URLs are routed. Anything else (a file path, another scheme)
11
+ # falls through to the real xdg-open, which resolves it locally in the box.
12
+
13
+ set -uo pipefail
14
+
15
+ target="${1:-}"
16
+
17
+ case "$target" in
18
+ http://* | https://*)
19
+ exec agentbox-ctl open "$target"
20
+ ;;
21
+ *)
22
+ if [[ -x /usr/bin/xdg-open ]]; then
23
+ exec /usr/bin/xdg-open "$@"
24
+ fi
25
+ echo "agentbox-open: not an http(s) URL: $target" >&2
26
+ exit 1
27
+ ;;
28
+ esac