@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.
- package/CHANGELOG.md +89 -0
- package/README.md +161 -0
- package/dist/{_cloud-attach-T727ZPRV.js → _cloud-attach-O6NYTLES.js} +4 -4
- package/dist/{chunk-67N47KUS.js → chunk-2GPORKYF.js} +349 -182
- package/dist/chunk-2GPORKYF.js.map +1 -0
- package/dist/{chunk-6OZDFNBF.js → chunk-7UIAO7PC.js} +401 -82
- package/dist/chunk-7UIAO7PC.js.map +1 -0
- package/dist/{chunk-BGK32PZE.js → chunk-KL36BRN4.js} +2 -2
- package/dist/chunk-KL36BRN4.js.map +1 -0
- package/dist/chunk-MTVI44DW.js +662 -0
- package/dist/chunk-MTVI44DW.js.map +1 -0
- package/dist/{chunk-FODMEHD3.js → chunk-R4O5WPHW.js} +705 -77
- package/dist/chunk-R4O5WPHW.js.map +1 -0
- package/dist/{dist-ZODPD2I6.js → dist-5FQGYRW5.js} +20 -10
- package/dist/dist-5FQGYRW5.js.map +1 -0
- package/dist/{dist-LOZBWMBF.js → dist-BQNX7RQE.js} +19 -3
- package/dist/dist-PZW3GWWU.js +874 -0
- package/dist/dist-PZW3GWWU.js.map +1 -0
- package/dist/{dist-L4LCG5SJ.js → dist-TMHSUVTP.js} +4 -4
- package/dist/index.js +2385 -842
- package/dist/index.js.map +1 -1
- package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js → prepared-state-CL4CWXQA-H5THETIM.js} +2 -2
- package/package.json +11 -7
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +9 -8
- package/runtime/docker/packages/ctl/dist/bin.cjs +129 -31
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
- package/runtime/hetzner/agentbox-setup-skill.md +9 -8
- package/runtime/hetzner/agentbox-vnc-start +15 -1
- package/runtime/hetzner/ctl.cjs +129 -31
- package/runtime/relay/bin.cjs +260 -39
- package/runtime/vercel/agentbox-checkpoint-cleanup +52 -0
- package/runtime/vercel/agentbox-codex-hooks.json +68 -0
- package/runtime/vercel/agentbox-open +28 -0
- package/runtime/vercel/agentbox-setup-skill.md +197 -0
- package/runtime/vercel/agentbox-vnc-start +91 -0
- package/runtime/vercel/claude-managed-settings.json +115 -0
- package/runtime/vercel/ctl.cjs +23495 -0
- package/runtime/vercel/custom-system-CLAUDE.md +47 -0
- package/runtime/vercel/gh-shim +263 -0
- package/runtime/vercel/git-shim +131 -0
- package/runtime/vercel/scripts/provision.sh +314 -0
- package/share/agentbox-setup/SKILL.md +9 -8
- package/dist/chunk-67N47KUS.js.map +0 -1
- package/dist/chunk-6OZDFNBF.js.map +0 -1
- package/dist/chunk-BGK32PZE.js.map +0 -1
- package/dist/chunk-FODMEHD3.js.map +0 -1
- package/dist/dist-ZODPD2I6.js.map +0 -1
- /package/dist/{_cloud-attach-T727ZPRV.js.map → _cloud-attach-O6NYTLES.js.map} +0 -0
- /package/dist/{dist-LOZBWMBF.js.map → dist-BQNX7RQE.js.map} +0 -0
- /package/dist/{dist-L4LCG5SJ.js.map → dist-TMHSUVTP.js.map} +0 -0
- /package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js.map → prepared-state-CL4CWXQA-H5THETIM.js.map} +0 -0
package/runtime/relay/bin.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
18402
|
-
|
|
18403
|
-
|
|
18404
|
-
|
|
18405
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
20773
|
-
|
|
20774
|
-
|
|
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(
|
|
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
|