@madarco/agentbox 0.8.0 → 0.10.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 (51) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +161 -0
  3. package/dist/{_cloud-attach-T727ZPRV.js → _cloud-attach-O6NYTLES.js} +4 -4
  4. package/dist/{chunk-67N47KUS.js → chunk-2GPORKYF.js} +349 -182
  5. package/dist/chunk-2GPORKYF.js.map +1 -0
  6. package/dist/{chunk-6OZDFNBF.js → chunk-7UIAO7PC.js} +401 -82
  7. package/dist/chunk-7UIAO7PC.js.map +1 -0
  8. package/dist/{chunk-BGK32PZE.js → chunk-KL36BRN4.js} +2 -2
  9. package/dist/chunk-KL36BRN4.js.map +1 -0
  10. package/dist/chunk-MTVI44DW.js +662 -0
  11. package/dist/chunk-MTVI44DW.js.map +1 -0
  12. package/dist/{chunk-FODMEHD3.js → chunk-R4O5WPHW.js} +705 -77
  13. package/dist/chunk-R4O5WPHW.js.map +1 -0
  14. package/dist/{dist-ZODPD2I6.js → dist-5FQGYRW5.js} +20 -10
  15. package/dist/dist-5FQGYRW5.js.map +1 -0
  16. package/dist/{dist-LOZBWMBF.js → dist-BQNX7RQE.js} +19 -3
  17. package/dist/dist-PZW3GWWU.js +874 -0
  18. package/dist/dist-PZW3GWWU.js.map +1 -0
  19. package/dist/{dist-L4LCG5SJ.js → dist-TMHSUVTP.js} +4 -4
  20. package/dist/index.js +2385 -842
  21. package/dist/index.js.map +1 -1
  22. package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js → prepared-state-CL4CWXQA-H5THETIM.js} +2 -2
  23. package/package.json +11 -7
  24. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +9 -8
  25. package/runtime/docker/packages/ctl/dist/bin.cjs +129 -31
  26. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
  27. package/runtime/hetzner/agentbox-setup-skill.md +9 -8
  28. package/runtime/hetzner/agentbox-vnc-start +15 -1
  29. package/runtime/hetzner/ctl.cjs +129 -31
  30. package/runtime/relay/bin.cjs +260 -39
  31. package/runtime/vercel/agentbox-checkpoint-cleanup +52 -0
  32. package/runtime/vercel/agentbox-codex-hooks.json +68 -0
  33. package/runtime/vercel/agentbox-open +28 -0
  34. package/runtime/vercel/agentbox-setup-skill.md +197 -0
  35. package/runtime/vercel/agentbox-vnc-start +91 -0
  36. package/runtime/vercel/claude-managed-settings.json +115 -0
  37. package/runtime/vercel/ctl.cjs +23495 -0
  38. package/runtime/vercel/custom-system-CLAUDE.md +47 -0
  39. package/runtime/vercel/gh-shim +263 -0
  40. package/runtime/vercel/git-shim +131 -0
  41. package/runtime/vercel/scripts/provision.sh +314 -0
  42. package/share/agentbox-setup/SKILL.md +9 -8
  43. package/dist/chunk-67N47KUS.js.map +0 -1
  44. package/dist/chunk-6OZDFNBF.js.map +0 -1
  45. package/dist/chunk-BGK32PZE.js.map +0 -1
  46. package/dist/chunk-FODMEHD3.js.map +0 -1
  47. package/dist/dist-ZODPD2I6.js.map +0 -1
  48. /package/dist/{_cloud-attach-T727ZPRV.js.map → _cloud-attach-O6NYTLES.js.map} +0 -0
  49. /package/dist/{dist-LOZBWMBF.js.map → dist-BQNX7RQE.js.map} +0 -0
  50. /package/dist/{dist-L4LCG5SJ.js.map → dist-TMHSUVTP.js.map} +0 -0
  51. /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 ?? {};
@@ -18659,6 +18684,18 @@ async function runCheckpointRpc(action, deps) {
18659
18684
  stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run checkpoint host-side\n"
18660
18685
  };
18661
18686
  }
