@madarco/agentbox 0.13.0 → 0.15.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 +125 -0
- package/README.md +11 -8
- package/dist/{_cloud-attach-HJC672UR.js → _cloud-attach-R6TRWG5L.js} +4 -4
- package/dist/{chunk-QYRK5H6Q.js → chunk-43Q5GWP6.js} +108 -56
- package/dist/chunk-43Q5GWP6.js.map +1 -0
- package/dist/{chunk-ECLLV5JH.js → chunk-72CJTXN6.js} +156 -5
- package/dist/chunk-72CJTXN6.js.map +1 -0
- package/dist/{chunk-R5XIDQFR.js → chunk-BKU34KYY.js} +170 -6
- package/dist/chunk-BKU34KYY.js.map +1 -0
- package/dist/{chunk-4NQXNQ53.js → chunk-E7CHS7ZR.js} +168 -58
- package/dist/chunk-E7CHS7ZR.js.map +1 -0
- package/dist/chunk-MCOU6CZS.js +346 -0
- package/dist/chunk-MCOU6CZS.js.map +1 -0
- package/dist/{chunk-B4QG2MCW.js → chunk-MLMFNN4T.js} +762 -483
- package/dist/chunk-MLMFNN4T.js.map +1 -0
- package/dist/{chunk-2LF5YILI.js → chunk-RSKG7AFU.js} +80 -6
- package/dist/chunk-RSKG7AFU.js.map +1 -0
- package/dist/{chunk-SNTHHWKY.js → chunk-XKH7NTT7.js} +80 -22
- package/dist/chunk-XKH7NTT7.js.map +1 -0
- package/dist/{dist-7KVUIKJX.js → dist-AGTIA7AD.js} +37 -226
- package/dist/dist-AGTIA7AD.js.map +1 -0
- package/dist/{dist-OPIBZ7XM.js → dist-FIFEFKJ7.js} +14 -69
- package/dist/dist-FIFEFKJ7.js.map +1 -0
- package/dist/dist-JZ3XO6EB.js +662 -0
- package/dist/dist-JZ3XO6EB.js.map +1 -0
- package/dist/{dist-OG6NW6SM.js → dist-OGJGZETZ.js} +5 -3
- package/dist/{dist-JAN5VABY.js → dist-S4XR4ACV.js} +25 -177
- package/dist/dist-S4XR4ACV.js.map +1 -0
- package/dist/index.js +2229 -1314
- package/dist/index.js.map +1 -1
- package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js → prepared-state-MQHD3M5F-Q27AZU53.js} +2 -2
- package/package.json +6 -4
- package/runtime/docker/Dockerfile.box +21 -26
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +67 -1
- package/runtime/docker/packages/ctl/dist/bin.cjs +361 -43
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +17 -6
- package/runtime/docker/packages/sandbox-docker/scripts/chromium-resolver +57 -0
- package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +2 -1
- package/runtime/e2b/agentbox-checkpoint-cleanup +52 -0
- package/runtime/e2b/agentbox-codex-hooks.json +68 -0
- package/runtime/e2b/agentbox-open +28 -0
- package/runtime/e2b/agentbox-setup-skill.md +263 -0
- package/runtime/e2b/agentbox-vnc-start +102 -0
- package/runtime/e2b/attach-helper.cjs +167 -0
- package/runtime/e2b/claude-managed-settings.json +116 -0
- package/runtime/e2b/ctl.cjs +24158 -0
- package/runtime/e2b/custom-system-CLAUDE.md +46 -0
- package/runtime/e2b/gh-shim +344 -0
- package/runtime/e2b/git-shim +131 -0
- package/runtime/e2b/scripts/build-template.sh +295 -0
- package/runtime/hetzner/agentbox-setup-skill.md +67 -1
- package/runtime/hetzner/agentbox-vnc-start +17 -6
- package/runtime/hetzner/claude-managed-settings.json +2 -1
- package/runtime/hetzner/ctl.cjs +361 -43
- package/runtime/relay/bin.cjs +380 -233
- package/runtime/vercel/agentbox-setup-skill.md +67 -1
- package/runtime/vercel/agentbox-vnc-start +17 -6
- package/runtime/vercel/claude-managed-settings.json +2 -1
- package/runtime/vercel/ctl.cjs +361 -43
- package/share/agentbox-setup/SKILL.md +67 -1
- package/share/host-skills/agentbox-info/SKILL.md +47 -35
- package/dist/chunk-2LF5YILI.js.map +0 -1
- package/dist/chunk-4NQXNQ53.js.map +0 -1
- package/dist/chunk-B4QG2MCW.js.map +0 -1
- package/dist/chunk-ECLLV5JH.js.map +0 -1
- package/dist/chunk-QYRK5H6Q.js.map +0 -1
- package/dist/chunk-R5XIDQFR.js.map +0 -1
- package/dist/chunk-SNTHHWKY.js.map +0 -1
- package/dist/dist-7KVUIKJX.js.map +0 -1
- package/dist/dist-JAN5VABY.js.map +0 -1
- package/dist/dist-OPIBZ7XM.js.map +0 -1
- /package/dist/{_cloud-attach-HJC672UR.js.map → _cloud-attach-R6TRWG5L.js.map} +0 -0
- /package/dist/{dist-OG6NW6SM.js.map → dist-OGJGZETZ.js.map} +0 -0
- /package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js.map → prepared-state-MQHD3M5F-Q27AZU53.js.map} +0 -0
package/runtime/relay/bin.cjs
CHANGED
|
@@ -3057,15 +3057,15 @@ var require_windows = __commonJS({
|
|
|
3057
3057
|
}
|
|
3058
3058
|
return false;
|
|
3059
3059
|
}
|
|
3060
|
-
function checkStat(
|
|
3061
|
-
if (!
|
|
3060
|
+
function checkStat(stat2, path6, options) {
|
|
3061
|
+
if (!stat2.isSymbolicLink() && !stat2.isFile()) {
|
|
3062
3062
|
return false;
|
|
3063
3063
|
}
|
|
3064
3064
|
return checkPathExt(path6, options);
|
|
3065
3065
|
}
|
|
3066
3066
|
function isexe(path6, options, cb) {
|
|
3067
|
-
fs.stat(path6, function(er,
|
|
3068
|
-
cb(er, er ? false : checkStat(
|
|
3067
|
+
fs.stat(path6, function(er, stat2) {
|
|
3068
|
+
cb(er, er ? false : checkStat(stat2, path6, options));
|
|
3069
3069
|
});
|
|
3070
3070
|
}
|
|
3071
3071
|
function sync(path6, options) {
|
|
@@ -3082,20 +3082,20 @@ var require_mode = __commonJS({
|
|
|
3082
3082
|
isexe.sync = sync;
|
|
3083
3083
|
var fs = require("fs");
|
|
3084
3084
|
function isexe(path6, options, cb) {
|
|
3085
|
-
fs.stat(path6, function(er,
|
|
3086
|
-
cb(er, er ? false : checkStat(
|
|
3085
|
+
fs.stat(path6, function(er, stat2) {
|
|
3086
|
+
cb(er, er ? false : checkStat(stat2, options));
|
|
3087
3087
|
});
|
|
3088
3088
|
}
|
|
3089
3089
|
function sync(path6, options) {
|
|
3090
3090
|
return checkStat(fs.statSync(path6), options);
|
|
3091
3091
|
}
|
|
3092
|
-
function checkStat(
|
|
3093
|
-
return
|
|
3092
|
+
function checkStat(stat2, options) {
|
|
3093
|
+
return stat2.isFile() && checkMode(stat2, options);
|
|
3094
3094
|
}
|
|
3095
|
-
function checkMode(
|
|
3096
|
-
var mod =
|
|
3097
|
-
var uid =
|
|
3098
|
-
var gid =
|
|
3095
|
+
function checkMode(stat2, options) {
|
|
3096
|
+
var mod = stat2.mode;
|
|
3097
|
+
var uid = stat2.uid;
|
|
3098
|
+
var gid = stat2.gid;
|
|
3099
3099
|
var myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid();
|
|
3100
3100
|
var myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid();
|
|
3101
3101
|
var u2 = parseInt("100", 8);
|
|
@@ -11808,11 +11808,11 @@ var replacements = Object.entries(specialMainSymbols);
|
|
|
11808
11808
|
// ../../node_modules/.pnpm/yoctocolors@2.1.2/node_modules/yoctocolors/base.js
|
|
11809
11809
|
var import_node_tty = __toESM(require("tty"), 1);
|
|
11810
11810
|
var hasColors = import_node_tty.default?.WriteStream?.prototype?.hasColors?.() ?? false;
|
|
11811
|
-
var format = (
|
|
11811
|
+
var format = (open2, close) => {
|
|
11812
11812
|
if (!hasColors) {
|
|
11813
11813
|
return (input) => input;
|
|
11814
11814
|
}
|
|
11815
|
-
const openCode = `\x1B[${
|
|
11815
|
+
const openCode = `\x1B[${open2}m`;
|
|
11816
11816
|
const closeCode = `\x1B[${close}m`;
|
|
11817
11817
|
return (input) => {
|
|
11818
11818
|
const string = input + "";
|
|
@@ -18367,6 +18367,21 @@ var HostInitiatedTokens = class {
|
|
|
18367
18367
|
var import_node_crypto2 = require("crypto");
|
|
18368
18368
|
var PendingPrompts = class {
|
|
18369
18369
|
entries = /* @__PURE__ */ new Map();
|
|
18370
|
+
autoApprove = null;
|
|
18371
|
+
/** Install the per-box auto-approve policy (relay server, once at startup). */
|
|
18372
|
+
setAutoApprovePolicy(policy) {
|
|
18373
|
+
this.autoApprove = policy;
|
|
18374
|
+
}
|
|
18375
|
+
/**
|
|
18376
|
+
* True when this box opted into `box.autoApproveHostActions`. Records the
|
|
18377
|
+
* bypass to the audit sink as a side effect so the caller short-circuits
|
|
18378
|
+
* with a trail. Returns false when no policy is installed.
|
|
18379
|
+
*/
|
|
18380
|
+
consumeAutoApprove(boxId, params) {
|
|
18381
|
+
if (!this.autoApprove || !this.autoApprove.shouldAutoApprove(boxId)) return false;
|
|
18382
|
+
this.autoApprove.audit(boxId, params);
|
|
18383
|
+
return true;
|
|
18384
|
+
}
|
|
18370
18385
|
add(boxId, ev) {
|
|
18371
18386
|
return new Promise((resolve2) => {
|
|
18372
18387
|
this.entries.set(ev.id, {
|
|
@@ -18453,6 +18468,9 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
|
|
|
18453
18468
|
if (process.env.AGENTBOX_PROMPT === "off") {
|
|
18454
18469
|
return { answer: "y" };
|
|
18455
18470
|
}
|
|
18471
|
+
if (prompts.consumeAutoApprove(boxId, params)) {
|
|
18472
|
+
return { answer: "y" };
|
|
18473
|
+
}
|
|
18456
18474
|
const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
|
|
18457
18475
|
const promise = prompts.add(boxId, ev);
|
|
18458
18476
|
subscribers.broadcast(boxId, "prompt-ask", ev);
|
|
@@ -18492,6 +18510,10 @@ async function resolveCloudBackend(name) {
|
|
|
18492
18510
|
const pkg = "@agentbox/sandbox-vercel";
|
|
18493
18511
|
return loadCloudBackend(pkg, async () => (await import(pkg)).vercelBackend);
|
|
18494
18512
|
}
|
|
18513
|
+
if (name === "e2b") {
|
|
18514
|
+
const pkg = "@agentbox/sandbox-e2b";
|
|
18515
|
+
return loadCloudBackend(pkg, async () => (await import(pkg)).e2bBackend);
|
|
18516
|
+
}
|
|
18495
18517
|
throw new Error(`no host executor for cloud backend '${name}'`);
|
|
18496
18518
|
}
|
|
18497
18519
|
async function loadCloudBackend(pkg, load2) {
|
|
@@ -19426,6 +19448,21 @@ function createRelayServer(opts) {
|
|
|
19426
19448
|
const prompts = new PendingPrompts();
|
|
19427
19449
|
const subscribers = new PromptSubscribers();
|
|
19428
19450
|
const notices = new BoxNotices(subscribers);
|
|
19451
|
+
prompts.setAutoApprovePolicy({
|
|
19452
|
+
shouldAutoApprove: (boxId) => registry.get(boxId)?.autoApproveHostActions === true,
|
|
19453
|
+
audit: (boxId, params) => {
|
|
19454
|
+
events.append({
|
|
19455
|
+
boxId,
|
|
19456
|
+
type: "host-action-auto-approved",
|
|
19457
|
+
payload: {
|
|
19458
|
+
command: params.context?.command,
|
|
19459
|
+
argv: params.context?.argv,
|
|
19460
|
+
message: params.message
|
|
19461
|
+
}
|
|
19462
|
+
});
|
|
19463
|
+
log(`auto-approved host action for ${boxId}: ${params.context?.command ?? params.message}`);
|
|
19464
|
+
}
|
|
19465
|
+
});
|
|
19429
19466
|
const hostInitiatedTokens = new HostInitiatedTokens();
|
|
19430
19467
|
let queuePoke = null;
|
|
19431
19468
|
const host = opts.host ?? "0.0.0.0";
|
|
@@ -19613,11 +19650,22 @@ function createRelayServer(opts) {
|
|
|
19613
19650
|
send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
|
|
19614
19651
|
return;
|
|
19615
19652
|
}
|
|
19653
|
+
if (params.exclude !== void 0 && (!Array.isArray(params.exclude) || params.exclude.some((p) => typeof p !== "string"))) {
|
|
19654
|
+
send(res, 400, { error: "cp.* exclude must be an array of strings" });
|
|
19655
|
+
return;
|
|
19656
|
+
}
|
|
19616
19657
|
const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
|
|
19658
|
+
const pathDetail = body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`;
|
|
19659
|
+
const detailParts = [pathDetail];
|
|
19660
|
+
if (params.exclude && params.exclude.length > 0) {
|
|
19661
|
+
detailParts.push(`exclude: ${params.exclude.join(", ")}`);
|
|
19662
|
+
}
|
|
19663
|
+
if (params.defaultExcludes === false) detailParts.push("(default excludes off)");
|
|
19664
|
+
if (params.yes) detailParts.push("(over size limit \u2014 confirmed)");
|
|
19617
19665
|
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
19618
19666
|
kind: "confirm",
|
|
19619
19667
|
message: `Allow cp (${direction}) on ${reg.name}?`,
|
|
19620
|
-
detail:
|
|
19668
|
+
detail: detailParts.join("\n"),
|
|
19621
19669
|
defaultAnswer: "n",
|
|
19622
19670
|
context: {
|
|
19623
19671
|
command: body.method,
|
|
@@ -19783,7 +19831,8 @@ function createRelayServer(opts) {
|
|
|
19783
19831
|
worktrees,
|
|
19784
19832
|
previewUrl: typeof body.previewUrl === "string" && body.previewUrl.length > 0 ? body.previewUrl : void 0,
|
|
19785
19833
|
previewToken: typeof body.previewToken === "string" && body.previewToken.length > 0 ? body.previewToken : void 0,
|
|
19786
|
-
bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0
|
|
19834
|
+
bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0,
|
|
19835
|
+
autoApproveHostActions: body.autoApproveHostActions === true
|
|
19787
19836
|
};
|
|
19788
19837
|
registry.register(reg);
|
|
19789
19838
|
log(
|
|
@@ -19891,6 +19940,15 @@ function createRelayServer(opts) {
|
|
|
19891
19940
|
send(res, 200, { boxes: redacted });
|
|
19892
19941
|
return;
|
|
19893
19942
|
}
|
|
19943
|
+
if (route === "GET /admin/prompts") {
|
|
19944
|
+
const boxId = url.searchParams.get("boxId") ?? "";
|
|
19945
|
+
if (boxId.length === 0) {
|
|
19946
|
+
send(res, 400, { error: "missing boxId query param" });
|
|
19947
|
+
return;
|
|
19948
|
+
}
|
|
19949
|
+
send(res, 200, { prompts: prompts.forBox(boxId) });
|
|
19950
|
+
return;
|
|
19951
|
+
}
|
|
19894
19952
|
if (route === "GET /admin/prompts/stream") {
|
|
19895
19953
|
const boxId = url.searchParams.get("boxId") ?? "";
|
|
19896
19954
|
if (boxId.length === 0) {
|
|
@@ -20208,7 +20266,11 @@ async function handleCpRpc(reg, method, params) {
|
|
|
20208
20266
|
};
|
|
20209
20267
|
}
|
|
20210
20268
|
const boxRef = `${reg.name}:${params.boxPath}`;
|
|
20211
|
-
const
|
|
20269
|
+
const flags = [];
|
|
20270
|
+
for (const pat of params.exclude ?? []) flags.push("--exclude", pat);
|
|
20271
|
+
if (params.defaultExcludes === false) flags.push("--no-default-excludes");
|
|
20272
|
+
if (params.yes) flags.push("--yes");
|
|
20273
|
+
const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath, ...flags] : [process.execPath, entry, "cp", params.hostPath, boxRef, ...flags];
|
|
20212
20274
|
return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
|
|
20213
20275
|
}
|
|
20214
20276
|
async function handleDownloadRpc(reg, kind) {
|
|
@@ -20306,8 +20368,8 @@ async function startRelayServer(opts) {
|
|
|
20306
20368
|
}
|
|
20307
20369
|
|
|
20308
20370
|
// src/autopause.ts
|
|
20309
|
-
var
|
|
20310
|
-
var
|
|
20371
|
+
var import_node_child_process9 = require("child_process");
|
|
20372
|
+
var import_promises18 = require("fs/promises");
|
|
20311
20373
|
|
|
20312
20374
|
// ../config/dist/index.js
|
|
20313
20375
|
var import_yaml = __toESM(require_dist(), 1);
|
|
@@ -20325,15 +20387,18 @@ var BUILT_IN_DEFAULTS = {
|
|
|
20325
20387
|
defaultCheckpointDaytona: "",
|
|
20326
20388
|
defaultCheckpointHetzner: "",
|
|
20327
20389
|
defaultCheckpointVercel: "",
|
|
20390
|
+
defaultCheckpointE2b: "",
|
|
20328
20391
|
size: "",
|
|
20329
20392
|
sizeDocker: "",
|
|
20330
20393
|
sizeDaytona: "",
|
|
20331
20394
|
sizeHetzner: "",
|
|
20332
20395
|
sizeVercel: "",
|
|
20396
|
+
sizeE2b: "",
|
|
20333
20397
|
withPlaywright: false,
|
|
20334
20398
|
withEnv: false,
|
|
20335
20399
|
resyncOnStart: true,
|
|
20336
20400
|
vnc: true,
|
|
20401
|
+
autoApproveHostActions: false,
|
|
20337
20402
|
isolateClaudeConfig: false,
|
|
20338
20403
|
isolateCodexConfig: false,
|
|
20339
20404
|
isolateOpencodeConfig: false,
|
|
@@ -20342,6 +20407,7 @@ var BUILT_IN_DEFAULTS = {
|
|
|
20342
20407
|
imageDaytona: "",
|
|
20343
20408
|
imageHetzner: "",
|
|
20344
20409
|
imageVercel: "",
|
|
20410
|
+
imageE2b: "",
|
|
20345
20411
|
// Mirrors BOX_IMAGE_REGISTRY in @agentbox/sandbox-docker. Empty disables the
|
|
20346
20412
|
// registry pull (always build the docker base image locally).
|
|
20347
20413
|
imageRegistry: "ghcr.io/madarco/agentbox/box",
|
|
@@ -20353,7 +20419,8 @@ var BUILT_IN_DEFAULTS = {
|
|
|
20353
20419
|
bundleDepth: void 0,
|
|
20354
20420
|
vercelVcpus: 2,
|
|
20355
20421
|
vercelTimeoutMs: 27e5,
|
|
20356
|
-
vercelNetworkPolicy: ""
|
|
20422
|
+
vercelNetworkPolicy: "",
|
|
20423
|
+
cpMaxBytes: 100 * 1024 * 1024
|
|
20357
20424
|
},
|
|
20358
20425
|
checkpoint: {
|
|
20359
20426
|
maxLayers: 3
|
|
@@ -20409,7 +20476,8 @@ var BUILT_IN_DEFAULTS = {
|
|
|
20409
20476
|
enabled: true,
|
|
20410
20477
|
maxConcurrent: 5,
|
|
20411
20478
|
maxWorking: 0,
|
|
20412
|
-
idleGraceSeconds: 15
|
|
20479
|
+
idleGraceSeconds: 15,
|
|
20480
|
+
openIn: "none"
|
|
20413
20481
|
},
|
|
20414
20482
|
cloud: {
|
|
20415
20483
|
useCurrentBranch: false
|
|
@@ -20423,8 +20491,8 @@ var KEY_REGISTRY = [
|
|
|
20423
20491
|
{
|
|
20424
20492
|
key: "box.provider",
|
|
20425
20493
|
type: "enum",
|
|
20426
|
-
enumValues: ["docker", "daytona", "hetzner", "vercel"],
|
|
20427
|
-
description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or
|
|
20494
|
+
enumValues: ["docker", "daytona", "hetzner", "vercel", "e2b"],
|
|
20495
|
+
description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, Vercel Sandboxes, or E2B microVMs."
|
|
20428
20496
|
},
|
|
20429
20497
|
{
|
|
20430
20498
|
key: "box.hostSnapshot",
|
|
@@ -20460,6 +20528,12 @@ var KEY_REGISTRY = [
|
|
|
20460
20528
|
description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
|
|
20461
20529
|
advanced: true
|
|
20462
20530
|
},
|
|
20531
|
+
{
|
|
20532
|
+
key: "box.defaultCheckpointE2b",
|
|
20533
|
+
type: "string",
|
|
20534
|
+
description: "Per-provider override of `box.defaultCheckpoint` for e2b. Wins over the global when set; set via `agentbox checkpoint set-default --provider e2b`.",
|
|
20535
|
+
advanced: true
|
|
20536
|
+
},
|
|
20463
20537
|
{
|
|
20464
20538
|
key: "box.size",
|
|
20465
20539
|
type: "string",
|
|
@@ -20489,6 +20563,12 @@ var KEY_REGISTRY = [
|
|
|
20489
20563
|
description: "Per-provider override of `box.size` for vercel. Reserved \u2014 vercel sizing is controlled via `box.vercelVcpus`.",
|
|
20490
20564
|
advanced: true
|
|
20491
20565
|
},
|
|
20566
|
+
{
|
|
20567
|
+
key: "box.sizeE2b",
|
|
20568
|
+
type: "string",
|
|
20569
|
+
description: "Per-provider override of `box.size` for e2b. Reserved \u2014 e2b sizing is template-level (set at `agentbox prepare --provider e2b` time via --vcpus / --memory).",
|
|
20570
|
+
advanced: true
|
|
20571
|
+
},
|
|
20492
20572
|
{
|
|
20493
20573
|
key: "checkpoint.maxLayers",
|
|
20494
20574
|
type: "int",
|
|
@@ -20515,6 +20595,11 @@ var KEY_REGISTRY = [
|
|
|
20515
20595
|
type: "bool",
|
|
20516
20596
|
description: "Run the per-box Xvnc + noVNC stack."
|
|
20517
20597
|
},
|
|
20598
|
+
{
|
|
20599
|
+
key: "box.autoApproveHostActions",
|
|
20600
|
+
type: "bool",
|
|
20601
|
+
description: "Auto-approve host-action confirmations (git push, cp host<->box, gh PR writes, checkpoint) for this box without an interactive prompt. Off by default; intended for unattended orchestration of trusted boxes. Each auto-approval is recorded as a relay event (visible in `agentbox agent` / the dashboard)."
|
|
20602
|
+
},
|
|
20518
20603
|
{
|
|
20519
20604
|
key: "box.isolateClaudeConfig",
|
|
20520
20605
|
type: "bool",
|
|
@@ -20560,6 +20645,12 @@ var KEY_REGISTRY = [
|
|
|
20560
20645
|
description: "Per-provider override of `box.image` for vercel (snapshot id, e.g. `snap_\u2026`). Written by `agentbox prepare --provider vercel`.",
|
|
20561
20646
|
advanced: true
|
|
20562
20647
|
},
|
|
20648
|
+
{
|
|
20649
|
+
key: "box.imageE2b",
|
|
20650
|
+
type: "string",
|
|
20651
|
+
description: "Per-provider override of `box.image` for e2b (template id or `name:tag`, e.g. `agentbox-base:latest`). Written by `agentbox prepare --provider e2b`.",
|
|
20652
|
+
advanced: true
|
|
20653
|
+
},
|
|
20563
20654
|
{
|
|
20564
20655
|
key: "box.imageRegistry",
|
|
20565
20656
|
type: "string",
|
|
@@ -20607,6 +20698,12 @@ var KEY_REGISTRY = [
|
|
|
20607
20698
|
type: "int",
|
|
20608
20699
|
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."
|
|
20609
20700
|
},
|
|
20701
|
+
{
|
|
20702
|
+
key: "box.cpMaxBytes",
|
|
20703
|
+
type: "int",
|
|
20704
|
+
description: "Max bytes a single host\u2192box copy may transfer after excludes, shared by `agentbox cp` (blocked with a size breakdown unless --yes) and each `carry:` entry (rejected at resolve time). Default 104857600 (100 MiB).",
|
|
20705
|
+
advanced: true
|
|
20706
|
+
},
|
|
20610
20707
|
{
|
|
20611
20708
|
key: "box.vercelNetworkPolicy",
|
|
20612
20709
|
type: "string",
|
|
@@ -20754,6 +20851,12 @@ var KEY_REGISTRY = [
|
|
|
20754
20851
|
type: "int",
|
|
20755
20852
|
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."
|
|
20756
20853
|
},
|
|
20854
|
+
{
|
|
20855
|
+
key: "queue.openIn",
|
|
20856
|
+
type: "enum",
|
|
20857
|
+
enumValues: ["none", "split", "window", "tab"],
|
|
20858
|
+
description: "When a background `-i` job finishes creating its box, where the host relay opens an attached terminal onto it: `none` (default \u2014 open nothing, just queue), `split`, `window`, or `tab`. Honored only when the submitting shell runs inside tmux, cmux, or iTerm2 (the targeting is captured at submit time). Under cmux, `split` splits the pane you submitted from (falling back to the parent workspace, then a new workspace), `tab` adds a tab in the parent workspace, and `window` opens a separate workspace; iTerm2 opens relative to the frontmost window. Unlike `attach.openIn` there is no `same` mode \u2014 the box is created asynchronously, so it is always a fresh terminal."
|
|
20859
|
+
},
|
|
20757
20860
|
{
|
|
20758
20861
|
key: "cloud.useCurrentBranch",
|
|
20759
20862
|
type: "bool",
|
|
@@ -20900,210 +21003,18 @@ var GLOBAL_CONFIG_FILE = (0, import_path2.join)(STATE_DIR2, "config.yaml");
|
|
|
20900
21003
|
var PROJECTS_DIR = (0, import_path2.join)(STATE_DIR2, "projects");
|
|
20901
21004
|
var PROJECT_GC_COUNTER_FILE = (0, import_path3.join)(PROJECTS_DIR, ".gc.json");
|
|
20902
21005
|
|
|
20903
|
-
// src/autopause.ts
|
|
20904
|
-
function selectBoxesToPause(entries, cfg) {
|
|
20905
|
-
if (!cfg.enabled) return [];
|
|
20906
|
-
const runningCount = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
|
|
20907
|
-
const excess = runningCount - cfg.maxRunningBoxes;
|
|
20908
|
-
if (excess <= 0) return [];
|
|
20909
|
-
const idleThresholdMs = cfg.idleMinutes * 6e4;
|
|
20910
|
-
const candidates = entries.filter(
|
|
20911
|
-
(e) => e.running && e.claudeState === "idle" && e.idleMs != null && e.idleMs >= idleThresholdMs
|
|
20912
|
-
);
|
|
20913
|
-
candidates.sort(
|
|
20914
|
-
(a2, b) => b.idleMs - a2.idleMs || a2.createdAt - b.createdAt || (a2.boxId < b.boxId ? -1 : a2.boxId > b.boxId ? 1 : 0)
|
|
20915
|
-
);
|
|
20916
|
-
return candidates.slice(0, excess).map((e) => e.boxId);
|
|
20917
|
-
}
|
|
20918
|
-
async function loadAutopauseConfig() {
|
|
20919
|
-
const d = BUILT_IN_DEFAULTS.autopause;
|
|
20920
|
-
let global3 = {};
|
|
20921
|
-
try {
|
|
20922
|
-
global3 = parseUserConfig(await (0, import_promises16.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
|
|
20923
|
-
} catch {
|
|
20924
|
-
}
|
|
20925
|
-
const a2 = global3.autopause ?? {};
|
|
20926
|
-
return {
|
|
20927
|
-
enabled: a2.enabled ?? d.enabled,
|
|
20928
|
-
maxRunningBoxes: a2.maxRunningBoxes ?? d.maxRunningBoxes,
|
|
20929
|
-
idleMinutes: a2.idleMinutes ?? d.idleMinutes
|
|
20930
|
-
};
|
|
20931
|
-
}
|
|
20932
|
-
var DEFAULT_INTERVAL_MS = 6e4;
|
|
20933
|
-
function startAutopauseLoop(deps) {
|
|
20934
|
-
const loadConfig = deps.loadConfig ?? loadAutopauseConfig;
|
|
20935
|
-
const inspectStatus = deps.inspectStatus ?? inspectContainerState;
|
|
20936
|
-
const pause = deps.pause ?? pauseContainer;
|
|
20937
|
-
const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
20938
|
-
const { registry, statusStore, events, log } = deps;
|
|
20939
|
-
let ticking = false;
|
|
20940
|
-
let stopped = false;
|
|
20941
|
-
let inFlight = Promise.resolve();
|
|
20942
|
-
async function tick() {
|
|
20943
|
-
if (ticking) return;
|
|
20944
|
-
ticking = true;
|
|
20945
|
-
try {
|
|
20946
|
-
const cfg = await loadConfig();
|
|
20947
|
-
if (!cfg.enabled) return;
|
|
20948
|
-
const entries = [];
|
|
20949
|
-
for (const reg of registry.list()) {
|
|
20950
|
-
if (!reg.containerName) continue;
|
|
20951
|
-
const state = await inspectStatus(reg.containerName);
|
|
20952
|
-
const claude = readClaude(statusStore.get(reg.boxId));
|
|
20953
|
-
const idleMs = claude.state === "idle" && claude.updatedAt ? msSince(claude.updatedAt) : null;
|
|
20954
|
-
entries.push({
|
|
20955
|
-
boxId: reg.boxId,
|
|
20956
|
-
containerName: reg.containerName,
|
|
20957
|
-
running: state === "running",
|
|
20958
|
-
claudeState: claude.state,
|
|
20959
|
-
idleMs,
|
|
20960
|
-
createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0
|
|
20961
|
-
});
|
|
20962
|
-
}
|
|
20963
|
-
const toPause = selectBoxesToPause(entries, cfg);
|
|
20964
|
-
if (toPause.length === 0) return;
|
|
20965
|
-
const byId = new Map(entries.map((e) => [e.boxId, e]));
|
|
20966
|
-
const runningBefore = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
|
|
20967
|
-
for (const boxId of toPause) {
|
|
20968
|
-
const e = byId.get(boxId);
|
|
20969
|
-
if (!e) continue;
|
|
20970
|
-
try {
|
|
20971
|
-
await pause(e.containerName);
|
|
20972
|
-
const mins = e.idleMs != null ? Math.round(e.idleMs / 6e4) : null;
|
|
20973
|
-
events.append({
|
|
20974
|
-
boxId,
|
|
20975
|
-
type: "autopause",
|
|
20976
|
-
payload: {
|
|
20977
|
-
containerName: e.containerName,
|
|
20978
|
-
action: "paused",
|
|
20979
|
-
idleMs: e.idleMs,
|
|
20980
|
-
runningBefore,
|
|
20981
|
-
max: cfg.maxRunningBoxes
|
|
20982
|
-
}
|
|
20983
|
-
});
|
|
20984
|
-
log(
|
|
20985
|
-
`autopause: paused box ${boxId} (${e.containerName})` + (mins != null ? ` after ~${String(mins)}m idle` : "") + `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`
|
|
20986
|
-
);
|
|
20987
|
-
} catch (err) {
|
|
20988
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
20989
|
-
log(`autopause: docker pause ${e.containerName} failed: ${msg}`);
|
|
20990
|
-
events.append({
|
|
20991
|
-
boxId,
|
|
20992
|
-
type: "autopause",
|
|
20993
|
-
payload: { containerName: e.containerName, action: "pause-failed", error: msg }
|
|
20994
|
-
});
|
|
20995
|
-
}
|
|
20996
|
-
}
|
|
20997
|
-
} catch (err) {
|
|
20998
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
20999
|
-
log(`autopause: tick error: ${msg}`);
|
|
21000
|
-
} finally {
|
|
21001
|
-
ticking = false;
|
|
21002
|
-
}
|
|
21003
|
-
}
|
|
21004
|
-
const timer = setInterval(() => {
|
|
21005
|
-
if (stopped) return;
|
|
21006
|
-
inFlight = tick();
|
|
21007
|
-
}, intervalMs);
|
|
21008
|
-
timer.unref();
|
|
21009
|
-
return {
|
|
21010
|
-
stop: async () => {
|
|
21011
|
-
stopped = true;
|
|
21012
|
-
clearInterval(timer);
|
|
21013
|
-
await inFlight.catch(() => {
|
|
21014
|
-
});
|
|
21015
|
-
}
|
|
21016
|
-
};
|
|
21017
|
-
}
|
|
21018
|
-
function readClaude(snap) {
|
|
21019
|
-
const c3 = snap && typeof snap === "object" ? snap.claude : void 0;
|
|
21020
|
-
if (!c3 || typeof c3 !== "object") return { state: null, updatedAt: null };
|
|
21021
|
-
const o2 = c3;
|
|
21022
|
-
const state = o2.state === "working" || o2.state === "idle" || o2.state === "waiting" || o2.state === "unknown" ? o2.state : null;
|
|
21023
|
-
return { state, updatedAt: typeof o2.updatedAt === "string" ? o2.updatedAt : null };
|
|
21024
|
-
}
|
|
21025
|
-
function msSince(iso) {
|
|
21026
|
-
const t = Date.parse(iso);
|
|
21027
|
-
return Number.isNaN(t) ? null : Date.now() - t;
|
|
21028
|
-
}
|
|
21029
|
-
function toEpoch(iso) {
|
|
21030
|
-
const t = Date.parse(iso);
|
|
21031
|
-
return Number.isNaN(t) ? 0 : t;
|
|
21032
|
-
}
|
|
21033
|
-
var INSPECT_TIMEOUT_MS = 15e3;
|
|
21034
|
-
var PAUSE_TIMEOUT_MS = 3e4;
|
|
21035
|
-
function runDocker(args, timeoutMs) {
|
|
21036
|
-
return new Promise((resolve2) => {
|
|
21037
|
-
const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
21038
|
-
let stdout = "";
|
|
21039
|
-
let stderr = "";
|
|
21040
|
-
let settled = false;
|
|
21041
|
-
const finish = (exitCode) => {
|
|
21042
|
-
if (settled) return;
|
|
21043
|
-
settled = true;
|
|
21044
|
-
resolve2({ exitCode, stdout, stderr });
|
|
21045
|
-
};
|
|
21046
|
-
const timer = setTimeout(() => {
|
|
21047
|
-
child.kill("SIGTERM");
|
|
21048
|
-
stderr += `
|
|
21049
|
-
relay: docker ${args.join(" ")} timed out after ${String(timeoutMs)}ms
|
|
21050
|
-
`;
|
|
21051
|
-
finish(124);
|
|
21052
|
-
}, timeoutMs);
|
|
21053
|
-
child.stdout?.on("data", (c3) => {
|
|
21054
|
-
stdout += c3.toString("utf8");
|
|
21055
|
-
});
|
|
21056
|
-
child.stderr?.on("data", (c3) => {
|
|
21057
|
-
stderr += c3.toString("utf8");
|
|
21058
|
-
});
|
|
21059
|
-
child.on("error", (err) => {
|
|
21060
|
-
clearTimeout(timer);
|
|
21061
|
-
stderr += String(err.message ?? err);
|
|
21062
|
-
finish(127);
|
|
21063
|
-
});
|
|
21064
|
-
child.on("close", (code) => {
|
|
21065
|
-
clearTimeout(timer);
|
|
21066
|
-
finish(code ?? -1);
|
|
21067
|
-
});
|
|
21068
|
-
});
|
|
21069
|
-
}
|
|
21070
|
-
async function inspectContainerState(name) {
|
|
21071
|
-
const r = await runDocker(["inspect", "--format", "{{.State.Status}}", name], INSPECT_TIMEOUT_MS);
|
|
21072
|
-
if (r.exitCode !== 0) return "missing";
|
|
21073
|
-
switch (r.stdout.trim()) {
|
|
21074
|
-
case "running":
|
|
21075
|
-
return "running";
|
|
21076
|
-
case "paused":
|
|
21077
|
-
return "paused";
|
|
21078
|
-
case "created":
|
|
21079
|
-
case "exited":
|
|
21080
|
-
case "dead":
|
|
21081
|
-
case "restarting":
|
|
21082
|
-
case "removing":
|
|
21083
|
-
return "stopped";
|
|
21084
|
-
default:
|
|
21085
|
-
return "missing";
|
|
21086
|
-
}
|
|
21087
|
-
}
|
|
21088
|
-
async function pauseContainer(name) {
|
|
21089
|
-
const r = await runDocker(["pause", name], PAUSE_TIMEOUT_MS);
|
|
21090
|
-
if (r.exitCode !== 0) {
|
|
21091
|
-
throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);
|
|
21092
|
-
}
|
|
21093
|
-
}
|
|
21094
|
-
|
|
21095
21006
|
// src/queue.ts
|
|
21096
|
-
var
|
|
21097
|
-
var
|
|
21007
|
+
var import_node_child_process8 = require("child_process");
|
|
21008
|
+
var import_promises16 = require("fs/promises");
|
|
21098
21009
|
var import_node_fs6 = require("fs");
|
|
21099
21010
|
var import_node_path8 = require("path");
|
|
21100
|
-
var
|
|
21011
|
+
var import_promises17 = require("timers/promises");
|
|
21101
21012
|
var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
|
|
21102
21013
|
async function loadQueueConfig() {
|
|
21103
21014
|
const d = BUILT_IN_DEFAULTS.queue;
|
|
21104
21015
|
let global3 = {};
|
|
21105
21016
|
try {
|
|
21106
|
-
global3 = parseUserConfig(await (0,
|
|
21017
|
+
global3 = parseUserConfig(await (0, import_promises16.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
|
|
21107
21018
|
} catch {
|
|
21108
21019
|
}
|
|
21109
21020
|
const q = global3.queue ?? {};
|
|
@@ -21115,25 +21026,32 @@ async function loadQueueConfig() {
|
|
|
21115
21026
|
};
|
|
21116
21027
|
}
|
|
21117
21028
|
async function writeJob(job) {
|
|
21118
|
-
await (0,
|
|
21029
|
+
await (0, import_promises16.mkdir)(QUEUE_DIR, { recursive: true });
|
|
21119
21030
|
const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
|
|
21120
21031
|
const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
|
|
21121
|
-
await (0,
|
|
21122
|
-
await (0,
|
|
21032
|
+
await (0, import_promises16.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
|
|
21033
|
+
await (0, import_promises16.rename)(tmp, final);
|
|
21123
21034
|
}
|
|
21124
21035
|
async function readJob(id) {
|
|
21125
21036
|
try {
|
|
21126
|
-
const raw = await (0,
|
|
21037
|
+
const raw = await (0, import_promises16.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
|
|
21127
21038
|
return JSON.parse(raw);
|
|
21128
21039
|
} catch (err) {
|
|
21129
21040
|
if (err.code === "ENOENT") return null;
|
|
21130
21041
|
throw err;
|
|
21131
21042
|
}
|
|
21132
21043
|
}
|
|
21044
|
+
async function deleteJob(id) {
|
|
21045
|
+
try {
|
|
21046
|
+
await (0, import_promises16.unlink)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`));
|
|
21047
|
+
} catch (err) {
|
|
21048
|
+
if (err.code !== "ENOENT") throw err;
|
|
21049
|
+
}
|
|
21050
|
+
}
|
|
21133
21051
|
async function loadQueue() {
|
|
21134
21052
|
let entries;
|
|
21135
21053
|
try {
|
|
21136
|
-
entries = await (0,
|
|
21054
|
+
entries = await (0, import_promises16.readdir)(QUEUE_DIR);
|
|
21137
21055
|
} catch (err) {
|
|
21138
21056
|
if (err.code === "ENOENT") return [];
|
|
21139
21057
|
throw err;
|
|
@@ -21142,7 +21060,7 @@ async function loadQueue() {
|
|
|
21142
21060
|
for (const name of entries) {
|
|
21143
21061
|
if (!name.endsWith(".json")) continue;
|
|
21144
21062
|
try {
|
|
21145
|
-
const raw = await (0,
|
|
21063
|
+
const raw = await (0, import_promises16.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
|
|
21146
21064
|
out.push(JSON.parse(raw));
|
|
21147
21065
|
} catch {
|
|
21148
21066
|
}
|
|
@@ -21216,22 +21134,23 @@ function parseTime(iso) {
|
|
|
21216
21134
|
const t = Date.parse(iso);
|
|
21217
21135
|
return Number.isNaN(t) ? 0 : t;
|
|
21218
21136
|
}
|
|
21219
|
-
function
|
|
21137
|
+
function msSince(iso) {
|
|
21220
21138
|
if (!iso) return null;
|
|
21221
21139
|
const t = Date.parse(iso);
|
|
21222
21140
|
return Number.isNaN(t) ? null : Date.now() - t;
|
|
21223
21141
|
}
|
|
21224
|
-
var
|
|
21142
|
+
var DEFAULT_INTERVAL_MS = 2e3;
|
|
21225
21143
|
function startQueueLoop(deps) {
|
|
21226
21144
|
const loadConfig = deps.loadConfig ?? loadQueueConfig;
|
|
21227
21145
|
const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
|
|
21228
21146
|
const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
|
|
21229
|
-
const intervalMs = deps.intervalMs ??
|
|
21147
|
+
const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
21230
21148
|
const { log, onStatusChange } = deps;
|
|
21231
21149
|
const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
|
|
21232
21150
|
let ticking = false;
|
|
21233
21151
|
let stopped = false;
|
|
21234
21152
|
let warnedNoWorkingDeps = false;
|
|
21153
|
+
let lastSweepAt = 0;
|
|
21235
21154
|
let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
|
|
21236
21155
|
log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
21237
21156
|
});
|
|
@@ -21241,6 +21160,13 @@ function startQueueLoop(deps) {
|
|
|
21241
21160
|
try {
|
|
21242
21161
|
const cfg = await loadConfig();
|
|
21243
21162
|
if (!cfg.enabled) return;
|
|
21163
|
+
const now = Date.now();
|
|
21164
|
+
if (now - lastSweepAt >= SWEEP_INTERVAL_MS) {
|
|
21165
|
+
lastSweepAt = now;
|
|
21166
|
+
await sweepTerminalJobs(log, now).catch((err) => {
|
|
21167
|
+
log(`queue: sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
21168
|
+
});
|
|
21169
|
+
}
|
|
21244
21170
|
const jobs = await loadQueue();
|
|
21245
21171
|
const hasQueued = jobs.some((j) => j.status === "queued");
|
|
21246
21172
|
if (!hasQueued) return;
|
|
@@ -21343,6 +21269,21 @@ async function recoverOrphanedWorkers(log, onChange) {
|
|
|
21343
21269
|
log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
|
|
21344
21270
|
}
|
|
21345
21271
|
}
|
|
21272
|
+
var TERMINAL_RETENTION_MS = 60 * 60 * 1e3;
|
|
21273
|
+
var SWEEP_INTERVAL_MS = 60 * 1e3;
|
|
21274
|
+
var TERMINAL_STATUSES = ["done", "failed", "cancelled"];
|
|
21275
|
+
async function sweepTerminalJobs(log, now) {
|
|
21276
|
+
const jobs = await loadQueue();
|
|
21277
|
+
let swept = 0;
|
|
21278
|
+
for (const j of jobs) {
|
|
21279
|
+
if (!TERMINAL_STATUSES.includes(j.status)) continue;
|
|
21280
|
+
const since = Date.parse(j.finishedAt ?? j.createdAt);
|
|
21281
|
+
if (Number.isNaN(since) || now - since < TERMINAL_RETENTION_MS) continue;
|
|
21282
|
+
await deleteJob(j.id);
|
|
21283
|
+
swept += 1;
|
|
21284
|
+
}
|
|
21285
|
+
if (swept > 0) log(`queue: swept ${String(swept)} stale terminal manifest(s)`);
|
|
21286
|
+
}
|
|
21346
21287
|
function processAlive(pid) {
|
|
21347
21288
|
try {
|
|
21348
21289
|
process.kill(pid, 0);
|
|
@@ -21359,8 +21300,8 @@ async function defaultCountWorkingBoxes(registry, statusStore, idleGraceMs) {
|
|
|
21359
21300
|
return {
|
|
21360
21301
|
key: b.boxId,
|
|
21361
21302
|
agentState: active.state,
|
|
21362
|
-
sinceUpdateMs:
|
|
21363
|
-
sinceCreateMs:
|
|
21303
|
+
sinceUpdateMs: msSince(active.updatedAt),
|
|
21304
|
+
sinceCreateMs: msSince(b.createdAt)
|
|
21364
21305
|
};
|
|
21365
21306
|
});
|
|
21366
21307
|
const count2 = countWorkingSlots(entries, idleGraceMs);
|
|
@@ -21432,7 +21373,7 @@ async function uncachedBoxStateCount() {
|
|
|
21432
21373
|
}
|
|
21433
21374
|
function inspectDockerState(containerName) {
|
|
21434
21375
|
return new Promise((resolveP) => {
|
|
21435
|
-
const child = (0,
|
|
21376
|
+
const child = (0, import_node_child_process8.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
|
|
21436
21377
|
stdio: ["ignore", "pipe", "pipe"]
|
|
21437
21378
|
});
|
|
21438
21379
|
let out = "";
|
|
@@ -21466,9 +21407,9 @@ async function defaultSpawnWorker(job) {
|
|
|
21466
21407
|
`AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
|
|
21467
21408
|
);
|
|
21468
21409
|
}
|
|
21469
|
-
await (0,
|
|
21410
|
+
await (0, import_promises16.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
|
|
21470
21411
|
const fd = (0, import_node_fs6.openSync)(job.logPath, "a");
|
|
21471
|
-
const child = (0,
|
|
21412
|
+
const child = (0, import_node_child_process8.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
|
|
21472
21413
|
detached: true,
|
|
21473
21414
|
stdio: ["ignore", fd, fd],
|
|
21474
21415
|
env: process.env
|
|
@@ -21482,6 +21423,212 @@ async function defaultSpawnWorker(job) {
|
|
|
21482
21423
|
}
|
|
21483
21424
|
var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
|
|
21484
21425
|
|
|
21426
|
+
// src/autopause.ts
|
|
21427
|
+
function selectBoxesToPause(entries, cfg) {
|
|
21428
|
+
if (!cfg.enabled) return [];
|
|
21429
|
+
const runningCount = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
|
|
21430
|
+
const excess = runningCount - cfg.maxRunningBoxes;
|
|
21431
|
+
if (excess <= 0) return [];
|
|
21432
|
+
const idleThresholdMs = cfg.idleMinutes * 6e4;
|
|
21433
|
+
const candidates = entries.filter(
|
|
21434
|
+
(e) => e.running && e.claudeState === "idle" && e.idleMs != null && e.idleMs >= idleThresholdMs
|
|
21435
|
+
);
|
|
21436
|
+
candidates.sort(
|
|
21437
|
+
(a2, b) => b.idleMs - a2.idleMs || a2.createdAt - b.createdAt || (a2.boxId < b.boxId ? -1 : a2.boxId > b.boxId ? 1 : 0)
|
|
21438
|
+
);
|
|
21439
|
+
return candidates.slice(0, excess).map((e) => e.boxId);
|
|
21440
|
+
}
|
|
21441
|
+
async function loadAutopauseConfig() {
|
|
21442
|
+
const d = BUILT_IN_DEFAULTS.autopause;
|
|
21443
|
+
let global3 = {};
|
|
21444
|
+
try {
|
|
21445
|
+
global3 = parseUserConfig(await (0, import_promises18.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
|
|
21446
|
+
} catch {
|
|
21447
|
+
}
|
|
21448
|
+
const a2 = global3.autopause ?? {};
|
|
21449
|
+
return {
|
|
21450
|
+
enabled: a2.enabled ?? d.enabled,
|
|
21451
|
+
maxRunningBoxes: a2.maxRunningBoxes ?? d.maxRunningBoxes,
|
|
21452
|
+
idleMinutes: a2.idleMinutes ?? d.idleMinutes
|
|
21453
|
+
};
|
|
21454
|
+
}
|
|
21455
|
+
var DEFAULT_INTERVAL_MS2 = 6e4;
|
|
21456
|
+
function startAutopauseLoop(deps) {
|
|
21457
|
+
const loadConfig = deps.loadConfig ?? loadAutopauseConfig;
|
|
21458
|
+
const inspectStatus = deps.inspectStatus ?? inspectContainerState;
|
|
21459
|
+
const pause = deps.pause ?? pauseContainer;
|
|
21460
|
+
const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
|
|
21461
|
+
const { registry, statusStore, events, log } = deps;
|
|
21462
|
+
let ticking = false;
|
|
21463
|
+
let stopped = false;
|
|
21464
|
+
let inFlight = Promise.resolve();
|
|
21465
|
+
async function tick() {
|
|
21466
|
+
if (ticking) return;
|
|
21467
|
+
ticking = true;
|
|
21468
|
+
try {
|
|
21469
|
+
const cfg = await loadConfig();
|
|
21470
|
+
if (!cfg.enabled) return;
|
|
21471
|
+
const entries = [];
|
|
21472
|
+
for (const reg of registry.list()) {
|
|
21473
|
+
if (!reg.containerName) continue;
|
|
21474
|
+
const state = await inspectStatus(reg.containerName);
|
|
21475
|
+
const active = readPauseState(statusStore.get(reg.boxId));
|
|
21476
|
+
const idleMs = active.state === "idle" && active.updatedAt ? msSince2(active.updatedAt) : null;
|
|
21477
|
+
entries.push({
|
|
21478
|
+
boxId: reg.boxId,
|
|
21479
|
+
containerName: reg.containerName,
|
|
21480
|
+
running: state === "running",
|
|
21481
|
+
claudeState: active.state,
|
|
21482
|
+
idleMs,
|
|
21483
|
+
createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0
|
|
21484
|
+
});
|
|
21485
|
+
}
|
|
21486
|
+
const toPause = selectBoxesToPause(entries, cfg);
|
|
21487
|
+
if (toPause.length === 0) return;
|
|
21488
|
+
const byId = new Map(entries.map((e) => [e.boxId, e]));
|
|
21489
|
+
const runningBefore = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
|
|
21490
|
+
for (const boxId of toPause) {
|
|
21491
|
+
const e = byId.get(boxId);
|
|
21492
|
+
if (!e) continue;
|
|
21493
|
+
try {
|
|
21494
|
+
await pause(e.containerName);
|
|
21495
|
+
const mins = e.idleMs != null ? Math.round(e.idleMs / 6e4) : null;
|
|
21496
|
+
events.append({
|
|
21497
|
+
boxId,
|
|
21498
|
+
type: "autopause",
|
|
21499
|
+
payload: {
|
|
21500
|
+
containerName: e.containerName,
|
|
21501
|
+
action: "paused",
|
|
21502
|
+
idleMs: e.idleMs,
|
|
21503
|
+
runningBefore,
|
|
21504
|
+
max: cfg.maxRunningBoxes
|
|
21505
|
+
}
|
|
21506
|
+
});
|
|
21507
|
+
log(
|
|
21508
|
+
`autopause: paused box ${boxId} (${e.containerName})` + (mins != null ? ` after ~${String(mins)}m idle` : "") + `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`
|
|
21509
|
+
);
|
|
21510
|
+
} catch (err) {
|
|
21511
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
21512
|
+
log(`autopause: docker pause ${e.containerName} failed: ${msg}`);
|
|
21513
|
+
events.append({
|
|
21514
|
+
boxId,
|
|
21515
|
+
type: "autopause",
|
|
21516
|
+
payload: { containerName: e.containerName, action: "pause-failed", error: msg }
|
|
21517
|
+
});
|
|
21518
|
+
}
|
|
21519
|
+
}
|
|
21520
|
+
} catch (err) {
|
|
21521
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
21522
|
+
log(`autopause: tick error: ${msg}`);
|
|
21523
|
+
} finally {
|
|
21524
|
+
ticking = false;
|
|
21525
|
+
}
|
|
21526
|
+
}
|
|
21527
|
+
const timer = setInterval(() => {
|
|
21528
|
+
if (stopped) return;
|
|
21529
|
+
inFlight = tick();
|
|
21530
|
+
}, intervalMs);
|
|
21531
|
+
timer.unref();
|
|
21532
|
+
return {
|
|
21533
|
+
stop: async () => {
|
|
21534
|
+
stopped = true;
|
|
21535
|
+
clearInterval(timer);
|
|
21536
|
+
await inFlight.catch(() => {
|
|
21537
|
+
});
|
|
21538
|
+
}
|
|
21539
|
+
};
|
|
21540
|
+
}
|
|
21541
|
+
function readPauseState(snap) {
|
|
21542
|
+
const active = readActiveAgent(snap);
|
|
21543
|
+
return { state: coarsePauseState(active.state), updatedAt: active.updatedAt };
|
|
21544
|
+
}
|
|
21545
|
+
function coarsePauseState(s) {
|
|
21546
|
+
switch (s) {
|
|
21547
|
+
case "idle":
|
|
21548
|
+
return "idle";
|
|
21549
|
+
case "waiting":
|
|
21550
|
+
return "waiting";
|
|
21551
|
+
case "working":
|
|
21552
|
+
case "compacting":
|
|
21553
|
+
return "working";
|
|
21554
|
+
case null:
|
|
21555
|
+
return null;
|
|
21556
|
+
// end-plan / question / error / unknown: a live session expecting attention
|
|
21557
|
+
// — never auto-pause it (maps to a non-idle, non-candidate state).
|
|
21558
|
+
default:
|
|
21559
|
+
return "unknown";
|
|
21560
|
+
}
|
|
21561
|
+
}
|
|
21562
|
+
function msSince2(iso) {
|
|
21563
|
+
const t = Date.parse(iso);
|
|
21564
|
+
return Number.isNaN(t) ? null : Date.now() - t;
|
|
21565
|
+
}
|
|
21566
|
+
function toEpoch(iso) {
|
|
21567
|
+
const t = Date.parse(iso);
|
|
21568
|
+
return Number.isNaN(t) ? 0 : t;
|
|
21569
|
+
}
|
|
21570
|
+
var INSPECT_TIMEOUT_MS = 15e3;
|
|
21571
|
+
var PAUSE_TIMEOUT_MS = 3e4;
|
|
21572
|
+
function runDocker(args, timeoutMs) {
|
|
21573
|
+
return new Promise((resolve2) => {
|
|
21574
|
+
const child = (0, import_node_child_process9.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
21575
|
+
let stdout = "";
|
|
21576
|
+
let stderr = "";
|
|
21577
|
+
let settled = false;
|
|
21578
|
+
const finish = (exitCode) => {
|
|
21579
|
+
if (settled) return;
|
|
21580
|
+
settled = true;
|
|
21581
|
+
resolve2({ exitCode, stdout, stderr });
|
|
21582
|
+
};
|
|
21583
|
+
const timer = setTimeout(() => {
|
|
21584
|
+
child.kill("SIGTERM");
|
|
21585
|
+
stderr += `
|
|
21586
|
+
relay: docker ${args.join(" ")} timed out after ${String(timeoutMs)}ms
|
|
21587
|
+
`;
|
|
21588
|
+
finish(124);
|
|
21589
|
+
}, timeoutMs);
|
|
21590
|
+
child.stdout?.on("data", (c3) => {
|
|
21591
|
+
stdout += c3.toString("utf8");
|
|
21592
|
+
});
|
|
21593
|
+
child.stderr?.on("data", (c3) => {
|
|
21594
|
+
stderr += c3.toString("utf8");
|
|
21595
|
+
});
|
|
21596
|
+
child.on("error", (err) => {
|
|
21597
|
+
clearTimeout(timer);
|
|
21598
|
+
stderr += String(err.message ?? err);
|
|
21599
|
+
finish(127);
|
|
21600
|
+
});
|
|
21601
|
+
child.on("close", (code) => {
|
|
21602
|
+
clearTimeout(timer);
|
|
21603
|
+
finish(code ?? -1);
|
|
21604
|
+
});
|
|
21605
|
+
});
|
|
21606
|
+
}
|
|
21607
|
+
async function inspectContainerState(name) {
|
|
21608
|
+
const r = await runDocker(["inspect", "--format", "{{.State.Status}}", name], INSPECT_TIMEOUT_MS);
|
|
21609
|
+
if (r.exitCode !== 0) return "missing";
|
|
21610
|
+
switch (r.stdout.trim()) {
|
|
21611
|
+
case "running":
|
|
21612
|
+
return "running";
|
|
21613
|
+
case "paused":
|
|
21614
|
+
return "paused";
|
|
21615
|
+
case "created":
|
|
21616
|
+
case "exited":
|
|
21617
|
+
case "dead":
|
|
21618
|
+
case "restarting":
|
|
21619
|
+
case "removing":
|
|
21620
|
+
return "stopped";
|
|
21621
|
+
default:
|
|
21622
|
+
return "missing";
|
|
21623
|
+
}
|
|
21624
|
+
}
|
|
21625
|
+
async function pauseContainer(name) {
|
|
21626
|
+
const r = await runDocker(["pause", name], PAUSE_TIMEOUT_MS);
|
|
21627
|
+
if (r.exitCode !== 0) {
|
|
21628
|
+
throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);
|
|
21629
|
+
}
|
|
21630
|
+
}
|
|
21631
|
+
|
|
21485
21632
|
// src/bin.ts
|
|
21486
21633
|
var program2 = new Command();
|
|
21487
21634
|
program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");
|