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