18687
+ if (deps.backendName === "vercel" && deps.prompts && deps.subscribers && deps.subscribers.forBox(deps.boxId).length > 0) {
18688
+ const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, {
18689
+ kind: "confirm",
18690
+ message: `Create checkpoint on ${deps.boxName ?? deps.boxId}? The vercel box will stop and reboot.`,
18691
+ detail: params.name ? `checkpoint: ${params.name}` : "(auto-named)",
18692
+ defaultAnswer: "n",
18693
+ context: { command: "checkpoint create", argv: params.name ? [params.name] : [] }
18694
+ });
18695
+ if (verdict.answer !== "y") {
18696
+ return { exitCode: 10, stdout: "", stderr: "checkpoint denied by user\n" };
18697
+ }
18698
+ }
18662
18699
  const argv = [process.execPath, entry, "checkpoint", "create", deps.boxId];
18663
18700
  if (params.name) argv.push("--name", params.name);
18664
18701
  if (params.merged === true) argv.push("--merged");
@@ -19270,7 +19307,18 @@ function createRelayServer(opts) {
19270
19307
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "relay"}`);
19271
19308
  const route = `${req.method ?? "GET"} ${url.pathname}`;
19272
19309
  if (route === "GET /healthz") {
19273
- send(res, 200, { ok: true, boxes: registry.size(), events: events.size() });
19310
+ send(res, 200, {
19311
+ ok: true,
19312
+ boxes: registry.size(),
19313
+ events: events.size(),
19314
+ pid: process.pid,
19315
+ cliEntry: Boolean(process.env.AGENTBOX_CLI_ENTRY),
19316
+ // The spawning CLI's version/commit (inherited via env at spawn time).
19317
+ // `version` lets host-side ensureRelay reclaim a relay left over from a
19318
+ // different agentbox version; `commit` is observability-only.
19319
+ version: process.env.AGENTBOX_CLI_VERSION || void 0,
19320
+ commit: process.env.AGENTBOX_CLI_COMMIT || void 0
19321
+ });
19274
19322
  return;
19275
19323
  }
19276
19324
  if (url.pathname.startsWith("/bridge/")) {
@@ -19928,7 +19976,9 @@ async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiate
19928
19976
  return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
19929
19977
  }
19930
19978
  }
19931
- return runHostGh(["pr", op, ...args], worktree.hostMainRepo);
19979
+ const finalArgs = injectPrCreateHead(op, worktree.branch, args);
19980
+ if (prCreateNeedsHead(op, finalArgs)) return PR_CREATE_NO_HEAD_REFUSAL;
19981
+ return runHostGh(["pr", op, ...finalArgs], worktree.hostMainRepo);
19932
19982
  }
19933
19983
  async function handleCpRpc(reg, method, params) {
19934
19984
  const entry = process.env.AGENTBOX_CLI_ENTRY;
@@ -20056,6 +20106,7 @@ var BUILT_IN_DEFAULTS = {
20056
20106
  defaultCheckpointDocker: "",
20057
20107
  defaultCheckpointDaytona: "",
20058
20108
  defaultCheckpointHetzner: "",
20109
+ defaultCheckpointVercel: "",
20059
20110
  withPlaywright: false,
20060
20111
  withEnv: false,
20061
20112
  vnc: true,
@@ -20068,16 +20119,21 @@ var BUILT_IN_DEFAULTS = {
20068
20119
  cpus: 0,
20069
20120
  pidsLimit: 0,
20070
20121
  disk: "",
20071
- bundleDepth: void 0
20122
+ bundleDepth: void 0,
20123
+ vercelVcpus: 2,
20124
+ vercelTimeoutMs: 27e5,
20125
+ vercelNetworkPolicy: ""
20072
20126
  },
20073
20127
  checkpoint: {
20074
20128
  maxLayers: 3
20075
20129
  },
20076
20130
  claude: {
20077
- sessionName: "claude"
20131
+ sessionName: "claude",
20132
+ dangerouslySkipPermissions: true
20078
20133
  },
20079
20134
  codex: {
20080
- sessionName: "codex"
20135
+ sessionName: "codex",
20136
+ dangerouslySkipPermissions: true
20081
20137
  },
20082
20138
  opencode: {
20083
20139
  sessionName: "opencode"
@@ -20119,7 +20175,12 @@ var BUILT_IN_DEFAULTS = {
20119
20175
  },
20120
20176
  queue: {
20121
20177
  enabled: true,
20122
- maxConcurrent: 5
20178
+ maxConcurrent: 5,
20179
+ maxWorking: 0,
20180
+ idleGraceSeconds: 15
20181
+ },
20182
+ cloud: {
20183
+ useCurrentBranch: false
20123
20184
  },
20124
20185
  maintenance: {
20125
20186
  pruneProjectConfigs: true,
@@ -20130,8 +20191,8 @@ var KEY_REGISTRY = [
20130
20191
  {
20131
20192
  key: "box.provider",
20132
20193
  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."
20194
+ enumValues: ["docker", "daytona", "hetzner", "vercel"],
20195
+ description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or Vercel Sandboxes."
20135
20196
  },
20136
20197
  {
20137
20198
  key: "box.hostSnapshot",
@@ -20161,6 +20222,12 @@ var KEY_REGISTRY = [
20161
20222
  description: "Per-provider override of `box.defaultCheckpoint` for hetzner. Wins over the global when set; set via `agentbox checkpoint set-default --provider hetzner`.",
20162
20223
  advanced: true
20163
20224
  },
20225
+ {
20226
+ key: "box.defaultCheckpointVercel",
20227
+ type: "string",
20228
+ description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
20229
+ advanced: true
20230
+ },
20164
20231
  {
20165
20232
  key: "checkpoint.maxLayers",
20166
20233
  type: "int",
@@ -20234,16 +20301,41 @@ var KEY_REGISTRY = [
20234
20301
  type: "int",
20235
20302
  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
20303
  },
20304
+ {
20305
+ key: "box.vercelVcpus",
20306
+ type: "int",
20307
+ 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."
20308
+ },
20309
+ {
20310
+ key: "box.vercelTimeoutMs",
20311
+ type: "int",
20312
+ 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."
20313
+ },
20314
+ {
20315
+ key: "box.vercelNetworkPolicy",
20316
+ type: "string",
20317
+ 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."
20318
+ },
20237
20319
  {
20238
20320
  key: "claude.sessionName",
20239
20321
  type: "string",
20240
20322
  description: "tmux session name for `agentbox claude`."
20241
20323
  },
20324
+ {
20325
+ key: "claude.dangerouslySkipPermissions",
20326
+ type: "bool",
20327
+ description: "Launch claude in new boxes with --dangerously-skip-permissions (auto-accept tool use). Safe because boxes are isolated; on by default. Override per-box with --no-dangerously-skip-permissions."
20328
+ },
20242
20329
  {
20243
20330
  key: "codex.sessionName",
20244
20331
  type: "string",
20245
20332
  description: "tmux session name for `agentbox codex`."
20246
20333
  },
20334
+ {
20335
+ key: "codex.dangerouslySkipPermissions",
20336
+ type: "bool",
20337
+ description: "Launch codex in new boxes with --dangerously-bypass-approvals-and-sandbox (never prompt for approval). Safe because boxes are isolated; on by default. Override per-box with --no-dangerously-skip-permissions."
20338
+ },
20247
20339
  {
20248
20340
  key: "opencode.sessionName",
20249
20341
  type: "string",
@@ -20351,6 +20443,21 @@ var KEY_REGISTRY = [
20351
20443
  type: "int",
20352
20444
  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
20445
  },
20446
+ {
20447
+ key: "queue.maxWorking",
20448
+ type: "int",
20449
+ 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>`."
20450
+ },
20451
+ {
20452
+ key: "queue.idleGraceSeconds",
20453
+ type: "int",
20454
+ 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."
20455
+ },
20456
+ {
20457
+ key: "cloud.useCurrentBranch",
20458
+ type: "bool",
20459
+ 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."
20460
+ },
20354
20461
  {
20355
20462
  key: "maintenance.pruneProjectConfigs",
20356
20463
  type: "bool",
@@ -20701,7 +20808,9 @@ async function loadQueueConfig() {
20701
20808
  const q = global3.queue ?? {};
20702
20809
  return {
20703
20810
  enabled: q.enabled ?? d.enabled,
20704
- maxConcurrent: q.maxConcurrent ?? d.maxConcurrent
20811
+ maxConcurrent: q.maxConcurrent ?? d.maxConcurrent,
20812
+ maxWorking: q.maxWorking ?? d.maxWorking,
20813
+ idleGraceMs: (q.idleGraceSeconds ?? d.idleGraceSeconds) * 1e3
20705
20814
  };
20706
20815
  }
20707
20816
  async function writeJob(job) {
@@ -20747,6 +20856,70 @@ function selectNextRunnable(jobs, runningCount) {
20747
20856
  }
20748
20857
  return null;
20749
20858
  }
20859
+ var WORKING_AGENT_STATES = [
20860
+ "working",
20861
+ "idle",
20862
+ "waiting",
20863
+ "end-plan",
20864
+ "question",
20865
+ "compacting",
20866
+ "error",
20867
+ "unknown"
20868
+ ];
20869
+ function isWorkingAgentState(v) {
20870
+ return typeof v === "string" && WORKING_AGENT_STATES.includes(v);
20871
+ }
20872
+ var STARTUP_GRACE_MS = 9e4;
20873
+ function occupiesWorkingSlot(e, idleGraceMs) {
20874
+ if (e.agentState === "working" || e.agentState === "compacting") return true;
20875
+ if ((e.agentState === null || e.agentState === "unknown") && e.sinceCreateMs !== null && e.sinceCreateMs < STARTUP_GRACE_MS) {
20876
+ return true;
20877
+ }
20878
+ if ((e.agentState === "idle" || e.agentState === "waiting" || e.agentState === "end-plan" || e.agentState === "question") && e.sinceUpdateMs !== null && e.sinceUpdateMs < idleGraceMs) {
20879
+ return true;
20880
+ }
20881
+ return false;
20882
+ }
20883
+ function countWorkingSlots(entries, idleGraceMs) {
20884
+ return entries.reduce((n2, e) => occupiesWorkingSlot(e, idleGraceMs) ? n2 + 1 : n2, 0);
20885
+ }
20886
+ function selectNextRunnableByWorking(jobs, workingCount, globalMaxWorking) {
20887
+ for (const j of jobs) {
20888
+ if (j.status !== "queued") continue;
20889
+ const ceil = typeof j.maxWorking === "number" && j.maxWorking > 0 ? j.maxWorking : globalMaxWorking;
20890
+ if (workingCount < ceil) return j;
20891
+ }
20892
+ return null;
20893
+ }
20894
+ function readActiveAgent(snap) {
20895
+ if (!snap || typeof snap !== "object") return { state: null, updatedAt: null };
20896
+ const candidates = [];
20897
+ for (const key of ["claude", "codex", "opencode"]) {
20898
+ const sub = snap[key];
20899
+ if (!sub || typeof sub !== "object") continue;
20900
+ const o2 = sub;
20901
+ if (!isWorkingAgentState(o2.state)) continue;
20902
+ candidates.push({
20903
+ state: o2.state,
20904
+ updatedAt: typeof o2.updatedAt === "string" ? o2.updatedAt : null
20905
+ });
20906
+ }
20907
+ if (candidates.length === 0) return { state: null, updatedAt: null };
20908
+ const active = candidates.find((c3) => c3.state === "working" || c3.state === "compacting");
20909
+ if (active) return active;
20910
+ candidates.sort((a2, b) => parseTime(b.updatedAt) - parseTime(a2.updatedAt));
20911
+ return candidates[0];
20912
+ }
20913
+ function parseTime(iso) {
20914
+ if (!iso) return 0;
20915
+ const t = Date.parse(iso);
20916
+ return Number.isNaN(t) ? 0 : t;
20917
+ }
20918
+ function msSince2(iso) {
20919
+ if (!iso) return null;
20920
+ const t = Date.parse(iso);
20921
+ return Number.isNaN(t) ? null : Date.now() - t;
20922
+ }
20750
20923
  var DEFAULT_INTERVAL_MS2 = 2e3;
20751
20924
  function startQueueLoop(deps) {
20752
20925
  const loadConfig = deps.loadConfig ?? loadQueueConfig;
@@ -20754,8 +20927,10 @@ function startQueueLoop(deps) {
20754
20927
  const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
20755
20928
  const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
20756
20929
  const { log, onStatusChange } = deps;
20930
+ const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
20757
20931
  let ticking = false;
20758
20932
  let stopped = false;
20933
+ let warnedNoWorkingDeps = false;
20759
20934
  let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
20760
20935
  log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
20761
20936
  });
@@ -20768,10 +20943,26 @@ function startQueueLoop(deps) {
20768
20943
  const jobs = await loadQueue();
20769
20944
  const hasQueued = jobs.some((j) => j.status === "queued");
20770
20945
  if (!hasQueued) return;
20946
+ let gateByWorking = cfg.maxWorking > 0;
20947
+ if (gateByWorking && !countWorking) {
20948
+ gateByWorking = false;
20949
+ if (!warnedNoWorkingDeps) {
20950
+ warnedNoWorkingDeps = true;
20951
+ log("queue: maxWorking set but registry/statusStore not wired; using running-box gate");
20952
+ }
20953
+ }
20771
20954
  while (!stopped) {
20772
- const running = await countRunning();
20773
- const fresh = await loadQueue();
20774
- const next = selectNextRunnable(fresh, running);
20955
+ let occupancy;
20956
+ let next;
20957
+ if (gateByWorking && countWorking) {
20958
+ occupancy = await countWorking(cfg.idleGraceMs);
20959
+ const fresh = await loadQueue();
20960
+ next = selectNextRunnableByWorking(fresh, occupancy, cfg.maxWorking);
20961
+ } else {
20962
+ occupancy = await countRunning();
20963
+ const fresh = await loadQueue();
20964
+ next = selectNextRunnable(fresh, occupancy);
20965
+ }
20775
20966
  if (!next) return;
20776
20967
  const current = await readJob(next.id);
20777
20968
  if (!current || current.status !== "queued") continue;
@@ -20788,8 +20979,9 @@ function startQueueLoop(deps) {
20788
20979
  const withPid = { ...updated, pid };
20789
20980
  await writeJob(withPid);
20790
20981
  onStatusChange?.(withPid);
20982
+ const ceil = gateByWorking ? typeof updated.maxWorking === "number" && updated.maxWorking > 0 ? updated.maxWorking : cfg.maxWorking : updated.maxConcurrent;
20791
20983
  log(
20792
- `queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; running ${String(running + 1)}/${String(updated.maxConcurrent)}`
20984
+ `queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; ${gateByWorking ? "working" : "running"} ${String(occupancy + 1)}/${String(ceil)}`
20793
20985
  );
20794
20986
  } else {
20795
20987
  log(`queue: started job ${updated.id} (${updated.agent}); pid unknown`);
@@ -20858,6 +21050,33 @@ function processAlive(pid) {
20858
21050
  return false;
20859
21051
  }
20860
21052
  }
21053
+ async function defaultCountWorkingBoxes(registry, statusStore, idleGraceMs) {
21054
+ const boxes = registry.list();
21055
+ const registeredIds = new Set(boxes.map((b) => b.boxId));
21056
+ const entries = boxes.map((b) => {
21057
+ const active = readActiveAgent(statusStore.get(b.boxId));
21058
+ return {
21059
+ key: b.boxId,
21060
+ agentState: active.state,
21061
+ sinceUpdateMs: msSince2(active.updatedAt),
21062
+ sinceCreateMs: msSince2(b.createdAt)
21063
+ };
21064
+ });
21065
+ let count2 = countWorkingSlots(entries, idleGraceMs);
21066
+ let jobs;
21067
+ try {
21068
+ jobs = await loadQueue();
21069
+ } catch {
21070
+ return count2;
21071
+ }
21072
+ for (const j of jobs) {
21073
+ if (j.status !== "running") continue;
21074
+ if (j.boxId && registeredIds.has(j.boxId)) continue;
21075
+ if (typeof j.pid === "number" && !processAlive(j.pid)) continue;
21076
+ count2 += 1;
21077
+ }
21078
+ return count2;
21079
+ }
20861
21080
  var RUNNING_COUNT_CACHE_MS = 3e3;
20862
21081
  var runningCountCache = null;
20863
21082
  async function defaultCountRunningBoxes() {
@@ -20974,7 +21193,9 @@ program2.command("serve").description("Run the HTTP relay in the foreground").op
20974
21193
  });
20975
21194
  const queue = startQueueLoop({
20976
21195
  log: (line) => process.stdout.write(`agentbox-relay: ${line}
20977
- `)
21196
+ `),
21197
+ registry: handle.registry,
21198
+ statusStore: handle.statusStore
20978
21199
  });
20979
21200
  handle.setQueuePoke(() => {
20980
21201
  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