@madarco/agentbox 0.7.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-ZXBCNWJX.js +13 -0
- package/dist/{chunk-NW5NYTQM.js → chunk-BXQMIEHC.js} +459 -110
- package/dist/chunk-BXQMIEHC.js.map +1 -0
- package/dist/{chunk-UK72UQ5U.js → chunk-G3H2L3O2.js} +55 -4
- package/dist/chunk-G3H2L3O2.js.map +1 -0
- package/dist/{chunk-7KOEFGN2.js → chunk-GU5LW4B5.js} +385 -31
- package/dist/chunk-GU5LW4B5.js.map +1 -0
- package/dist/chunk-KL36BRN4.js +455 -0
- package/dist/chunk-KL36BRN4.js.map +1 -0
- package/dist/{chunk-V5KZGB5V.js → chunk-LEV3KICD.js} +18 -2
- package/dist/chunk-LEV3KICD.js.map +1 -0
- package/dist/chunk-MTVI44DW.js +662 -0
- package/dist/chunk-MTVI44DW.js.map +1 -0
- package/dist/{chunk-NAVL4R34.js → chunk-NCJP5MTN.js} +1281 -556
- package/dist/chunk-NCJP5MTN.js.map +1 -0
- package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js → cloud-poller-SUNA6ZQC-2RG5WPRN.js} +2 -2
- package/dist/{dist-ETCFRVPA.js → dist-32EZBYG4.js} +50 -20
- package/dist/{dist-R67WMLCF.js → dist-CX5CGVEB.js} +120 -10
- package/dist/dist-CX5CGVEB.js.map +1 -0
- package/dist/{dist-QZGJIBT5.js → dist-GDHP34ZK.js} +141 -75
- 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 +3881 -867
- package/dist/index.js.map +1 -1
- package/dist/prepared-state-CL4CWXQA-H5THETIM.js +18 -0
- package/dist/prepared-state-CL4CWXQA-H5THETIM.js.map +1 -0
- package/package.json +7 -5
- package/runtime/daytona/custom-system-CLAUDE.md +39 -0
- package/runtime/docker/Dockerfile.box +22 -0
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +1 -1
- package/runtime/docker/packages/ctl/dist/bin.cjs +1214 -98
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +66 -35
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
- package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
- package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
- package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
- package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
- package/runtime/hetzner/agentbox-codex-hooks.json +66 -35
- package/runtime/hetzner/agentbox-setup-skill.md +1 -1
- package/runtime/hetzner/agentbox-vnc-start +15 -1
- package/runtime/hetzner/claude-managed-settings.json +62 -1
- package/runtime/hetzner/ctl.cjs +1214 -98
- package/runtime/hetzner/custom-system-CLAUDE.md +26 -14
- package/runtime/hetzner/gh-shim +263 -0
- package/runtime/hetzner/git-shim +131 -0
- package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
- package/runtime/hetzner/scripts/install-box.sh +11 -2
- package/runtime/relay/bin.cjs +1146 -63
- 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/share/agentbox-setup/SKILL.md +1 -1
- package/share/host-skills/agentbox/SKILL.md +29 -0
- package/share/host-skills/agentbox-info/SKILL.md +211 -0
- package/share/host-skills/codex/agentbox.md +35 -0
- package/share/host-skills/opencode/agentbox.md +26 -0
- package/dist/_cloud-attach-DMVH6GWO.js +0 -12
- package/dist/chunk-7KOEFGN2.js.map +0 -1
- package/dist/chunk-NAVL4R34.js.map +0 -1
- package/dist/chunk-NW5NYTQM.js.map +0 -1
- package/dist/chunk-UK72UQ5U.js.map +0 -1
- package/dist/chunk-V5KZGB5V.js.map +0 -1
- package/dist/dist-QZGJIBT5.js.map +0 -1
- package/dist/dist-R67WMLCF.js.map +0 -1
- /package/dist/{_cloud-attach-DMVH6GWO.js.map → _cloud-attach-ZXBCNWJX.js.map} +0 -0
- /package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
- /package/dist/{dist-ETCFRVPA.js.map → dist-32EZBYG4.js.map} +0 -0
package/runtime/relay/bin.cjs
CHANGED
|
@@ -3514,7 +3514,7 @@ var require_cross_spawn = __commonJS({
|
|
|
3514
3514
|
var cp = require("child_process");
|
|
3515
3515
|
var parse = require_parse();
|
|
3516
3516
|
var enoent = require_enoent();
|
|
3517
|
-
function
|
|
3517
|
+
function spawn6(command, args, options) {
|
|
3518
3518
|
const parsed = parse(command, args, options);
|
|
3519
3519
|
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
3520
3520
|
enoent.hookChildProcess(spawned, parsed);
|
|
@@ -3526,8 +3526,8 @@ var require_cross_spawn = __commonJS({
|
|
|
3526
3526
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
3527
3527
|
return result;
|
|
3528
3528
|
}
|
|
3529
|
-
module2.exports =
|
|
3530
|
-
module2.exports.spawn =
|
|
3529
|
+
module2.exports = spawn6;
|
|
3530
|
+
module2.exports.spawn = spawn6;
|
|
3531
3531
|
module2.exports.sync = spawnSync2;
|
|
3532
3532
|
module2.exports._parse = parse;
|
|
3533
3533
|
module2.exports._enoent = enoent;
|
|
@@ -3540,7 +3540,13 @@ __export(cloud_poller_exports, {
|
|
|
3540
3540
|
CloudBoxPoller: () => CloudBoxPoller,
|
|
3541
3541
|
CloudBoxPollers: () => CloudBoxPollers
|
|
3542
3542
|
});
|
|
3543
|
-
|
|
3543
|
+
function isConnectionLevelError(err) {
|
|
3544
|
+
const code = err?.code;
|
|
3545
|
+
if (code && CONNECTION_LEVEL_CODES.has(code)) return true;
|
|
3546
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3547
|
+
return /\b(ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ECONNRESET|EPIPE)\b/.test(msg);
|
|
3548
|
+
}
|
|
3549
|
+
var import_node_http, import_node_https, import_promises15, BACKOFF_BASE_MS, BACKOFF_MAX_MS, REQUEST_TIMEOUT_MS, FAST_REQUEST_TIMEOUT_MS, FAST_MODE_DECAY_POLLS, STOPPED_TICK_MS, CONNECTION_LEVEL_CODES, CloudBoxPoller, CloudBoxPollers;
|
|
3544
3550
|
var init_cloud_poller = __esm({
|
|
3545
3551
|
"src/cloud-poller.ts"() {
|
|
3546
3552
|
"use strict";
|
|
@@ -3553,9 +3559,17 @@ var init_cloud_poller = __esm({
|
|
|
3553
3559
|
FAST_REQUEST_TIMEOUT_MS = 8e3;
|
|
3554
3560
|
FAST_MODE_DECAY_POLLS = 5;
|
|
3555
3561
|
STOPPED_TICK_MS = 250;
|
|
3562
|
+
CONNECTION_LEVEL_CODES = /* @__PURE__ */ new Set([
|
|
3563
|
+
"ECONNREFUSED",
|
|
3564
|
+
"ENOTFOUND",
|
|
3565
|
+
"EHOSTUNREACH",
|
|
3566
|
+
"ECONNRESET",
|
|
3567
|
+
"EPIPE"
|
|
3568
|
+
]);
|
|
3556
3569
|
CloudBoxPoller = class {
|
|
3557
3570
|
constructor(deps) {
|
|
3558
3571
|
this.deps = deps;
|
|
3572
|
+
this.currentPreviewUrl = deps.previewUrl;
|
|
3559
3573
|
}
|
|
3560
3574
|
deps;
|
|
3561
3575
|
stopped = false;
|
|
@@ -3569,6 +3583,15 @@ var init_cloud_poller = __esm({
|
|
|
3569
3583
|
* within ~5 successful round-trips.
|
|
3570
3584
|
*/
|
|
3571
3585
|
fastModePolls = 0;
|
|
3586
|
+
/**
|
|
3587
|
+
* Mutable copy of `deps.previewUrl`. We don't read `deps.previewUrl`
|
|
3588
|
+
* directly after construction because `recoverPreviewUrl` may hand us a
|
|
3589
|
+
* new URL (e.g. Hetzner reopens its SSH ControlMaster and the `-L`
|
|
3590
|
+
* forward gets a new ephemeral local port).
|
|
3591
|
+
*/
|
|
3592
|
+
currentPreviewUrl;
|
|
3593
|
+
/** Guards against recovery storms — at most one recovery attempt in flight. */
|
|
3594
|
+
recovering = null;
|
|
3572
3595
|
start() {
|
|
3573
3596
|
if (this.loopPromise) return;
|
|
3574
3597
|
this.loopPromise = this.run().catch((err) => {
|
|
@@ -3585,7 +3608,7 @@ var init_cloud_poller = __esm({
|
|
|
3585
3608
|
* and the in-box agent finally sees the answer.
|
|
3586
3609
|
*/
|
|
3587
3610
|
async respond(actionId, result) {
|
|
3588
|
-
const base = this.
|
|
3611
|
+
const base = this.currentPreviewUrl.replace(/\/+$/, "");
|
|
3589
3612
|
const url = new URL(`${base}/bridge/action-result`);
|
|
3590
3613
|
const isHttps = url.protocol === "https:";
|
|
3591
3614
|
const transport = isHttps ? import_node_https.request : import_node_http.request;
|
|
@@ -3675,6 +3698,9 @@ var init_cloud_poller = __esm({
|
|
|
3675
3698
|
this.fastModePolls = FAST_MODE_DECAY_POLLS;
|
|
3676
3699
|
}
|
|
3677
3700
|
this.log(`poll error: ${msg}`);
|
|
3701
|
+
if (this.deps.recoverPreviewUrl && isConnectionLevelError(err)) {
|
|
3702
|
+
await this.tryRecoverPreviewUrl();
|
|
3703
|
+
}
|
|
3678
3704
|
await this.backoff();
|
|
3679
3705
|
}
|
|
3680
3706
|
if (this.currentBackoffMs === 0 && this.fastModePolls > 0) {
|
|
@@ -3687,8 +3713,33 @@ var init_cloud_poller = __esm({
|
|
|
3687
3713
|
this.currentBackoffMs = this.currentBackoffMs === 0 ? BACKOFF_BASE_MS : Math.min(this.currentBackoffMs * 2, BACKOFF_MAX_MS);
|
|
3688
3714
|
await (0, import_promises15.setTimeout)(this.currentBackoffMs);
|
|
3689
3715
|
}
|
|
3716
|
+
async tryRecoverPreviewUrl() {
|
|
3717
|
+
if (!this.deps.recoverPreviewUrl) return;
|
|
3718
|
+
if (this.recovering) {
|
|
3719
|
+
await this.recovering;
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
this.recovering = (async () => {
|
|
3723
|
+
try {
|
|
3724
|
+
const next = await this.deps.recoverPreviewUrl();
|
|
3725
|
+
if (typeof next === "string" && next.length > 0 && next !== this.currentPreviewUrl) {
|
|
3726
|
+
this.log(`preview URL recovered: ${this.currentPreviewUrl} \u2192 ${next}`);
|
|
3727
|
+
this.currentPreviewUrl = next;
|
|
3728
|
+
this.currentBackoffMs = 0;
|
|
3729
|
+
} else if (typeof next === "string" && next === this.currentPreviewUrl) {
|
|
3730
|
+
this.log("preview URL recovered (unchanged)");
|
|
3731
|
+
this.currentBackoffMs = 0;
|
|
3732
|
+
}
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
this.log(`preview URL recover failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3735
|
+
} finally {
|
|
3736
|
+
this.recovering = null;
|
|
3737
|
+
}
|
|
3738
|
+
})();
|
|
3739
|
+
await this.recovering;
|
|
3740
|
+
}
|
|
3690
3741
|
async pollOnce() {
|
|
3691
|
-
const base = this.
|
|
3742
|
+
const base = this.currentPreviewUrl.replace(/\/+$/, "");
|
|
3692
3743
|
const url = new URL(`${base}/bridge/poll?since=${String(this.cursor)}`);
|
|
3693
3744
|
const isHttps = url.protocol === "https:";
|
|
3694
3745
|
const transport = isHttps ? import_node_https.request : import_node_http.request;
|
|
@@ -11124,10 +11175,11 @@ var import_node_http3 = require("http");
|
|
|
11124
11175
|
|
|
11125
11176
|
// src/types.ts
|
|
11126
11177
|
var DEFAULT_RELAY_PORT = 8787;
|
|
11178
|
+
var DEFAULT_BOX_RELAY_PORT = 8788;
|
|
11127
11179
|
var RELAY_EVENT_RING_SIZE = 1e3;
|
|
11128
11180
|
|
|
11129
11181
|
// src/server.ts
|
|
11130
|
-
var
|
|
11182
|
+
var import_node_child_process7 = require("child_process");
|
|
11131
11183
|
var import_node_http2 = require("http");
|
|
11132
11184
|
|
|
11133
11185
|
// ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
|
|
@@ -17953,6 +18005,8 @@ function projectDockerFields(box) {
|
|
|
17953
18005
|
webHostPort: box.webHostPort,
|
|
17954
18006
|
portlessAlias: box.portlessAlias,
|
|
17955
18007
|
portlessUrl: box.portlessUrl,
|
|
18008
|
+
portlessVncAlias: box.portlessVncAlias,
|
|
18009
|
+
portlessVncUrl: box.portlessVncUrl,
|
|
17956
18010
|
dockerVolume: box.dockerVolume,
|
|
17957
18011
|
dockerCacheShared: box.dockerCacheShared,
|
|
17958
18012
|
checkpointImage: box.checkpointImage
|
|
@@ -17973,8 +18027,262 @@ function findBox(idOrName, state) {
|
|
|
17973
18027
|
return { kind: "none" };
|
|
17974
18028
|
}
|
|
17975
18029
|
|
|
17976
|
-
// src/
|
|
18030
|
+
// src/gh.ts
|
|
18031
|
+
var import_node_child_process6 = require("child_process");
|
|
18032
|
+
var GH_PR_OPS = [
|
|
18033
|
+
"create",
|
|
18034
|
+
"view",
|
|
18035
|
+
"list",
|
|
18036
|
+
"comment",
|
|
18037
|
+
"review",
|
|
18038
|
+
"merge",
|
|
18039
|
+
"checkout",
|
|
18040
|
+
"close",
|
|
18041
|
+
"reopen"
|
|
18042
|
+
];
|
|
18043
|
+
function isGhPrOp(value) {
|
|
18044
|
+
return GH_PR_OPS.includes(value);
|
|
18045
|
+
}
|
|
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
|
+
};
|
|
18064
|
+
var GH_RPC_TIMEOUT_MS = 12e4;
|
|
18065
|
+
var GH_READY_CACHE_TTL_MS = 6e4;
|
|
18066
|
+
var ghReadyCache;
|
|
18067
|
+
async function assertGhReady() {
|
|
18068
|
+
const now = Date.now();
|
|
18069
|
+
if (ghReadyCache && ghReadyCache.expiresAt > now) {
|
|
18070
|
+
return ghReadyCache.result;
|
|
18071
|
+
}
|
|
18072
|
+
const result = await probeGh();
|
|
18073
|
+
ghReadyCache = { result, expiresAt: now + GH_READY_CACHE_TTL_MS };
|
|
18074
|
+
return result;
|
|
18075
|
+
}
|
|
18076
|
+
async function probeGh() {
|
|
18077
|
+
const version = await runHostGh(["--version"], process.cwd(), 1e4);
|
|
18078
|
+
if (version.exitCode === 127 || /ENOENT/.test(version.stderr)) {
|
|
18079
|
+
return {
|
|
18080
|
+
exitCode: 127,
|
|
18081
|
+
stdout: "",
|
|
18082
|
+
stderr: "gh not installed on host (https://cli.github.com)\n"
|
|
18083
|
+
};
|
|
18084
|
+
}
|
|
18085
|
+
if (version.exitCode !== 0) {
|
|
18086
|
+
return {
|
|
18087
|
+
exitCode: version.exitCode,
|
|
18088
|
+
stdout: "",
|
|
18089
|
+
stderr: `gh --version failed: ${version.stderr || version.stdout}`.trimEnd() + "\n"
|
|
18090
|
+
};
|
|
18091
|
+
}
|
|
18092
|
+
const auth = await runHostGh(["auth", "status"], process.cwd(), 15e3);
|
|
18093
|
+
if (auth.exitCode !== 0) {
|
|
18094
|
+
return {
|
|
18095
|
+
exitCode: 4,
|
|
18096
|
+
stdout: "",
|
|
18097
|
+
stderr: "gh not authenticated on host (run `gh auth login`)\n"
|
|
18098
|
+
};
|
|
18099
|
+
}
|
|
18100
|
+
return null;
|
|
18101
|
+
}
|
|
18102
|
+
function runHostGh(args, cwd, timeoutMs = GH_RPC_TIMEOUT_MS) {
|
|
18103
|
+
return new Promise((resolve2) => {
|
|
18104
|
+
const child = (0, import_node_child_process6.spawn)("gh", args, {
|
|
18105
|
+
cwd,
|
|
18106
|
+
env: process.env,
|
|
18107
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
18108
|
+
});
|
|
18109
|
+
let stdout = "";
|
|
18110
|
+
let stderr = "";
|
|
18111
|
+
let settled = false;
|
|
18112
|
+
const finish = (exitCode) => {
|
|
18113
|
+
if (settled) return;
|
|
18114
|
+
settled = true;
|
|
18115
|
+
resolve2({ exitCode, stdout, stderr });
|
|
18116
|
+
};
|
|
18117
|
+
const timer = setTimeout(() => {
|
|
18118
|
+
child.kill("SIGTERM");
|
|
18119
|
+
stderr += `
|
|
18120
|
+
relay: gh command timed out after ${String(timeoutMs)}ms
|
|
18121
|
+
`;
|
|
18122
|
+
finish(124);
|
|
18123
|
+
}, timeoutMs);
|
|
18124
|
+
child.stdout?.on("data", (chunk) => {
|
|
18125
|
+
stdout += chunk.toString("utf8");
|
|
18126
|
+
});
|
|
18127
|
+
child.stderr?.on("data", (chunk) => {
|
|
18128
|
+
stderr += chunk.toString("utf8");
|
|
18129
|
+
});
|
|
18130
|
+
child.on("error", (err) => {
|
|
18131
|
+
clearTimeout(timer);
|
|
18132
|
+
const code = err.code;
|
|
18133
|
+
stderr += String(err.message ?? err);
|
|
18134
|
+
finish(code === "ENOENT" ? 127 : 1);
|
|
18135
|
+
});
|
|
18136
|
+
child.on("close", (code) => {
|
|
18137
|
+
clearTimeout(timer);
|
|
18138
|
+
finish(code ?? -1);
|
|
18139
|
+
});
|
|
18140
|
+
});
|
|
18141
|
+
}
|
|
18142
|
+
async function checkoutGuards(hostMainRepo, registeredBranches) {
|
|
18143
|
+
const status = await runGitProbe(["-C", hostMainRepo, "status", "--porcelain"]);
|
|
18144
|
+
if (status.exitCode !== 0) {
|
|
18145
|
+
return {
|
|
18146
|
+
exitCode: status.exitCode,
|
|
18147
|
+
stdout: "",
|
|
18148
|
+
stderr: `gh pr checkout: failed to inspect host repo: ${status.stderr || status.stdout}`.trimEnd() + "\n"
|
|
18149
|
+
};
|
|
18150
|
+
}
|
|
18151
|
+
if (status.stdout.trim().length > 0) {
|
|
18152
|
+
return {
|
|
18153
|
+
exitCode: 12,
|
|
18154
|
+
stdout: "",
|
|
18155
|
+
stderr: `gh pr checkout: ${hostMainRepo} has uncommitted changes; refusing to switch branches
|
|
18156
|
+
`
|
|
18157
|
+
};
|
|
18158
|
+
}
|
|
18159
|
+
const head = await runGitProbe(["-C", hostMainRepo, "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
18160
|
+
if (head.exitCode !== 0) {
|
|
18161
|
+
return {
|
|
18162
|
+
exitCode: head.exitCode,
|
|
18163
|
+
stdout: "",
|
|
18164
|
+
stderr: `gh pr checkout: failed to resolve HEAD: ${head.stderr || head.stdout}`.trimEnd() + "\n"
|
|
18165
|
+
};
|
|
18166
|
+
}
|
|
18167
|
+
const currentBranch = head.stdout.trim();
|
|
18168
|
+
if (registeredBranches.includes(currentBranch)) {
|
|
18169
|
+
return {
|
|
18170
|
+
exitCode: 12,
|
|
18171
|
+
stdout: "",
|
|
18172
|
+
stderr: `gh pr checkout: ${hostMainRepo} is on registered box branch ${currentBranch}; refusing (would corrupt the bind-mounted box HEAD)
|
|
18173
|
+
`
|
|
18174
|
+
};
|
|
18175
|
+
}
|
|
18176
|
+
return null;
|
|
18177
|
+
}
|
|
18178
|
+
function runGitProbe(args) {
|
|
18179
|
+
return new Promise((resolve2) => {
|
|
18180
|
+
const child = (0, import_node_child_process6.spawn)("git", args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
18181
|
+
let stdout = "";
|
|
18182
|
+
let stderr = "";
|
|
18183
|
+
child.stdout?.on("data", (c3) => {
|
|
18184
|
+
stdout += c3.toString("utf8");
|
|
18185
|
+
});
|
|
18186
|
+
child.stderr?.on("data", (c3) => {
|
|
18187
|
+
stderr += c3.toString("utf8");
|
|
18188
|
+
});
|
|
18189
|
+
child.on("error", (err) => {
|
|
18190
|
+
resolve2({ exitCode: 127, stdout, stderr: stderr + String(err.message ?? err) });
|
|
18191
|
+
});
|
|
18192
|
+
child.on("close", (code) => {
|
|
18193
|
+
resolve2({ exitCode: code ?? -1, stdout, stderr });
|
|
18194
|
+
});
|
|
18195
|
+
});
|
|
18196
|
+
}
|
|
18197
|
+
function refuseMergeBypass(op) {
|
|
18198
|
+
if (op !== "merge") return null;
|
|
18199
|
+
if (process.env["AGENTBOX_PROMPT"] !== "off") return null;
|
|
18200
|
+
if (process.env["AGENTBOX_GH_FORCE"] === "1") return null;
|
|
18201
|
+
return {
|
|
18202
|
+
exitCode: 10,
|
|
18203
|
+
stdout: "",
|
|
18204
|
+
stderr: "gh pr merge: AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1 (merge is irreversible)\n"
|
|
18205
|
+
};
|
|
18206
|
+
}
|
|
18207
|
+
function refuseCheckoutByDefault(op) {
|
|
18208
|
+
if (op !== "checkout") return null;
|
|
18209
|
+
if (process.env["AGENTBOX_GH_PR_CHECKOUT"] === "allow") return null;
|
|
18210
|
+
return {
|
|
18211
|
+
exitCode: 13,
|
|
18212
|
+
stdout: "",
|
|
18213
|
+
stderr: "gh pr checkout: disabled by default; set AGENTBOX_GH_PR_CHECKOUT=allow to enable\n"
|
|
18214
|
+
};
|
|
18215
|
+
}
|
|
18216
|
+
|
|
18217
|
+
// src/host-initiated.ts
|
|
17977
18218
|
var import_node_crypto = require("crypto");
|
|
18219
|
+
var DEFAULT_TTL_MS = 12e4;
|
|
18220
|
+
function hashRpcParams(params) {
|
|
18221
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonicalJson(params)).digest("hex");
|
|
18222
|
+
}
|
|
18223
|
+
function canonicalJson(v) {
|
|
18224
|
+
if (v === null) return "null";
|
|
18225
|
+
if (typeof v === "undefined") return "null";
|
|
18226
|
+
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
|
|
18227
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
18228
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
18229
|
+
if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
|
|
18230
|
+
if (typeof v === "object") {
|
|
18231
|
+
const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a2], [b]) => a2 < b ? -1 : a2 > b ? 1 : 0);
|
|
18232
|
+
return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
|
|
18233
|
+
}
|
|
18234
|
+
return "null";
|
|
18235
|
+
}
|
|
18236
|
+
var HostInitiatedTokens = class {
|
|
18237
|
+
store = /* @__PURE__ */ new Map();
|
|
18238
|
+
/**
|
|
18239
|
+
* Mint a fresh one-time token scoped to (boxId, method, paramsHash).
|
|
18240
|
+
* `paramsHash` MUST be supplied for any call surface where the box can
|
|
18241
|
+
* influence the eventual RPC params. Pass `null` only when there are no
|
|
18242
|
+
* params (no current call sites use this).
|
|
18243
|
+
*/
|
|
18244
|
+
mint(boxId, method, paramsHash, ttlMs = DEFAULT_TTL_MS) {
|
|
18245
|
+
const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
|
|
18246
|
+
this.store.set(token, { boxId, method, paramsHash, expiresAt: Date.now() + ttlMs });
|
|
18247
|
+
return token;
|
|
18248
|
+
}
|
|
18249
|
+
/**
|
|
18250
|
+
* Returns true exactly once if `token` is a valid, unexpired token for the
|
|
18251
|
+
* given `(boxId, method)` AND the supplied `incomingParamsHash` matches
|
|
18252
|
+
* the hash bound at mint time. The token is removed on a successful match
|
|
18253
|
+
* (one-shot semantics). All failure modes return false — callers fall back
|
|
18254
|
+
* to the normal prompt path.
|
|
18255
|
+
*/
|
|
18256
|
+
consume(token, boxId, method, incomingParamsHash) {
|
|
18257
|
+
if (!token || typeof token !== "string") return false;
|
|
18258
|
+
const record = this.store.get(token);
|
|
18259
|
+
if (!record) return false;
|
|
18260
|
+
if (record.expiresAt < Date.now()) {
|
|
18261
|
+
this.store.delete(token);
|
|
18262
|
+
return false;
|
|
18263
|
+
}
|
|
18264
|
+
if (record.boxId !== boxId || record.method !== method) return false;
|
|
18265
|
+
if (record.paramsHash !== null && record.paramsHash !== incomingParamsHash) {
|
|
18266
|
+
return false;
|
|
18267
|
+
}
|
|
18268
|
+
this.store.delete(token);
|
|
18269
|
+
return true;
|
|
18270
|
+
}
|
|
18271
|
+
/** Drop expired entries. Cheap; safe to call periodically. */
|
|
18272
|
+
gc() {
|
|
18273
|
+
const now = Date.now();
|
|
18274
|
+
for (const [token, record] of this.store) {
|
|
18275
|
+
if (record.expiresAt < now) this.store.delete(token);
|
|
18276
|
+
}
|
|
18277
|
+
}
|
|
18278
|
+
/** Test-only: number of live tokens. */
|
|
18279
|
+
size() {
|
|
18280
|
+
return this.store.size;
|
|
18281
|
+
}
|
|
18282
|
+
};
|
|
18283
|
+
|
|
18284
|
+
// src/prompts.ts
|
|
18285
|
+
var import_node_crypto2 = require("crypto");
|
|
17978
18286
|
var PendingPrompts = class {
|
|
17979
18287
|
entries = /* @__PURE__ */ new Map();
|
|
17980
18288
|
add(boxId, ev) {
|
|
@@ -18063,7 +18371,7 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
|
|
|
18063
18371
|
if (process.env.AGENTBOX_PROMPT === "off") {
|
|
18064
18372
|
return { answer: "y" };
|
|
18065
18373
|
}
|
|
18066
|
-
const ev = { id: (0,
|
|
18374
|
+
const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
|
|
18067
18375
|
const promise = prompts.add(boxId, ev);
|
|
18068
18376
|
subscribers.broadcast(boxId, "prompt-ask", ev);
|
|
18069
18377
|
if (opts?.ttlMs !== void 0 && opts.ttlMs > 0) {
|
|
@@ -18092,36 +18400,43 @@ function isPromptAnswerBody(v) {
|
|
|
18092
18400
|
async function resolveCloudBackend(name) {
|
|
18093
18401
|
if (name === "daytona") {
|
|
18094
18402
|
const pkg = "@agentbox/sandbox-daytona";
|
|
18095
|
-
|
|
18096
|
-
const mod = await import(pkg);
|
|
18097
|
-
return mod.daytonaBackend;
|
|
18098
|
-
} catch (err) {
|
|
18099
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
18100
|
-
if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
|
|
18101
|
-
throw new Error(
|
|
18102
|
-
`relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
|
|
18103
|
-
);
|
|
18104
|
-
}
|
|
18105
|
-
throw err;
|
|
18106
|
-
}
|
|
18403
|
+
return loadCloudBackend(pkg, async () => (await import(pkg)).daytonaBackend);
|
|
18107
18404
|
}
|
|
18108
18405
|
if (name === "hetzner") {
|
|
18109
18406
|
const pkg = "@agentbox/sandbox-hetzner";
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
18113
|
-
|
|
18114
|
-
|
|
18115
|
-
if (/cannot find module|MODULE_NOT_FOUND/i.test(msg)) {
|
|
18116
|
-
throw new Error(
|
|
18117
|
-
`relay: cannot load '${pkg}' at runtime \u2014 install it alongside @agentbox/relay (the @madarco/agentbox CLI normally provides this dependency). Original: ${msg}`
|
|
18118
|
-
);
|
|
18119
|
-
}
|
|
18120
|
-
throw err;
|
|
18121
|
-
}
|
|
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);
|
|
18122
18412
|
}
|
|
18123
18413
|
throw new Error(`no host executor for cloud backend '${name}'`);
|
|
18124
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
|
+
}
|
|
18428
|
+
async function refreshCloudPreviewUrl(backendName, boxId, port) {
|
|
18429
|
+
try {
|
|
18430
|
+
const backend = await resolveCloudBackend(backendName);
|
|
18431
|
+
if (!backend.refreshPreviewUrl) return null;
|
|
18432
|
+
const lookup = await lookupCloudBox(boxId);
|
|
18433
|
+
const handle = { sandboxId: lookup.cloudSandboxId };
|
|
18434
|
+
const url = await backend.refreshPreviewUrl(handle, port);
|
|
18435
|
+
return url.url;
|
|
18436
|
+
} catch {
|
|
18437
|
+
return null;
|
|
18438
|
+
}
|
|
18439
|
+
}
|
|
18125
18440
|
async function executeCloudAction(action, deps) {
|
|
18126
18441
|
const log = deps.log ?? (() => {
|
|
18127
18442
|
});
|
|
@@ -18141,6 +18456,17 @@ async function executeCloudAction(action, deps) {
|
|
|
18141
18456
|
if (action.method === "browser.open.mirror") {
|
|
18142
18457
|
return runBrowserOpenMirror(action, deps);
|
|
18143
18458
|
}
|
|
18459
|
+
if (action.method.startsWith("gh.pr.")) {
|
|
18460
|
+
return runGhPrRpc(action, deps);
|
|
18461
|
+
}
|
|
18462
|
+
if (action.method === "git.clone" || action.method === "gh.repo.clone") {
|
|
18463
|
+
return {
|
|
18464
|
+
exitCode: 64,
|
|
18465
|
+
stdout: "",
|
|
18466
|
+
stderr: `${action.method}: not yet implemented (deferred; see docs/plans/gh-and-git-shims-host-only.md). Run \`gh\` / \`git\` on the host directly for now.
|
|
18467
|
+
`
|
|
18468
|
+
};
|
|
18469
|
+
}
|
|
18144
18470
|
return {
|
|
18145
18471
|
exitCode: 1,
|
|
18146
18472
|
stdout: "",
|
|
@@ -18148,6 +18474,99 @@ async function executeCloudAction(action, deps) {
|
|
|
18148
18474
|
`
|
|
18149
18475
|
};
|
|
18150
18476
|
}
|
|
18477
|
+
async function runGhPrRpc(action, deps) {
|
|
18478
|
+
const op = action.method.slice("gh.pr.".length);
|
|
18479
|
+
if (!isGhPrOp(op)) {
|
|
18480
|
+
return {
|
|
18481
|
+
exitCode: 64,
|
|
18482
|
+
stdout: "",
|
|
18483
|
+
stderr: `unknown gh.pr.* op: ${op}
|
|
18484
|
+
`
|
|
18485
|
+
};
|
|
18486
|
+
}
|
|
18487
|
+
const mergeBypass = refuseMergeBypass(op);
|
|
18488
|
+
if (mergeBypass) return mergeBypass;
|
|
18489
|
+
const checkoutOptIn = refuseCheckoutByDefault(op);
|
|
18490
|
+
if (checkoutOptIn) return checkoutOptIn;
|
|
18491
|
+
const params = action.params ?? {};
|
|
18492
|
+
const args = Array.isArray(params.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
|
|
18493
|
+
const ghReady = await assertGhReady();
|
|
18494
|
+
if (ghReady) return ghReady;
|
|
18495
|
+
const lookup = await lookupCloudBox(deps.boxId);
|
|
18496
|
+
if (op === "checkout") {
|
|
18497
|
+
const guard = await checkoutGuards(lookup.workspacePath, []);
|
|
18498
|
+
if (guard) return guard;
|
|
18499
|
+
}
|
|
18500
|
+
const tokenClaimedGhCloud = typeof params.hostInitiated === "string";
|
|
18501
|
+
const incomingHashGhCloud = hashRpcParams(params);
|
|
18502
|
+
const hostInitiatedGhOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && (deps.hostInitiatedTokens?.consume(
|
|
18503
|
+
params.hostInitiated,
|
|
18504
|
+
deps.boxId,
|
|
18505
|
+
`gh.pr.${op}`,
|
|
18506
|
+
incomingHashGhCloud
|
|
18507
|
+
) ?? false);
|
|
18508
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && !hostInitiatedGhOk) {
|
|
18509
|
+
return {
|
|
18510
|
+
exitCode: 10,
|
|
18511
|
+
stdout: "",
|
|
18512
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
18513
|
+
};
|
|
18514
|
+
}
|
|
18515
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedGhOk && deps.prompts && deps.subscribers) {
|
|
18516
|
+
const detail = args.join(" ").slice(0, 200);
|
|
18517
|
+
const ctx = {
|
|
18518
|
+
kind: "confirm",
|
|
18519
|
+
message: `Allow gh pr ${op} from cloud box ${deps.boxName ?? deps.boxId}?`,
|
|
18520
|
+
detail,
|
|
18521
|
+
defaultAnswer: "n",
|
|
18522
|
+
context: {
|
|
18523
|
+
command: `gh pr ${op}`,
|
|
18524
|
+
cwd: params.path,
|
|
18525
|
+
argv: args
|
|
18526
|
+
}
|
|
18527
|
+
};
|
|
18528
|
+
const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
|
|
18529
|
+
if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
|
|
18530
|
+
const noSubMode = (process.env["AGENTBOX_GH_NO_SUB"] ?? "deny").toLowerCase();
|
|
18531
|
+
if (noSubMode === "deny") {
|
|
18532
|
+
return {
|
|
18533
|
+
exitCode: 10,
|
|
18534
|
+
stdout: "",
|
|
18535
|
+
stderr: "denied automatically \u2014 no attached wrapper to confirm. Attach `agentbox claude` (or similar) and retry, or set AGENTBOX_GH_NO_SUB=allow.\n"
|
|
18536
|
+
};
|
|
18537
|
+
}
|
|
18538
|
+
if (noSubMode === "allow") {
|
|
18539
|
+
deps.log?.(`gh.pr.${op} auto-approved (no subscribers, AGENTBOX_GH_NO_SUB=allow)`);
|
|
18540
|
+
} else {
|
|
18541
|
+
const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx, {
|
|
18542
|
+
ttlMs: 5 * 60 * 1e3
|
|
18543
|
+
});
|
|
18544
|
+
if (verdict.answer !== "y") {
|
|
18545
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
18546
|
+
}
|
|
18547
|
+
}
|
|
18548
|
+
} else {
|
|
18549
|
+
const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx);
|
|
18550
|
+
if (verdict.answer !== "y") {
|
|
18551
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
18552
|
+
}
|
|
18553
|
+
}
|
|
18554
|
+
}
|
|
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);
|
|
18569
|
+
}
|
|
18151
18570
|
async function runBrowserOpenMirror(action, deps) {
|
|
18152
18571
|
const params = action.params ?? {};
|
|
18153
18572
|
const url = typeof params.url === "string" ? params.url.trim() : "";
|
|
@@ -18174,8 +18593,8 @@ async function runBrowserOpenMirror(action, deps) {
|
|
|
18174
18593
|
{ ttlMs: TTL_MS }
|
|
18175
18594
|
);
|
|
18176
18595
|
if (verdict.answer === "y" && !verdict.cancelled) {
|
|
18177
|
-
const { spawn:
|
|
18178
|
-
const child =
|
|
18596
|
+
const { spawn: spawn6 } = await import("child_process");
|
|
18597
|
+
const child = spawn6("open", [url], { stdio: "ignore", detached: true });
|
|
18179
18598
|
child.unref();
|
|
18180
18599
|
}
|
|
18181
18600
|
} catch (err) {
|
|
@@ -18346,7 +18765,23 @@ async function runGitRpc(action, deps) {
|
|
|
18346
18765
|
stderr: `failed to resolve branch in sandbox ${containerPath}: ${branchProbe.stderr || branch}`
|
|
18347
18766
|
};
|
|
18348
18767
|
}
|
|
18349
|
-
|
|
18768
|
+
const isAgentboxBranch = branch.startsWith("agentbox/");
|
|
18769
|
+
const tokenClaimedGit = typeof params.hostInitiated === "string";
|
|
18770
|
+
const incomingHashGit = hashRpcParams(params);
|
|
18771
|
+
const hostInitiatedOk = !isAgentboxBranch && tokenClaimedGit && (deps.hostInitiatedTokens?.consume(
|
|
18772
|
+
params.hostInitiated,
|
|
18773
|
+
deps.boxId,
|
|
18774
|
+
"git.push",
|
|
18775
|
+
incomingHashGit
|
|
18776
|
+
) ?? false);
|
|
18777
|
+
if (action.method === "git.push" && !isAgentboxBranch && tokenClaimedGit && !hostInitiatedOk) {
|
|
18778
|
+
return {
|
|
18779
|
+
exitCode: 10,
|
|
18780
|
+
stdout: "",
|
|
18781
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
18782
|
+
};
|
|
18783
|
+
}
|
|
18784
|
+
if (action.method === "git.push" && !isAgentboxBranch && !hostInitiatedOk && deps.prompts && deps.subscribers) {
|
|
18350
18785
|
const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
|
|
18351
18786
|
if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
|
|
18352
18787
|
const noSubMode = (process.env["AGENTBOX_GIT_PUSH_NO_SUB"] ?? "deny").toLowerCase();
|
|
@@ -18421,10 +18856,45 @@ async function runGitRpc(action, deps) {
|
|
|
18421
18856
|
for (const a2 of params.args) if (typeof a2 === "string") argv.push(a2);
|
|
18422
18857
|
}
|
|
18423
18858
|
const push = await execa("git", argv, { reject: false });
|
|
18859
|
+
let pushStderr = push.stderr ?? "";
|
|
18860
|
+
if ((push.exitCode ?? 1) === 0 && !branch.startsWith("agentbox/")) {
|
|
18861
|
+
try {
|
|
18862
|
+
const sha = await execa(
|
|
18863
|
+
"git",
|
|
18864
|
+
["-C", lookup.workspacePath, "rev-parse", branch],
|
|
18865
|
+
{ reject: false }
|
|
18866
|
+
);
|
|
18867
|
+
const shaText = (sha.stdout ?? "").trim();
|
|
18868
|
+
if (sha.exitCode === 0 && shaText.length > 0) {
|
|
18869
|
+
const updateRef = await backend.exec(
|
|
18870
|
+
handle,
|
|
18871
|
+
`git -C ${shellQuote(containerPath)} update-ref refs/remotes/${remote2}/${branch} ${shellQuote(shaText)}`
|
|
18872
|
+
);
|
|
18873
|
+
if (updateRef.exitCode !== 0) {
|
|
18874
|
+
pushStderr += `
|
|
18875
|
+
relay: post-push in-box update-ref refs/remotes/${remote2}/${branch} failed: ${updateRef.stderr || updateRef.stdout}`;
|
|
18876
|
+
}
|
|
18877
|
+
const setUpstream = await backend.exec(
|
|
18878
|
+
handle,
|
|
18879
|
+
`git -C ${shellQuote(containerPath)} branch --set-upstream-to=${remote2}/${branch} ${shellQuote(branch)}`
|
|
18880
|
+
);
|
|
18881
|
+
if (setUpstream.exitCode !== 0) {
|
|
18882
|
+
pushStderr += `
|
|
18883
|
+
relay: post-push in-box --set-upstream-to=${remote2}/${branch} failed: ${setUpstream.stderr || setUpstream.stdout}`;
|
|
18884
|
+
}
|
|
18885
|
+
} else {
|
|
18886
|
+
pushStderr += `
|
|
18887
|
+
relay: post-push rev-parse ${branch} failed on host; skipping in-box origin/upstream sync`;
|
|
18888
|
+
}
|
|
18889
|
+
} catch (err) {
|
|
18890
|
+
pushStderr += `
|
|
18891
|
+
relay: post-push in-box origin/upstream sync threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
18892
|
+
}
|
|
18893
|
+
}
|
|
18424
18894
|
return {
|
|
18425
18895
|
exitCode: push.exitCode ?? 1,
|
|
18426
18896
|
stdout: push.stdout ?? "",
|
|
18427
|
-
stderr:
|
|
18897
|
+
stderr: pushStderr
|
|
18428
18898
|
};
|
|
18429
18899
|
}
|
|
18430
18900
|
const remote = params.remote ?? "origin";
|
|
@@ -18471,7 +18941,7 @@ function shellQuote(arg) {
|
|
|
18471
18941
|
}
|
|
18472
18942
|
|
|
18473
18943
|
// src/host-action-queue.ts
|
|
18474
|
-
var
|
|
18944
|
+
var import_node_crypto3 = require("crypto");
|
|
18475
18945
|
var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
|
|
18476
18946
|
var HostActionQueue = class {
|
|
18477
18947
|
map = /* @__PURE__ */ new Map();
|
|
@@ -18489,7 +18959,7 @@ var HostActionQueue = class {
|
|
|
18489
18959
|
* is open" semantics).
|
|
18490
18960
|
*/
|
|
18491
18961
|
enqueue(boxId, method, params) {
|
|
18492
|
-
const id = (0,
|
|
18962
|
+
const id = (0, import_node_crypto3.randomUUID)();
|
|
18493
18963
|
const action = {
|
|
18494
18964
|
id,
|
|
18495
18965
|
boxId,
|
|
@@ -18555,7 +19025,7 @@ var HostActionQueue = class {
|
|
|
18555
19025
|
};
|
|
18556
19026
|
|
|
18557
19027
|
// src/notices.ts
|
|
18558
|
-
var
|
|
19028
|
+
var import_node_crypto4 = require("crypto");
|
|
18559
19029
|
var DEFAULT_NOTICE_TTL_MS = 66e4;
|
|
18560
19030
|
var BoxNotices = class {
|
|
18561
19031
|
constructor(subscribers) {
|
|
@@ -18578,7 +19048,7 @@ var BoxNotices = class {
|
|
|
18578
19048
|
this.entries.delete(id);
|
|
18579
19049
|
}
|
|
18580
19050
|
}
|
|
18581
|
-
const ev = { id: (0,
|
|
19051
|
+
const ev = { id: (0, import_node_crypto4.randomUUID)(), kind, message };
|
|
18582
19052
|
const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;
|
|
18583
19053
|
const timer = setTimeout(() => {
|
|
18584
19054
|
if (this.entries.delete(ev.id)) {
|
|
@@ -18796,6 +19266,8 @@ function createRelayServer(opts) {
|
|
|
18796
19266
|
const prompts = new PendingPrompts();
|
|
18797
19267
|
const subscribers = new PromptSubscribers();
|
|
18798
19268
|
const notices = new BoxNotices(subscribers);
|
|
19269
|
+
const hostInitiatedTokens = new HostInitiatedTokens();
|
|
19270
|
+
let queuePoke = null;
|
|
18799
19271
|
const host = opts.host ?? "0.0.0.0";
|
|
18800
19272
|
const mode = opts.mode ?? "host";
|
|
18801
19273
|
const hostActions = mode === "box" ? new HostActionQueue() : null;
|
|
@@ -18823,7 +19295,13 @@ function createRelayServer(opts) {
|
|
|
18823
19295
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "relay"}`);
|
|
18824
19296
|
const route = `${req.method ?? "GET"} ${url.pathname}`;
|
|
18825
19297
|
if (route === "GET /healthz") {
|
|
18826
|
-
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
|
+
});
|
|
18827
19305
|
return;
|
|
18828
19306
|
}
|
|
18829
19307
|
if (url.pathname.startsWith("/bridge/")) {
|
|
@@ -18928,21 +19406,36 @@ function createRelayServer(opts) {
|
|
|
18928
19406
|
if (body.method === "git.push" || body.method === "git.fetch") {
|
|
18929
19407
|
if (body.method === "git.push") {
|
|
18930
19408
|
const params = body.params;
|
|
18931
|
-
const
|
|
18932
|
-
|
|
18933
|
-
|
|
18934
|
-
|
|
18935
|
-
|
|
18936
|
-
|
|
18937
|
-
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
18942
|
-
if (verdict.answer !== "y") {
|
|
18943
|
-
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
19409
|
+
const worktree = resolveWorktree(reg, params?.path ?? "/workspace");
|
|
19410
|
+
const isAgentboxBranch = worktree?.branch.startsWith("agentbox/") ?? false;
|
|
19411
|
+
const tokenClaimed = typeof params?.hostInitiated === "string";
|
|
19412
|
+
const incomingHash = hashRpcParams(params);
|
|
19413
|
+
const hostInitiatedOk = !isAgentboxBranch && tokenClaimed && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, "git.push", incomingHash);
|
|
19414
|
+
if (!isAgentboxBranch && tokenClaimed && !hostInitiatedOk) {
|
|
19415
|
+
send(res, 500, {
|
|
19416
|
+
exitCode: 10,
|
|
19417
|
+
stdout: "",
|
|
19418
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
19419
|
+
});
|
|
18944
19420
|
return;
|
|
18945
19421
|
}
|
|
19422
|
+
if (!isAgentboxBranch && !hostInitiatedOk) {
|
|
19423
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
19424
|
+
kind: "confirm",
|
|
19425
|
+
message: `Allow git push from box ${reg.name}?`,
|
|
19426
|
+
detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
|
|
19427
|
+
defaultAnswer: "n",
|
|
19428
|
+
context: {
|
|
19429
|
+
command: "git push",
|
|
19430
|
+
cwd: params?.path,
|
|
19431
|
+
argv: params?.args
|
|
19432
|
+
}
|
|
19433
|
+
});
|
|
19434
|
+
if (verdict.answer !== "y") {
|
|
19435
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
19436
|
+
return;
|
|
19437
|
+
}
|
|
19438
|
+
}
|
|
18946
19439
|
}
|
|
18947
19440
|
const result = await handleGitRpc(reg, body.method, body.params);
|
|
18948
19441
|
const status = result.exitCode === 0 ? 200 : 500;
|
|
@@ -18975,6 +19468,33 @@ function createRelayServer(opts) {
|
|
|
18975
19468
|
send(res, status, result);
|
|
18976
19469
|
return;
|
|
18977
19470
|
}
|
|
19471
|
+
if (body.method.startsWith("gh.pr.")) {
|
|
19472
|
+
const op = body.method.slice("gh.pr.".length);
|
|
19473
|
+
if (!isGhPrOp(op)) {
|
|
19474
|
+
send(res, 400, { error: `unknown gh.pr.* op: ${op}` });
|
|
19475
|
+
return;
|
|
19476
|
+
}
|
|
19477
|
+
const result = await handleGhPrRpc(
|
|
19478
|
+
op,
|
|
19479
|
+
reg,
|
|
19480
|
+
body.params,
|
|
19481
|
+
prompts,
|
|
19482
|
+
subscribers,
|
|
19483
|
+
hostInitiatedTokens
|
|
19484
|
+
);
|
|
19485
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
19486
|
+
send(res, status, result);
|
|
19487
|
+
return;
|
|
19488
|
+
}
|
|
19489
|
+
if (body.method === "git.clone" || body.method === "gh.repo.clone") {
|
|
19490
|
+
send(res, 501, {
|
|
19491
|
+
exitCode: 64,
|
|
19492
|
+
stdout: "",
|
|
19493
|
+
stderr: `${body.method}: not yet implemented (deferred; see docs/plans/gh-and-git-shims-host-only.md). Run \`gh\` / \`git\` on the host directly for now.
|
|
19494
|
+
`
|
|
19495
|
+
});
|
|
19496
|
+
return;
|
|
19497
|
+
}
|
|
18978
19498
|
if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
|
|
18979
19499
|
const params = body.params;
|
|
18980
19500
|
const kind = body.method.split(".")[1] ?? "workspace";
|
|
@@ -19111,6 +19631,7 @@ function createRelayServer(opts) {
|
|
|
19111
19631
|
boxName: reg.name,
|
|
19112
19632
|
prompts,
|
|
19113
19633
|
subscribers,
|
|
19634
|
+
hostInitiatedTokens,
|
|
19114
19635
|
log
|
|
19115
19636
|
});
|
|
19116
19637
|
await respond(result);
|
|
@@ -19123,6 +19644,11 @@ function createRelayServer(opts) {
|
|
|
19123
19644
|
});
|
|
19124
19645
|
}
|
|
19125
19646
|
} : void 0,
|
|
19647
|
+
// Self-heal a dead preview transport (hetzner SSH `-L` after a
|
|
19648
|
+
// ControlMaster death). The relay strips the `cloud:` prefix
|
|
19649
|
+
// the cloud-provider tags onto BoxRecord.container — what the
|
|
19650
|
+
// backend's `get(sandboxId)` expects is the bare sandbox id.
|
|
19651
|
+
recoverPreviewUrl: reg.backend ? async () => refreshCloudPreviewUrl(reg.backend, reg.boxId, DEFAULT_BOX_RELAY_PORT) : void 0,
|
|
19126
19652
|
logger: log
|
|
19127
19653
|
});
|
|
19128
19654
|
} catch (err) {
|
|
@@ -19237,6 +19763,27 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19237
19763
|
send(res, 204, null);
|
|
19238
19764
|
return;
|
|
19239
19765
|
}
|
|
19766
|
+
if (route === "POST /admin/host-initiated/mint") {
|
|
19767
|
+
const body = await readJsonBody(req);
|
|
19768
|
+
if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.method !== "string" || body.method.length === 0) {
|
|
19769
|
+
send(res, 400, { error: "expected {boxId, method, paramsHash, ttlMs?}" });
|
|
19770
|
+
return;
|
|
19771
|
+
}
|
|
19772
|
+
let paramsHash;
|
|
19773
|
+
if (body.paramsHash === null || body.paramsHash === void 0) {
|
|
19774
|
+
paramsHash = null;
|
|
19775
|
+
} else if (typeof body.paramsHash === "string" && /^[0-9a-f]{64}$/.test(body.paramsHash)) {
|
|
19776
|
+
paramsHash = body.paramsHash;
|
|
19777
|
+
} else {
|
|
19778
|
+
send(res, 400, { error: "paramsHash must be a 64-hex sha256 string or null" });
|
|
19779
|
+
return;
|
|
19780
|
+
}
|
|
19781
|
+
const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
|
|
19782
|
+
const token = hostInitiatedTokens.mint(body.boxId, body.method, paramsHash, ttlMs);
|
|
19783
|
+
log(`host-initiated-mint box=${body.boxId} method=${body.method} paramsBound=${paramsHash !== null}`);
|
|
19784
|
+
send(res, 200, { token });
|
|
19785
|
+
return;
|
|
19786
|
+
}
|
|
19240
19787
|
if (route === "POST /admin/notices/set") {
|
|
19241
19788
|
const body = await readJsonBody(req);
|
|
19242
19789
|
if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.kind !== "string" || body.kind.length === 0 || typeof body.message !== "string" || body.message.length === 0) {
|
|
@@ -19249,6 +19796,17 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19249
19796
|
send(res, 200, { id });
|
|
19250
19797
|
return;
|
|
19251
19798
|
}
|
|
19799
|
+
if (route === "POST /admin/queue/enqueue") {
|
|
19800
|
+
const body = await readJsonBody(req);
|
|
19801
|
+
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
19802
|
+
send(res, 400, { error: "expected {id}" });
|
|
19803
|
+
return;
|
|
19804
|
+
}
|
|
19805
|
+
log(`queue-enqueue id=${body.id}`);
|
|
19806
|
+
queuePoke?.();
|
|
19807
|
+
send(res, 204, null);
|
|
19808
|
+
return;
|
|
19809
|
+
}
|
|
19252
19810
|
if (route === "POST /admin/notices/clear") {
|
|
19253
19811
|
const body = await readJsonBody(req);
|
|
19254
19812
|
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
@@ -19285,6 +19843,9 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19285
19843
|
notices,
|
|
19286
19844
|
hostActions: hostActions ?? void 0,
|
|
19287
19845
|
url: `http://${host}:${String(opts.port)}`,
|
|
19846
|
+
setQueuePoke: (fn) => {
|
|
19847
|
+
queuePoke = fn;
|
|
19848
|
+
},
|
|
19288
19849
|
close: async () => {
|
|
19289
19850
|
if (pollers) await pollers.stopAll();
|
|
19290
19851
|
await new Promise((resolve2, reject) => {
|
|
@@ -19336,7 +19897,71 @@ async function handleGitRpc(reg, method, params) {
|
|
|
19336
19897
|
if (typeof a2 === "string") argv.push(a2);
|
|
19337
19898
|
}
|
|
19338
19899
|
}
|
|
19339
|
-
|
|
19900
|
+
const result = await runHostCommand(argv);
|
|
19901
|
+
if (method === "git.push" && result.exitCode === 0 && !worktree.branch.startsWith("agentbox/")) {
|
|
19902
|
+
await runHostCommand([
|
|
19903
|
+
"git",
|
|
19904
|
+
"-C",
|
|
19905
|
+
worktree.hostMainRepo,
|
|
19906
|
+
"branch",
|
|
19907
|
+
`--set-upstream-to=${remote}/${worktree.branch}`,
|
|
19908
|
+
worktree.branch
|
|
19909
|
+
]);
|
|
19910
|
+
}
|
|
19911
|
+
return result;
|
|
19912
|
+
}
|
|
19913
|
+
async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiatedTokens) {
|
|
19914
|
+
const mergeBypass = refuseMergeBypass(op);
|
|
19915
|
+
if (mergeBypass) return mergeBypass;
|
|
19916
|
+
const checkoutOptIn = refuseCheckoutByDefault(op);
|
|
19917
|
+
if (checkoutOptIn) return checkoutOptIn;
|
|
19918
|
+
const containerPath = params?.path ?? "/workspace";
|
|
19919
|
+
const worktree = resolveWorktree(reg, containerPath);
|
|
19920
|
+
if (!worktree) {
|
|
19921
|
+
return {
|
|
19922
|
+
exitCode: 64,
|
|
19923
|
+
stdout: "",
|
|
19924
|
+
stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
|
|
19925
|
+
};
|
|
19926
|
+
}
|
|
19927
|
+
const ghReady = await assertGhReady();
|
|
19928
|
+
if (ghReady) return ghReady;
|
|
19929
|
+
const args = Array.isArray(params?.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
|
|
19930
|
+
if (op === "checkout") {
|
|
19931
|
+
const branches = (reg.worktrees ?? []).map((w) => w.branch);
|
|
19932
|
+
const guard = await checkoutGuards(worktree.hostMainRepo, branches);
|
|
19933
|
+
if (guard) return guard;
|
|
19934
|
+
}
|
|
19935
|
+
const tokenClaimedGh = typeof params?.hostInitiated === "string";
|
|
19936
|
+
const incomingHashGh = hashRpcParams(params);
|
|
19937
|
+
const hostInitiatedOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, `gh.pr.${op}`, incomingHashGh);
|
|
19938
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && !hostInitiatedOk) {
|
|
19939
|
+
return {
|
|
19940
|
+
exitCode: 10,
|
|
19941
|
+
stdout: "",
|
|
19942
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
19943
|
+
};
|
|
19944
|
+
}
|
|
19945
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedOk) {
|
|
19946
|
+
const detail = args.join(" ").slice(0, 200);
|
|
19947
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
19948
|
+
kind: "confirm",
|
|
19949
|
+
message: `Allow gh pr ${op} from box ${reg.name}?`,
|
|
19950
|
+
detail,
|
|
19951
|
+
defaultAnswer: "n",
|
|
19952
|
+
context: {
|
|
19953
|
+
command: `gh pr ${op}`,
|
|
19954
|
+
cwd: containerPath,
|
|
19955
|
+
argv: args
|
|
19956
|
+
}
|
|
19957
|
+
});
|
|
19958
|
+
if (verdict.answer !== "y") {
|
|
19959
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
19960
|
+
}
|
|
19961
|
+
}
|
|
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);
|
|
19340
19965
|
}
|
|
19341
19966
|
async function handleCpRpc(reg, method, params) {
|
|
19342
19967
|
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
@@ -19397,7 +20022,7 @@ function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
|
|
|
19397
20022
|
resolve2({ exitCode: 64, stdout: "", stderr: "empty command" });
|
|
19398
20023
|
return;
|
|
19399
20024
|
}
|
|
19400
|
-
const child = (0,
|
|
20025
|
+
const child = (0, import_node_child_process7.spawn)(cmd, rest, {
|
|
19401
20026
|
env: process.env,
|
|
19402
20027
|
stdio: ["ignore", "pipe", "pipe"]
|
|
19403
20028
|
});
|
|
@@ -19446,7 +20071,7 @@ async function startRelayServer(opts) {
|
|
|
19446
20071
|
}
|
|
19447
20072
|
|
|
19448
20073
|
// src/autopause.ts
|
|
19449
|
-
var
|
|
20074
|
+
var import_node_child_process8 = require("child_process");
|
|
19450
20075
|
var import_promises16 = require("fs/promises");
|
|
19451
20076
|
|
|
19452
20077
|
// ../config/dist/index.js
|
|
@@ -19464,6 +20089,7 @@ var BUILT_IN_DEFAULTS = {
|
|
|
19464
20089
|
defaultCheckpointDocker: "",
|
|
19465
20090
|
defaultCheckpointDaytona: "",
|
|
19466
20091
|
defaultCheckpointHetzner: "",
|
|
20092
|
+
defaultCheckpointVercel: "",
|
|
19467
20093
|
withPlaywright: false,
|
|
19468
20094
|
withEnv: false,
|
|
19469
20095
|
vnc: true,
|
|
@@ -19475,7 +20101,11 @@ var BUILT_IN_DEFAULTS = {
|
|
|
19475
20101
|
memory: 0,
|
|
19476
20102
|
cpus: 0,
|
|
19477
20103
|
pidsLimit: 0,
|
|
19478
|
-
disk: ""
|
|
20104
|
+
disk: "",
|
|
20105
|
+
bundleDepth: void 0,
|
|
20106
|
+
vercelVcpus: 2,
|
|
20107
|
+
vercelTimeoutMs: 27e5,
|
|
20108
|
+
vercelNetworkPolicy: ""
|
|
19479
20109
|
},
|
|
19480
20110
|
checkpoint: {
|
|
19481
20111
|
maxLayers: 3
|
|
@@ -19524,6 +20154,15 @@ var BUILT_IN_DEFAULTS = {
|
|
|
19524
20154
|
maxRunningBoxes: 5,
|
|
19525
20155
|
idleMinutes: 5
|
|
19526
20156
|
},
|
|
20157
|
+
queue: {
|
|
20158
|
+
enabled: true,
|
|
20159
|
+
maxConcurrent: 5,
|
|
20160
|
+
maxWorking: 0,
|
|
20161
|
+
idleGraceSeconds: 15
|
|
20162
|
+
},
|
|
20163
|
+
cloud: {
|
|
20164
|
+
useCurrentBranch: false
|
|
20165
|
+
},
|
|
19527
20166
|
maintenance: {
|
|
19528
20167
|
pruneProjectConfigs: true,
|
|
19529
20168
|
pruneProjectConfigsEvery: 50
|
|
@@ -19533,8 +20172,8 @@ var KEY_REGISTRY = [
|
|
|
19533
20172
|
{
|
|
19534
20173
|
key: "box.provider",
|
|
19535
20174
|
type: "enum",
|
|
19536
|
-
enumValues: ["docker", "daytona", "hetzner"],
|
|
19537
|
-
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."
|
|
19538
20177
|
},
|
|
19539
20178
|
{
|
|
19540
20179
|
key: "box.hostSnapshot",
|
|
@@ -19564,6 +20203,12 @@ var KEY_REGISTRY = [
|
|
|
19564
20203
|
description: "Per-provider override of `box.defaultCheckpoint` for hetzner. Wins over the global when set; set via `agentbox checkpoint set-default --provider hetzner`.",
|
|
19565
20204
|
advanced: true
|
|
19566
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
|
+
},
|
|
19567
20212
|
{
|
|
19568
20213
|
key: "checkpoint.maxLayers",
|
|
19569
20214
|
type: "int",
|
|
@@ -19632,6 +20277,26 @@ var KEY_REGISTRY = [
|
|
|
19632
20277
|
description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
|
|
19633
20278
|
advanced: true
|
|
19634
20279
|
},
|
|
20280
|
+
{
|
|
20281
|
+
key: "box.bundleDepth",
|
|
20282
|
+
type: "int",
|
|
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/)."
|
|
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
|
+
},
|
|
19635
20300
|
{
|
|
19636
20301
|
key: "claude.sessionName",
|
|
19637
20302
|
type: "string",
|
|
@@ -19739,6 +20404,31 @@ var KEY_REGISTRY = [
|
|
|
19739
20404
|
type: "int",
|
|
19740
20405
|
description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
|
|
19741
20406
|
},
|
|
20407
|
+
{
|
|
20408
|
+
key: "queue.enabled",
|
|
20409
|
+
type: "bool",
|
|
20410
|
+
description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
|
|
20411
|
+
},
|
|
20412
|
+
{
|
|
20413
|
+
key: "queue.maxConcurrent",
|
|
20414
|
+
type: "int",
|
|
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>`."
|
|
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
|
+
},
|
|
19742
20432
|
{
|
|
19743
20433
|
key: "maintenance.pruneProjectConfigs",
|
|
19744
20434
|
type: "bool",
|
|
@@ -19833,6 +20523,15 @@ function parseUserConfigObject(doc, where) {
|
|
|
19833
20523
|
}
|
|
19834
20524
|
const out = {};
|
|
19835
20525
|
for (const [branchName, branchRaw] of Object.entries(doc)) {
|
|
20526
|
+
if (branchName === "schema") {
|
|
20527
|
+
if (branchRaw !== void 0 && branchRaw !== null) {
|
|
20528
|
+
if (typeof branchRaw !== "number" || !Number.isInteger(branchRaw)) {
|
|
20529
|
+
throw new UserConfigError(`${where}.schema: must be an integer (got ${String(branchRaw)})`);
|
|
20530
|
+
}
|
|
20531
|
+
out.schema = branchRaw;
|
|
20532
|
+
}
|
|
20533
|
+
continue;
|
|
20534
|
+
}
|
|
19836
20535
|
const branchSpec = BRANCHES.get(branchName);
|
|
19837
20536
|
if (!branchSpec) {
|
|
19838
20537
|
throw new UserConfigError(
|
|
@@ -20005,7 +20704,7 @@ var INSPECT_TIMEOUT_MS = 15e3;
|
|
|
20005
20704
|
var PAUSE_TIMEOUT_MS = 3e4;
|
|
20006
20705
|
function runDocker(args, timeoutMs) {
|
|
20007
20706
|
return new Promise((resolve2) => {
|
|
20008
|
-
const child = (0,
|
|
20707
|
+
const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
20009
20708
|
let stdout = "";
|
|
20010
20709
|
let stderr = "";
|
|
20011
20710
|
let settled = false;
|
|
@@ -20063,6 +20762,381 @@ async function pauseContainer(name) {
|
|
|
20063
20762
|
}
|
|
20064
20763
|
}
|
|
20065
20764
|
|
|
20765
|
+
// src/queue.ts
|
|
20766
|
+
var import_node_child_process9 = require("child_process");
|
|
20767
|
+
var import_promises17 = require("fs/promises");
|
|
20768
|
+
var import_node_fs6 = require("fs");
|
|
20769
|
+
var import_node_path8 = require("path");
|
|
20770
|
+
var import_promises18 = require("timers/promises");
|
|
20771
|
+
var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
|
|
20772
|
+
async function loadQueueConfig() {
|
|
20773
|
+
const d = BUILT_IN_DEFAULTS.queue;
|
|
20774
|
+
let global3 = {};
|
|
20775
|
+
try {
|
|
20776
|
+
global3 = parseUserConfig(await (0, import_promises17.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
|
|
20777
|
+
} catch {
|
|
20778
|
+
}
|
|
20779
|
+
const q = global3.queue ?? {};
|
|
20780
|
+
return {
|
|
20781
|
+
enabled: q.enabled ?? d.enabled,
|
|
20782
|
+
maxConcurrent: q.maxConcurrent ?? d.maxConcurrent,
|
|
20783
|
+
maxWorking: q.maxWorking ?? d.maxWorking,
|
|
20784
|
+
idleGraceMs: (q.idleGraceSeconds ?? d.idleGraceSeconds) * 1e3
|
|
20785
|
+
};
|
|
20786
|
+
}
|
|
20787
|
+
async function writeJob(job) {
|
|
20788
|
+
await (0, import_promises17.mkdir)(QUEUE_DIR, { recursive: true });
|
|
20789
|
+
const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
|
|
20790
|
+
const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
|
|
20791
|
+
await (0, import_promises17.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
|
|
20792
|
+
await (0, import_promises17.rename)(tmp, final);
|
|
20793
|
+
}
|
|
20794
|
+
async function readJob(id) {
|
|
20795
|
+
try {
|
|
20796
|
+
const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
|
|
20797
|
+
return JSON.parse(raw);
|
|
20798
|
+
} catch (err) {
|
|
20799
|
+
if (err.code === "ENOENT") return null;
|
|
20800
|
+
throw err;
|
|
20801
|
+
}
|
|
20802
|
+
}
|
|
20803
|
+
async function loadQueue() {
|
|
20804
|
+
let entries;
|
|
20805
|
+
try {
|
|
20806
|
+
entries = await (0, import_promises17.readdir)(QUEUE_DIR);
|
|
20807
|
+
} catch (err) {
|
|
20808
|
+
if (err.code === "ENOENT") return [];
|
|
20809
|
+
throw err;
|
|
20810
|
+
}
|
|
20811
|
+
const out = [];
|
|
20812
|
+
for (const name of entries) {
|
|
20813
|
+
if (!name.endsWith(".json")) continue;
|
|
20814
|
+
try {
|
|
20815
|
+
const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
|
|
20816
|
+
out.push(JSON.parse(raw));
|
|
20817
|
+
} catch {
|
|
20818
|
+
}
|
|
20819
|
+
}
|
|
20820
|
+
out.sort((a2, b) => a2.createdAt < b.createdAt ? -1 : a2.createdAt > b.createdAt ? 1 : 0);
|
|
20821
|
+
return out;
|
|
20822
|
+
}
|
|
20823
|
+
function selectNextRunnable(jobs, runningCount) {
|
|
20824
|
+
for (const j of jobs) {
|
|
20825
|
+
if (j.status !== "queued") continue;
|
|
20826
|
+
if (runningCount < j.maxConcurrent) return j;
|
|
20827
|
+
}
|
|
20828
|
+
return null;
|
|
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
|
+
}
|
|
20894
|
+
var DEFAULT_INTERVAL_MS2 = 2e3;
|
|
20895
|
+
function startQueueLoop(deps) {
|
|
20896
|
+
const loadConfig = deps.loadConfig ?? loadQueueConfig;
|
|
20897
|
+
const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
|
|
20898
|
+
const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
|
|
20899
|
+
const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
|
|
20900
|
+
const { log, onStatusChange } = deps;
|
|
20901
|
+
const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
|
|
20902
|
+
let ticking = false;
|
|
20903
|
+
let stopped = false;
|
|
20904
|
+
let warnedNoWorkingDeps = false;
|
|
20905
|
+
let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
|
|
20906
|
+
log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20907
|
+
});
|
|
20908
|
+
async function tick() {
|
|
20909
|
+
if (ticking) return;
|
|
20910
|
+
ticking = true;
|
|
20911
|
+
try {
|
|
20912
|
+
const cfg = await loadConfig();
|
|
20913
|
+
if (!cfg.enabled) return;
|
|
20914
|
+
const jobs = await loadQueue();
|
|
20915
|
+
const hasQueued = jobs.some((j) => j.status === "queued");
|
|
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
|
+
}
|
|
20925
|
+
while (!stopped) {
|
|
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
|
+
}
|
|
20937
|
+
if (!next) return;
|
|
20938
|
+
const current = await readJob(next.id);
|
|
20939
|
+
if (!current || current.status !== "queued") continue;
|
|
20940
|
+
const updated = {
|
|
20941
|
+
...current,
|
|
20942
|
+
status: "running",
|
|
20943
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20944
|
+
};
|
|
20945
|
+
await writeJob(updated);
|
|
20946
|
+
onStatusChange?.(updated);
|
|
20947
|
+
try {
|
|
20948
|
+
const pid = await spawnWorker(updated);
|
|
20949
|
+
if (typeof pid === "number") {
|
|
20950
|
+
const withPid = { ...updated, pid };
|
|
20951
|
+
await writeJob(withPid);
|
|
20952
|
+
onStatusChange?.(withPid);
|
|
20953
|
+
const ceil = gateByWorking ? typeof updated.maxWorking === "number" && updated.maxWorking > 0 ? updated.maxWorking : cfg.maxWorking : updated.maxConcurrent;
|
|
20954
|
+
log(
|
|
20955
|
+
`queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; ${gateByWorking ? "working" : "running"} ${String(occupancy + 1)}/${String(ceil)}`
|
|
20956
|
+
);
|
|
20957
|
+
} else {
|
|
20958
|
+
log(`queue: started job ${updated.id} (${updated.agent}); pid unknown`);
|
|
20959
|
+
}
|
|
20960
|
+
} catch (err) {
|
|
20961
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
20962
|
+
const failed = {
|
|
20963
|
+
...updated,
|
|
20964
|
+
status: "failed",
|
|
20965
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20966
|
+
reason: `worker-spawn-failed: ${msg}`
|
|
20967
|
+
};
|
|
20968
|
+
await writeJob(failed);
|
|
20969
|
+
onStatusChange?.(failed);
|
|
20970
|
+
log(`queue: spawn for job ${updated.id} failed: ${msg}`);
|
|
20971
|
+
}
|
|
20972
|
+
}
|
|
20973
|
+
} catch (err) {
|
|
20974
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
20975
|
+
log(`queue: tick error: ${msg}`);
|
|
20976
|
+
} finally {
|
|
20977
|
+
ticking = false;
|
|
20978
|
+
}
|
|
20979
|
+
}
|
|
20980
|
+
function poke() {
|
|
20981
|
+
if (stopped) return;
|
|
20982
|
+
inFlight = tick();
|
|
20983
|
+
}
|
|
20984
|
+
const handle = {
|
|
20985
|
+
poke,
|
|
20986
|
+
stop: async () => {
|
|
20987
|
+
stopped = true;
|
|
20988
|
+
clearInterval(timer);
|
|
20989
|
+
await inFlight.catch(() => {
|
|
20990
|
+
});
|
|
20991
|
+
}
|
|
20992
|
+
};
|
|
20993
|
+
const timer = setInterval(() => {
|
|
20994
|
+
if (stopped) return;
|
|
20995
|
+
inFlight = tick();
|
|
20996
|
+
}, intervalMs);
|
|
20997
|
+
timer.unref();
|
|
20998
|
+
return handle;
|
|
20999
|
+
}
|
|
21000
|
+
async function recoverOrphanedWorkers(log, onChange) {
|
|
21001
|
+
const jobs = await loadQueue();
|
|
21002
|
+
for (const j of jobs) {
|
|
21003
|
+
if (j.status !== "running") continue;
|
|
21004
|
+
if (typeof j.pid === "number" && processAlive(j.pid)) continue;
|
|
21005
|
+
const failed = {
|
|
21006
|
+
...j,
|
|
21007
|
+
status: "failed",
|
|
21008
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21009
|
+
reason: "worker-died"
|
|
21010
|
+
};
|
|
21011
|
+
await writeJob(failed);
|
|
21012
|
+
onChange?.(failed);
|
|
21013
|
+
log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
|
|
21014
|
+
}
|
|
21015
|
+
}
|
|
21016
|
+
function processAlive(pid) {
|
|
21017
|
+
try {
|
|
21018
|
+
process.kill(pid, 0);
|
|
21019
|
+
return true;
|
|
21020
|
+
} catch {
|
|
21021
|
+
return false;
|
|
21022
|
+
}
|
|
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
|
+
}
|
|
21051
|
+
var RUNNING_COUNT_CACHE_MS = 3e3;
|
|
21052
|
+
var runningCountCache = null;
|
|
21053
|
+
async function defaultCountRunningBoxes() {
|
|
21054
|
+
const now = Date.now();
|
|
21055
|
+
if (runningCountCache && runningCountCache.expiresAt > now) {
|
|
21056
|
+
return runningCountCache.value;
|
|
21057
|
+
}
|
|
21058
|
+
const value = await uncachedCountRunningBoxes();
|
|
21059
|
+
runningCountCache = { value, expiresAt: now + RUNNING_COUNT_CACHE_MS };
|
|
21060
|
+
return value;
|
|
21061
|
+
}
|
|
21062
|
+
async function uncachedCountRunningBoxes() {
|
|
21063
|
+
let boxes;
|
|
21064
|
+
try {
|
|
21065
|
+
boxes = (await readState(STATE_FILE)).boxes;
|
|
21066
|
+
} catch {
|
|
21067
|
+
return 0;
|
|
21068
|
+
}
|
|
21069
|
+
if (boxes.length === 0) return 0;
|
|
21070
|
+
let count2 = 0;
|
|
21071
|
+
const dockerBoxes = [];
|
|
21072
|
+
for (const b of boxes) {
|
|
21073
|
+
const provider = b.provider ?? "docker";
|
|
21074
|
+
if (provider === "docker") {
|
|
21075
|
+
dockerBoxes.push(b);
|
|
21076
|
+
} else {
|
|
21077
|
+
count2 += 1;
|
|
21078
|
+
}
|
|
21079
|
+
}
|
|
21080
|
+
if (dockerBoxes.length > 0) {
|
|
21081
|
+
const states = await Promise.all(dockerBoxes.map((b) => inspectDockerState(b.container)));
|
|
21082
|
+
for (const s of states) {
|
|
21083
|
+
if (s === "running") count2 += 1;
|
|
21084
|
+
}
|
|
21085
|
+
}
|
|
21086
|
+
return count2;
|
|
21087
|
+
}
|
|
21088
|
+
function inspectDockerState(containerName) {
|
|
21089
|
+
return new Promise((resolveP) => {
|
|
21090
|
+
const child = (0, import_node_child_process9.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
|
|
21091
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
21092
|
+
});
|
|
21093
|
+
let out = "";
|
|
21094
|
+
let settled = false;
|
|
21095
|
+
const finish = (state) => {
|
|
21096
|
+
if (settled) return;
|
|
21097
|
+
settled = true;
|
|
21098
|
+
resolveP(state);
|
|
21099
|
+
};
|
|
21100
|
+
const timer = setTimeout(() => {
|
|
21101
|
+
child.kill("SIGTERM");
|
|
21102
|
+
finish("other");
|
|
21103
|
+
}, 1e4);
|
|
21104
|
+
child.stdout?.on("data", (c3) => {
|
|
21105
|
+
out += c3.toString("utf8");
|
|
21106
|
+
});
|
|
21107
|
+
child.on("error", () => {
|
|
21108
|
+
clearTimeout(timer);
|
|
21109
|
+
finish("other");
|
|
21110
|
+
});
|
|
21111
|
+
child.on("close", () => {
|
|
21112
|
+
clearTimeout(timer);
|
|
21113
|
+
finish(out.trim() === "running" ? "running" : "other");
|
|
21114
|
+
});
|
|
21115
|
+
});
|
|
21116
|
+
}
|
|
21117
|
+
async function defaultSpawnWorker(job) {
|
|
21118
|
+
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
21119
|
+
if (!entry || !(0, import_node_fs6.existsSync)(entry)) {
|
|
21120
|
+
throw new Error(
|
|
21121
|
+
`AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
|
|
21122
|
+
);
|
|
21123
|
+
}
|
|
21124
|
+
await (0, import_promises17.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
|
|
21125
|
+
const fd = (0, import_node_fs6.openSync)(job.logPath, "a");
|
|
21126
|
+
const child = (0, import_node_child_process9.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
|
|
21127
|
+
detached: true,
|
|
21128
|
+
stdio: ["ignore", fd, fd],
|
|
21129
|
+
env: process.env
|
|
21130
|
+
});
|
|
21131
|
+
child.unref();
|
|
21132
|
+
try {
|
|
21133
|
+
(0, import_node_fs6.closeSync)(fd);
|
|
21134
|
+
} catch {
|
|
21135
|
+
}
|
|
21136
|
+
return typeof child.pid === "number" ? child.pid : null;
|
|
21137
|
+
}
|
|
21138
|
+
var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
|
|
21139
|
+
|
|
20066
21140
|
// src/bin.ts
|
|
20067
21141
|
var program2 = new Command();
|
|
20068
21142
|
program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");
|
|
@@ -20088,10 +21162,19 @@ program2.command("serve").description("Run the HTTP relay in the foreground").op
|
|
|
20088
21162
|
log: (line) => process.stdout.write(`agentbox-relay: ${line}
|
|
20089
21163
|
`)
|
|
20090
21164
|
});
|
|
21165
|
+
const queue = startQueueLoop({
|
|
21166
|
+
log: (line) => process.stdout.write(`agentbox-relay: ${line}
|
|
21167
|
+
`),
|
|
21168
|
+
registry: handle.registry,
|
|
21169
|
+
statusStore: handle.statusStore
|
|
21170
|
+
});
|
|
21171
|
+
handle.setQueuePoke(() => {
|
|
21172
|
+
queue.poke?.();
|
|
21173
|
+
});
|
|
20091
21174
|
const shutdown = (signal) => {
|
|
20092
21175
|
process.stdout.write(`agentbox-relay: ${signal} \u2014 shutting down
|
|
20093
21176
|
`);
|
|
20094
|
-
autopause.stop().finally(() => handle.close()).finally(() => process.exit(0));
|
|
21177
|
+
Promise.allSettled([autopause.stop(), queue.stop()]).finally(() => handle.close()).finally(() => process.exit(0));
|
|
20095
21178
|
};
|
|
20096
21179
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
20097
21180
|
process.on("SIGINT", () => shutdown("SIGINT"));
|