@madarco/agentbox 0.7.0 → 0.8.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 +13 -0
- package/dist/{chunk-NW5NYTQM.js → chunk-67N47KUS.js} +359 -85
- package/dist/chunk-67N47KUS.js.map +1 -0
- package/dist/{chunk-NAVL4R34.js → chunk-6OZDFNBF.js} +1084 -516
- package/dist/chunk-6OZDFNBF.js.map +1 -0
- package/dist/chunk-BGK32PZE.js +455 -0
- package/dist/chunk-BGK32PZE.js.map +1 -0
- package/dist/{chunk-7KOEFGN2.js → chunk-FODMEHD3.js} +52 -14
- package/dist/chunk-FODMEHD3.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-V5KZGB5V.js → chunk-LEV3KICD.js} +18 -2
- package/dist/chunk-LEV3KICD.js.map +1 -0
- package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js → cloud-poller-SUNA6ZQC-2RG5WPRN.js} +2 -2
- package/dist/{dist-R67WMLCF.js → dist-L4LCG5SJ.js} +120 -10
- package/dist/dist-L4LCG5SJ.js.map +1 -0
- package/dist/{dist-ETCFRVPA.js → dist-LOZBWMBF.js} +44 -20
- package/dist/{dist-QZGJIBT5.js → dist-ZODPD2I6.js} +142 -74
- package/dist/dist-ZODPD2I6.js.map +1 -0
- package/dist/index.js +3563 -845
- package/dist/index.js.map +1 -1
- package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js +18 -0
- package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js.map +1 -0
- package/package.json +4 -4
- 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 +1118 -71
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +66 -35
- 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/claude-managed-settings.json +62 -1
- package/runtime/hetzner/ctl.cjs +1118 -71
- 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 +927 -36
- 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-T727ZPRV.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-LOZBWMBF.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,245 @@ 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
|
+
var GH_RPC_TIMEOUT_MS = 12e4;
|
|
18048
|
+
var GH_READY_CACHE_TTL_MS = 6e4;
|
|
18049
|
+
var ghReadyCache;
|
|
18050
|
+
async function assertGhReady() {
|
|
18051
|
+
const now = Date.now();
|
|
18052
|
+
if (ghReadyCache && ghReadyCache.expiresAt > now) {
|
|
18053
|
+
return ghReadyCache.result;
|
|
18054
|
+
}
|
|
18055
|
+
const result = await probeGh();
|
|
18056
|
+
ghReadyCache = { result, expiresAt: now + GH_READY_CACHE_TTL_MS };
|
|
18057
|
+
return result;
|
|
18058
|
+
}
|
|
18059
|
+
async function probeGh() {
|
|
18060
|
+
const version = await runHostGh(["--version"], process.cwd(), 1e4);
|
|
18061
|
+
if (version.exitCode === 127 || /ENOENT/.test(version.stderr)) {
|
|
18062
|
+
return {
|
|
18063
|
+
exitCode: 127,
|
|
18064
|
+
stdout: "",
|
|
18065
|
+
stderr: "gh not installed on host (https://cli.github.com)\n"
|
|
18066
|
+
};
|
|
18067
|
+
}
|
|
18068
|
+
if (version.exitCode !== 0) {
|
|
18069
|
+
return {
|
|
18070
|
+
exitCode: version.exitCode,
|
|
18071
|
+
stdout: "",
|
|
18072
|
+
stderr: `gh --version failed: ${version.stderr || version.stdout}`.trimEnd() + "\n"
|
|
18073
|
+
};
|
|
18074
|
+
}
|
|
18075
|
+
const auth = await runHostGh(["auth", "status"], process.cwd(), 15e3);
|
|
18076
|
+
if (auth.exitCode !== 0) {
|
|
18077
|
+
return {
|
|
18078
|
+
exitCode: 4,
|
|
18079
|
+
stdout: "",
|
|
18080
|
+
stderr: "gh not authenticated on host (run `gh auth login`)\n"
|
|
18081
|
+
};
|
|
18082
|
+
}
|
|
18083
|
+
return null;
|
|
18084
|
+
}
|
|
18085
|
+
function runHostGh(args, cwd, timeoutMs = GH_RPC_TIMEOUT_MS) {
|
|
18086
|
+
return new Promise((resolve2) => {
|
|
18087
|
+
const child = (0, import_node_child_process6.spawn)("gh", args, {
|
|
18088
|
+
cwd,
|
|
18089
|
+
env: process.env,
|
|
18090
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
18091
|
+
});
|
|
18092
|
+
let stdout = "";
|
|
18093
|
+
let stderr = "";
|
|
18094
|
+
let settled = false;
|
|
18095
|
+
const finish = (exitCode) => {
|
|
18096
|
+
if (settled) return;
|
|
18097
|
+
settled = true;
|
|
18098
|
+
resolve2({ exitCode, stdout, stderr });
|
|
18099
|
+
};
|
|
18100
|
+
const timer = setTimeout(() => {
|
|
18101
|
+
child.kill("SIGTERM");
|
|
18102
|
+
stderr += `
|
|
18103
|
+
relay: gh command timed out after ${String(timeoutMs)}ms
|
|
18104
|
+
`;
|
|
18105
|
+
finish(124);
|
|
18106
|
+
}, timeoutMs);
|
|
18107
|
+
child.stdout?.on("data", (chunk) => {
|
|
18108
|
+
stdout += chunk.toString("utf8");
|
|
18109
|
+
});
|
|
18110
|
+
child.stderr?.on("data", (chunk) => {
|
|
18111
|
+
stderr += chunk.toString("utf8");
|
|
18112
|
+
});
|
|
18113
|
+
child.on("error", (err) => {
|
|
18114
|
+
clearTimeout(timer);
|
|
18115
|
+
const code = err.code;
|
|
18116
|
+
stderr += String(err.message ?? err);
|
|
18117
|
+
finish(code === "ENOENT" ? 127 : 1);
|
|
18118
|
+
});
|
|
18119
|
+
child.on("close", (code) => {
|
|
18120
|
+
clearTimeout(timer);
|
|
18121
|
+
finish(code ?? -1);
|
|
18122
|
+
});
|
|
18123
|
+
});
|
|
18124
|
+
}
|
|
18125
|
+
async function checkoutGuards(hostMainRepo, registeredBranches) {
|
|
18126
|
+
const status = await runGitProbe(["-C", hostMainRepo, "status", "--porcelain"]);
|
|
18127
|
+
if (status.exitCode !== 0) {
|
|
18128
|
+
return {
|
|
18129
|
+
exitCode: status.exitCode,
|
|
18130
|
+
stdout: "",
|
|
18131
|
+
stderr: `gh pr checkout: failed to inspect host repo: ${status.stderr || status.stdout}`.trimEnd() + "\n"
|
|
18132
|
+
};
|
|
18133
|
+
}
|
|
18134
|
+
if (status.stdout.trim().length > 0) {
|
|
18135
|
+
return {
|
|
18136
|
+
exitCode: 12,
|
|
18137
|
+
stdout: "",
|
|
18138
|
+
stderr: `gh pr checkout: ${hostMainRepo} has uncommitted changes; refusing to switch branches
|
|
18139
|
+
`
|
|
18140
|
+
};
|
|
18141
|
+
}
|
|
18142
|
+
const head = await runGitProbe(["-C", hostMainRepo, "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
18143
|
+
if (head.exitCode !== 0) {
|
|
18144
|
+
return {
|
|
18145
|
+
exitCode: head.exitCode,
|
|
18146
|
+
stdout: "",
|
|
18147
|
+
stderr: `gh pr checkout: failed to resolve HEAD: ${head.stderr || head.stdout}`.trimEnd() + "\n"
|
|
18148
|
+
};
|
|
18149
|
+
}
|
|
18150
|
+
const currentBranch = head.stdout.trim();
|
|
18151
|
+
if (registeredBranches.includes(currentBranch)) {
|
|
18152
|
+
return {
|
|
18153
|
+
exitCode: 12,
|
|
18154
|
+
stdout: "",
|
|
18155
|
+
stderr: `gh pr checkout: ${hostMainRepo} is on registered box branch ${currentBranch}; refusing (would corrupt the bind-mounted box HEAD)
|
|
18156
|
+
`
|
|
18157
|
+
};
|
|
18158
|
+
}
|
|
18159
|
+
return null;
|
|
18160
|
+
}
|
|
18161
|
+
function runGitProbe(args) {
|
|
18162
|
+
return new Promise((resolve2) => {
|
|
18163
|
+
const child = (0, import_node_child_process6.spawn)("git", args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
18164
|
+
let stdout = "";
|
|
18165
|
+
let stderr = "";
|
|
18166
|
+
child.stdout?.on("data", (c3) => {
|
|
18167
|
+
stdout += c3.toString("utf8");
|
|
18168
|
+
});
|
|
18169
|
+
child.stderr?.on("data", (c3) => {
|
|
18170
|
+
stderr += c3.toString("utf8");
|
|
18171
|
+
});
|
|
18172
|
+
child.on("error", (err) => {
|
|
18173
|
+
resolve2({ exitCode: 127, stdout, stderr: stderr + String(err.message ?? err) });
|
|
18174
|
+
});
|
|
18175
|
+
child.on("close", (code) => {
|
|
18176
|
+
resolve2({ exitCode: code ?? -1, stdout, stderr });
|
|
18177
|
+
});
|
|
18178
|
+
});
|
|
18179
|
+
}
|
|
18180
|
+
function refuseMergeBypass(op) {
|
|
18181
|
+
if (op !== "merge") return null;
|
|
18182
|
+
if (process.env["AGENTBOX_PROMPT"] !== "off") return null;
|
|
18183
|
+
if (process.env["AGENTBOX_GH_FORCE"] === "1") return null;
|
|
18184
|
+
return {
|
|
18185
|
+
exitCode: 10,
|
|
18186
|
+
stdout: "",
|
|
18187
|
+
stderr: "gh pr merge: AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1 (merge is irreversible)\n"
|
|
18188
|
+
};
|
|
18189
|
+
}
|
|
18190
|
+
function refuseCheckoutByDefault(op) {
|
|
18191
|
+
if (op !== "checkout") return null;
|
|
18192
|
+
if (process.env["AGENTBOX_GH_PR_CHECKOUT"] === "allow") return null;
|
|
18193
|
+
return {
|
|
18194
|
+
exitCode: 13,
|
|
18195
|
+
stdout: "",
|
|
18196
|
+
stderr: "gh pr checkout: disabled by default; set AGENTBOX_GH_PR_CHECKOUT=allow to enable\n"
|
|
18197
|
+
};
|
|
18198
|
+
}
|
|
18199
|
+
|
|
18200
|
+
// src/host-initiated.ts
|
|
17977
18201
|
var import_node_crypto = require("crypto");
|
|
18202
|
+
var DEFAULT_TTL_MS = 12e4;
|
|
18203
|
+
function hashRpcParams(params) {
|
|
18204
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonicalJson(params)).digest("hex");
|
|
18205
|
+
}
|
|
18206
|
+
function canonicalJson(v) {
|
|
18207
|
+
if (v === null) return "null";
|
|
18208
|
+
if (typeof v === "undefined") return "null";
|
|
18209
|
+
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
|
|
18210
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
18211
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
18212
|
+
if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
|
|
18213
|
+
if (typeof v === "object") {
|
|
18214
|
+
const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a2], [b]) => a2 < b ? -1 : a2 > b ? 1 : 0);
|
|
18215
|
+
return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
|
|
18216
|
+
}
|
|
18217
|
+
return "null";
|
|
18218
|
+
}
|
|
18219
|
+
var HostInitiatedTokens = class {
|
|
18220
|
+
store = /* @__PURE__ */ new Map();
|
|
18221
|
+
/**
|
|
18222
|
+
* Mint a fresh one-time token scoped to (boxId, method, paramsHash).
|
|
18223
|
+
* `paramsHash` MUST be supplied for any call surface where the box can
|
|
18224
|
+
* influence the eventual RPC params. Pass `null` only when there are no
|
|
18225
|
+
* params (no current call sites use this).
|
|
18226
|
+
*/
|
|
18227
|
+
mint(boxId, method, paramsHash, ttlMs = DEFAULT_TTL_MS) {
|
|
18228
|
+
const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
|
|
18229
|
+
this.store.set(token, { boxId, method, paramsHash, expiresAt: Date.now() + ttlMs });
|
|
18230
|
+
return token;
|
|
18231
|
+
}
|
|
18232
|
+
/**
|
|
18233
|
+
* Returns true exactly once if `token` is a valid, unexpired token for the
|
|
18234
|
+
* given `(boxId, method)` AND the supplied `incomingParamsHash` matches
|
|
18235
|
+
* the hash bound at mint time. The token is removed on a successful match
|
|
18236
|
+
* (one-shot semantics). All failure modes return false — callers fall back
|
|
18237
|
+
* to the normal prompt path.
|
|
18238
|
+
*/
|
|
18239
|
+
consume(token, boxId, method, incomingParamsHash) {
|
|
18240
|
+
if (!token || typeof token !== "string") return false;
|
|
18241
|
+
const record = this.store.get(token);
|
|
18242
|
+
if (!record) return false;
|
|
18243
|
+
if (record.expiresAt < Date.now()) {
|
|
18244
|
+
this.store.delete(token);
|
|
18245
|
+
return false;
|
|
18246
|
+
}
|
|
18247
|
+
if (record.boxId !== boxId || record.method !== method) return false;
|
|
18248
|
+
if (record.paramsHash !== null && record.paramsHash !== incomingParamsHash) {
|
|
18249
|
+
return false;
|
|
18250
|
+
}
|
|
18251
|
+
this.store.delete(token);
|
|
18252
|
+
return true;
|
|
18253
|
+
}
|
|
18254
|
+
/** Drop expired entries. Cheap; safe to call periodically. */
|
|
18255
|
+
gc() {
|
|
18256
|
+
const now = Date.now();
|
|
18257
|
+
for (const [token, record] of this.store) {
|
|
18258
|
+
if (record.expiresAt < now) this.store.delete(token);
|
|
18259
|
+
}
|
|
18260
|
+
}
|
|
18261
|
+
/** Test-only: number of live tokens. */
|
|
18262
|
+
size() {
|
|
18263
|
+
return this.store.size;
|
|
18264
|
+
}
|
|
18265
|
+
};
|
|
18266
|
+
|
|
18267
|
+
// src/prompts.ts
|
|
18268
|
+
var import_node_crypto2 = require("crypto");
|
|
17978
18269
|
var PendingPrompts = class {
|
|
17979
18270
|
entries = /* @__PURE__ */ new Map();
|
|
17980
18271
|
add(boxId, ev) {
|
|
@@ -18063,7 +18354,7 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
|
|
|
18063
18354
|
if (process.env.AGENTBOX_PROMPT === "off") {
|
|
18064
18355
|
return { answer: "y" };
|
|
18065
18356
|
}
|
|
18066
|
-
const ev = { id: (0,
|
|
18357
|
+
const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
|
|
18067
18358
|
const promise = prompts.add(boxId, ev);
|
|
18068
18359
|
subscribers.broadcast(boxId, "prompt-ask", ev);
|
|
18069
18360
|
if (opts?.ttlMs !== void 0 && opts.ttlMs > 0) {
|
|
@@ -18122,6 +18413,18 @@ async function resolveCloudBackend(name) {
|
|
|
18122
18413
|
}
|
|
18123
18414
|
throw new Error(`no host executor for cloud backend '${name}'`);
|
|
18124
18415
|
}
|
|
18416
|
+
async function refreshCloudPreviewUrl(backendName, boxId, port) {
|
|
18417
|
+
try {
|
|
18418
|
+
const backend = await resolveCloudBackend(backendName);
|
|
18419
|
+
if (!backend.refreshPreviewUrl) return null;
|
|
18420
|
+
const lookup = await lookupCloudBox(boxId);
|
|
18421
|
+
const handle = { sandboxId: lookup.cloudSandboxId };
|
|
18422
|
+
const url = await backend.refreshPreviewUrl(handle, port);
|
|
18423
|
+
return url.url;
|
|
18424
|
+
} catch {
|
|
18425
|
+
return null;
|
|
18426
|
+
}
|
|
18427
|
+
}
|
|
18125
18428
|
async function executeCloudAction(action, deps) {
|
|
18126
18429
|
const log = deps.log ?? (() => {
|
|
18127
18430
|
});
|
|
@@ -18141,6 +18444,17 @@ async function executeCloudAction(action, deps) {
|
|
|
18141
18444
|
if (action.method === "browser.open.mirror") {
|
|
18142
18445
|
return runBrowserOpenMirror(action, deps);
|
|
18143
18446
|
}
|
|
18447
|
+
if (action.method.startsWith("gh.pr.")) {
|
|
18448
|
+
return runGhPrRpc(action, deps);
|
|
18449
|
+
}
|
|
18450
|
+
if (action.method === "git.clone" || action.method === "gh.repo.clone") {
|
|
18451
|
+
return {
|
|
18452
|
+
exitCode: 64,
|
|
18453
|
+
stdout: "",
|
|
18454
|
+
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.
|
|
18455
|
+
`
|
|
18456
|
+
};
|
|
18457
|
+
}
|
|
18144
18458
|
return {
|
|
18145
18459
|
exitCode: 1,
|
|
18146
18460
|
stdout: "",
|
|
@@ -18148,6 +18462,86 @@ async function executeCloudAction(action, deps) {
|
|
|
18148
18462
|
`
|
|
18149
18463
|
};
|
|
18150
18464
|
}
|
|
18465
|
+
async function runGhPrRpc(action, deps) {
|
|
18466
|
+
const op = action.method.slice("gh.pr.".length);
|
|
18467
|
+
if (!isGhPrOp(op)) {
|
|
18468
|
+
return {
|
|
18469
|
+
exitCode: 64,
|
|
18470
|
+
stdout: "",
|
|
18471
|
+
stderr: `unknown gh.pr.* op: ${op}
|
|
18472
|
+
`
|
|
18473
|
+
};
|
|
18474
|
+
}
|
|
18475
|
+
const mergeBypass = refuseMergeBypass(op);
|
|
18476
|
+
if (mergeBypass) return mergeBypass;
|
|
18477
|
+
const checkoutOptIn = refuseCheckoutByDefault(op);
|
|
18478
|
+
if (checkoutOptIn) return checkoutOptIn;
|
|
18479
|
+
const params = action.params ?? {};
|
|
18480
|
+
const args = Array.isArray(params.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
|
|
18481
|
+
const ghReady = await assertGhReady();
|
|
18482
|
+
if (ghReady) return ghReady;
|
|
18483
|
+
const lookup = await lookupCloudBox(deps.boxId);
|
|
18484
|
+
if (op === "checkout") {
|
|
18485
|
+
const guard = await checkoutGuards(lookup.workspacePath, []);
|
|
18486
|
+
if (guard) return guard;
|
|
18487
|
+
}
|
|
18488
|
+
const tokenClaimedGhCloud = typeof params.hostInitiated === "string";
|
|
18489
|
+
const incomingHashGhCloud = hashRpcParams(params);
|
|
18490
|
+
const hostInitiatedGhOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && (deps.hostInitiatedTokens?.consume(
|
|
18491
|
+
params.hostInitiated,
|
|
18492
|
+
deps.boxId,
|
|
18493
|
+
`gh.pr.${op}`,
|
|
18494
|
+
incomingHashGhCloud
|
|
18495
|
+
) ?? false);
|
|
18496
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && !hostInitiatedGhOk) {
|
|
18497
|
+
return {
|
|
18498
|
+
exitCode: 10,
|
|
18499
|
+
stdout: "",
|
|
18500
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
18501
|
+
};
|
|
18502
|
+
}
|
|
18503
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedGhOk && deps.prompts && deps.subscribers) {
|
|
18504
|
+
const detail = args.join(" ").slice(0, 200);
|
|
18505
|
+
const ctx = {
|
|
18506
|
+
kind: "confirm",
|
|
18507
|
+
message: `Allow gh pr ${op} from cloud box ${deps.boxName ?? deps.boxId}?`,
|
|
18508
|
+
detail,
|
|
18509
|
+
defaultAnswer: "n",
|
|
18510
|
+
context: {
|
|
18511
|
+
command: `gh pr ${op}`,
|
|
18512
|
+
cwd: params.path,
|
|
18513
|
+
argv: args
|
|
18514
|
+
}
|
|
18515
|
+
};
|
|
18516
|
+
const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
|
|
18517
|
+
if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
|
|
18518
|
+
const noSubMode = (process.env["AGENTBOX_GH_NO_SUB"] ?? "deny").toLowerCase();
|
|
18519
|
+
if (noSubMode === "deny") {
|
|
18520
|
+
return {
|
|
18521
|
+
exitCode: 10,
|
|
18522
|
+
stdout: "",
|
|
18523
|
+
stderr: "denied automatically \u2014 no attached wrapper to confirm. Attach `agentbox claude` (or similar) and retry, or set AGENTBOX_GH_NO_SUB=allow.\n"
|
|
18524
|
+
};
|
|
18525
|
+
}
|
|
18526
|
+
if (noSubMode === "allow") {
|
|
18527
|
+
deps.log?.(`gh.pr.${op} auto-approved (no subscribers, AGENTBOX_GH_NO_SUB=allow)`);
|
|
18528
|
+
} else {
|
|
18529
|
+
const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx, {
|
|
18530
|
+
ttlMs: 5 * 60 * 1e3
|
|
18531
|
+
});
|
|
18532
|
+
if (verdict.answer !== "y") {
|
|
18533
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
18534
|
+
}
|
|
18535
|
+
}
|
|
18536
|
+
} else {
|
|
18537
|
+
const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx);
|
|
18538
|
+
if (verdict.answer !== "y") {
|
|
18539
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
18540
|
+
}
|
|
18541
|
+
}
|
|
18542
|
+
}
|
|
18543
|
+
return runHostGh(["pr", op, ...args], lookup.workspacePath);
|
|
18544
|
+
}
|
|
18151
18545
|
async function runBrowserOpenMirror(action, deps) {
|
|
18152
18546
|
const params = action.params ?? {};
|
|
18153
18547
|
const url = typeof params.url === "string" ? params.url.trim() : "";
|
|
@@ -18174,8 +18568,8 @@ async function runBrowserOpenMirror(action, deps) {
|
|
|
18174
18568
|
{ ttlMs: TTL_MS }
|
|
18175
18569
|
);
|
|
18176
18570
|
if (verdict.answer === "y" && !verdict.cancelled) {
|
|
18177
|
-
const { spawn:
|
|
18178
|
-
const child =
|
|
18571
|
+
const { spawn: spawn6 } = await import("child_process");
|
|
18572
|
+
const child = spawn6("open", [url], { stdio: "ignore", detached: true });
|
|
18179
18573
|
child.unref();
|
|
18180
18574
|
}
|
|
18181
18575
|
} catch (err) {
|
|
@@ -18346,7 +18740,23 @@ async function runGitRpc(action, deps) {
|
|
|
18346
18740
|
stderr: `failed to resolve branch in sandbox ${containerPath}: ${branchProbe.stderr || branch}`
|
|
18347
18741
|
};
|
|
18348
18742
|
}
|
|
18349
|
-
|
|
18743
|
+
const isAgentboxBranch = branch.startsWith("agentbox/");
|
|
18744
|
+
const tokenClaimedGit = typeof params.hostInitiated === "string";
|
|
18745
|
+
const incomingHashGit = hashRpcParams(params);
|
|
18746
|
+
const hostInitiatedOk = !isAgentboxBranch && tokenClaimedGit && (deps.hostInitiatedTokens?.consume(
|
|
18747
|
+
params.hostInitiated,
|
|
18748
|
+
deps.boxId,
|
|
18749
|
+
"git.push",
|
|
18750
|
+
incomingHashGit
|
|
18751
|
+
) ?? false);
|
|
18752
|
+
if (action.method === "git.push" && !isAgentboxBranch && tokenClaimedGit && !hostInitiatedOk) {
|
|
18753
|
+
return {
|
|
18754
|
+
exitCode: 10,
|
|
18755
|
+
stdout: "",
|
|
18756
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
18757
|
+
};
|
|
18758
|
+
}
|
|
18759
|
+
if (action.method === "git.push" && !isAgentboxBranch && !hostInitiatedOk && deps.prompts && deps.subscribers) {
|
|
18350
18760
|
const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
|
|
18351
18761
|
if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
|
|
18352
18762
|
const noSubMode = (process.env["AGENTBOX_GIT_PUSH_NO_SUB"] ?? "deny").toLowerCase();
|
|
@@ -18421,10 +18831,45 @@ async function runGitRpc(action, deps) {
|
|
|
18421
18831
|
for (const a2 of params.args) if (typeof a2 === "string") argv.push(a2);
|
|
18422
18832
|
}
|
|
18423
18833
|
const push = await execa("git", argv, { reject: false });
|
|
18834
|
+
let pushStderr = push.stderr ?? "";
|
|
18835
|
+
if ((push.exitCode ?? 1) === 0 && !branch.startsWith("agentbox/")) {
|
|
18836
|
+
try {
|
|
18837
|
+
const sha = await execa(
|
|
18838
|
+
"git",
|
|
18839
|
+
["-C", lookup.workspacePath, "rev-parse", branch],
|
|
18840
|
+
{ reject: false }
|
|
18841
|
+
);
|
|
18842
|
+
const shaText = (sha.stdout ?? "").trim();
|
|
18843
|
+
if (sha.exitCode === 0 && shaText.length > 0) {
|
|
18844
|
+
const updateRef = await backend.exec(
|
|
18845
|
+
handle,
|
|
18846
|
+
`git -C ${shellQuote(containerPath)} update-ref refs/remotes/${remote2}/${branch} ${shellQuote(shaText)}`
|
|
18847
|
+
);
|
|
18848
|
+
if (updateRef.exitCode !== 0) {
|
|
18849
|
+
pushStderr += `
|
|
18850
|
+
relay: post-push in-box update-ref refs/remotes/${remote2}/${branch} failed: ${updateRef.stderr || updateRef.stdout}`;
|
|
18851
|
+
}
|
|
18852
|
+
const setUpstream = await backend.exec(
|
|
18853
|
+
handle,
|
|
18854
|
+
`git -C ${shellQuote(containerPath)} branch --set-upstream-to=${remote2}/${branch} ${shellQuote(branch)}`
|
|
18855
|
+
);
|
|
18856
|
+
if (setUpstream.exitCode !== 0) {
|
|
18857
|
+
pushStderr += `
|
|
18858
|
+
relay: post-push in-box --set-upstream-to=${remote2}/${branch} failed: ${setUpstream.stderr || setUpstream.stdout}`;
|
|
18859
|
+
}
|
|
18860
|
+
} else {
|
|
18861
|
+
pushStderr += `
|
|
18862
|
+
relay: post-push rev-parse ${branch} failed on host; skipping in-box origin/upstream sync`;
|
|
18863
|
+
}
|
|
18864
|
+
} catch (err) {
|
|
18865
|
+
pushStderr += `
|
|
18866
|
+
relay: post-push in-box origin/upstream sync threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
18867
|
+
}
|
|
18868
|
+
}
|
|
18424
18869
|
return {
|
|
18425
18870
|
exitCode: push.exitCode ?? 1,
|
|
18426
18871
|
stdout: push.stdout ?? "",
|
|
18427
|
-
stderr:
|
|
18872
|
+
stderr: pushStderr
|
|
18428
18873
|
};
|
|
18429
18874
|
}
|
|
18430
18875
|
const remote = params.remote ?? "origin";
|
|
@@ -18471,7 +18916,7 @@ function shellQuote(arg) {
|
|
|
18471
18916
|
}
|
|
18472
18917
|
|
|
18473
18918
|
// src/host-action-queue.ts
|
|
18474
|
-
var
|
|
18919
|
+
var import_node_crypto3 = require("crypto");
|
|
18475
18920
|
var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
|
|
18476
18921
|
var HostActionQueue = class {
|
|
18477
18922
|
map = /* @__PURE__ */ new Map();
|
|
@@ -18489,7 +18934,7 @@ var HostActionQueue = class {
|
|
|
18489
18934
|
* is open" semantics).
|
|
18490
18935
|
*/
|
|
18491
18936
|
enqueue(boxId, method, params) {
|
|
18492
|
-
const id = (0,
|
|
18937
|
+
const id = (0, import_node_crypto3.randomUUID)();
|
|
18493
18938
|
const action = {
|
|
18494
18939
|
id,
|
|
18495
18940
|
boxId,
|
|
@@ -18555,7 +19000,7 @@ var HostActionQueue = class {
|
|
|
18555
19000
|
};
|
|
18556
19001
|
|
|
18557
19002
|
// src/notices.ts
|
|
18558
|
-
var
|
|
19003
|
+
var import_node_crypto4 = require("crypto");
|
|
18559
19004
|
var DEFAULT_NOTICE_TTL_MS = 66e4;
|
|
18560
19005
|
var BoxNotices = class {
|
|
18561
19006
|
constructor(subscribers) {
|
|
@@ -18578,7 +19023,7 @@ var BoxNotices = class {
|
|
|
18578
19023
|
this.entries.delete(id);
|
|
18579
19024
|
}
|
|
18580
19025
|
}
|
|
18581
|
-
const ev = { id: (0,
|
|
19026
|
+
const ev = { id: (0, import_node_crypto4.randomUUID)(), kind, message };
|
|
18582
19027
|
const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;
|
|
18583
19028
|
const timer = setTimeout(() => {
|
|
18584
19029
|
if (this.entries.delete(ev.id)) {
|
|
@@ -18796,6 +19241,8 @@ function createRelayServer(opts) {
|
|
|
18796
19241
|
const prompts = new PendingPrompts();
|
|
18797
19242
|
const subscribers = new PromptSubscribers();
|
|
18798
19243
|
const notices = new BoxNotices(subscribers);
|
|
19244
|
+
const hostInitiatedTokens = new HostInitiatedTokens();
|
|
19245
|
+
let queuePoke = null;
|
|
18799
19246
|
const host = opts.host ?? "0.0.0.0";
|
|
18800
19247
|
const mode = opts.mode ?? "host";
|
|
18801
19248
|
const hostActions = mode === "box" ? new HostActionQueue() : null;
|
|
@@ -18928,21 +19375,36 @@ function createRelayServer(opts) {
|
|
|
18928
19375
|
if (body.method === "git.push" || body.method === "git.fetch") {
|
|
18929
19376
|
if (body.method === "git.push") {
|
|
18930
19377
|
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" });
|
|
19378
|
+
const worktree = resolveWorktree(reg, params?.path ?? "/workspace");
|
|
19379
|
+
const isAgentboxBranch = worktree?.branch.startsWith("agentbox/") ?? false;
|
|
19380
|
+
const tokenClaimed = typeof params?.hostInitiated === "string";
|
|
19381
|
+
const incomingHash = hashRpcParams(params);
|
|
19382
|
+
const hostInitiatedOk = !isAgentboxBranch && tokenClaimed && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, "git.push", incomingHash);
|
|
19383
|
+
if (!isAgentboxBranch && tokenClaimed && !hostInitiatedOk) {
|
|
19384
|
+
send(res, 500, {
|
|
19385
|
+
exitCode: 10,
|
|
19386
|
+
stdout: "",
|
|
19387
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
19388
|
+
});
|
|
18944
19389
|
return;
|
|
18945
19390
|
}
|
|
19391
|
+
if (!isAgentboxBranch && !hostInitiatedOk) {
|
|
19392
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
19393
|
+
kind: "confirm",
|
|
19394
|
+
message: `Allow git push from box ${reg.name}?`,
|
|
19395
|
+
detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
|
|
19396
|
+
defaultAnswer: "n",
|
|
19397
|
+
context: {
|
|
19398
|
+
command: "git push",
|
|
19399
|
+
cwd: params?.path,
|
|
19400
|
+
argv: params?.args
|
|
19401
|
+
}
|
|
19402
|
+
});
|
|
19403
|
+
if (verdict.answer !== "y") {
|
|
19404
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
19405
|
+
return;
|
|
19406
|
+
}
|
|
19407
|
+
}
|
|
18946
19408
|
}
|
|
18947
19409
|
const result = await handleGitRpc(reg, body.method, body.params);
|
|
18948
19410
|
const status = result.exitCode === 0 ? 200 : 500;
|
|
@@ -18975,6 +19437,33 @@ function createRelayServer(opts) {
|
|
|
18975
19437
|
send(res, status, result);
|
|
18976
19438
|
return;
|
|
18977
19439
|
}
|
|
19440
|
+
if (body.method.startsWith("gh.pr.")) {
|
|
19441
|
+
const op = body.method.slice("gh.pr.".length);
|
|
19442
|
+
if (!isGhPrOp(op)) {
|
|
19443
|
+
send(res, 400, { error: `unknown gh.pr.* op: ${op}` });
|
|
19444
|
+
return;
|
|
19445
|
+
}
|
|
19446
|
+
const result = await handleGhPrRpc(
|
|
19447
|
+
op,
|
|
19448
|
+
reg,
|
|
19449
|
+
body.params,
|
|
19450
|
+
prompts,
|
|
19451
|
+
subscribers,
|
|
19452
|
+
hostInitiatedTokens
|
|
19453
|
+
);
|
|
19454
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
19455
|
+
send(res, status, result);
|
|
19456
|
+
return;
|
|
19457
|
+
}
|
|
19458
|
+
if (body.method === "git.clone" || body.method === "gh.repo.clone") {
|
|
19459
|
+
send(res, 501, {
|
|
19460
|
+
exitCode: 64,
|
|
19461
|
+
stdout: "",
|
|
19462
|
+
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.
|
|
19463
|
+
`
|
|
19464
|
+
});
|
|
19465
|
+
return;
|
|
19466
|
+
}
|
|
18978
19467
|
if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
|
|
18979
19468
|
const params = body.params;
|
|
18980
19469
|
const kind = body.method.split(".")[1] ?? "workspace";
|
|
@@ -19111,6 +19600,7 @@ function createRelayServer(opts) {
|
|
|
19111
19600
|
boxName: reg.name,
|
|
19112
19601
|
prompts,
|
|
19113
19602
|
subscribers,
|
|
19603
|
+
hostInitiatedTokens,
|
|
19114
19604
|
log
|
|
19115
19605
|
});
|
|
19116
19606
|
await respond(result);
|
|
@@ -19123,6 +19613,11 @@ function createRelayServer(opts) {
|
|
|
19123
19613
|
});
|
|
19124
19614
|
}
|
|
19125
19615
|
} : void 0,
|
|
19616
|
+
// Self-heal a dead preview transport (hetzner SSH `-L` after a
|
|
19617
|
+
// ControlMaster death). The relay strips the `cloud:` prefix
|
|
19618
|
+
// the cloud-provider tags onto BoxRecord.container — what the
|
|
19619
|
+
// backend's `get(sandboxId)` expects is the bare sandbox id.
|
|
19620
|
+
recoverPreviewUrl: reg.backend ? async () => refreshCloudPreviewUrl(reg.backend, reg.boxId, DEFAULT_BOX_RELAY_PORT) : void 0,
|
|
19126
19621
|
logger: log
|
|
19127
19622
|
});
|
|
19128
19623
|
} catch (err) {
|
|
@@ -19237,6 +19732,27 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19237
19732
|
send(res, 204, null);
|
|
19238
19733
|
return;
|
|
19239
19734
|
}
|
|
19735
|
+
if (route === "POST /admin/host-initiated/mint") {
|
|
19736
|
+
const body = await readJsonBody(req);
|
|
19737
|
+
if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.method !== "string" || body.method.length === 0) {
|
|
19738
|
+
send(res, 400, { error: "expected {boxId, method, paramsHash, ttlMs?}" });
|
|
19739
|
+
return;
|
|
19740
|
+
}
|
|
19741
|
+
let paramsHash;
|
|
19742
|
+
if (body.paramsHash === null || body.paramsHash === void 0) {
|
|
19743
|
+
paramsHash = null;
|
|
19744
|
+
} else if (typeof body.paramsHash === "string" && /^[0-9a-f]{64}$/.test(body.paramsHash)) {
|
|
19745
|
+
paramsHash = body.paramsHash;
|
|
19746
|
+
} else {
|
|
19747
|
+
send(res, 400, { error: "paramsHash must be a 64-hex sha256 string or null" });
|
|
19748
|
+
return;
|
|
19749
|
+
}
|
|
19750
|
+
const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
|
|
19751
|
+
const token = hostInitiatedTokens.mint(body.boxId, body.method, paramsHash, ttlMs);
|
|
19752
|
+
log(`host-initiated-mint box=${body.boxId} method=${body.method} paramsBound=${paramsHash !== null}`);
|
|
19753
|
+
send(res, 200, { token });
|
|
19754
|
+
return;
|
|
19755
|
+
}
|
|
19240
19756
|
if (route === "POST /admin/notices/set") {
|
|
19241
19757
|
const body = await readJsonBody(req);
|
|
19242
19758
|
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 +19765,17 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19249
19765
|
send(res, 200, { id });
|
|
19250
19766
|
return;
|
|
19251
19767
|
}
|
|
19768
|
+
if (route === "POST /admin/queue/enqueue") {
|
|
19769
|
+
const body = await readJsonBody(req);
|
|
19770
|
+
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
19771
|
+
send(res, 400, { error: "expected {id}" });
|
|
19772
|
+
return;
|
|
19773
|
+
}
|
|
19774
|
+
log(`queue-enqueue id=${body.id}`);
|
|
19775
|
+
queuePoke?.();
|
|
19776
|
+
send(res, 204, null);
|
|
19777
|
+
return;
|
|
19778
|
+
}
|
|
19252
19779
|
if (route === "POST /admin/notices/clear") {
|
|
19253
19780
|
const body = await readJsonBody(req);
|
|
19254
19781
|
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
@@ -19285,6 +19812,9 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19285
19812
|
notices,
|
|
19286
19813
|
hostActions: hostActions ?? void 0,
|
|
19287
19814
|
url: `http://${host}:${String(opts.port)}`,
|
|
19815
|
+
setQueuePoke: (fn) => {
|
|
19816
|
+
queuePoke = fn;
|
|
19817
|
+
},
|
|
19288
19818
|
close: async () => {
|
|
19289
19819
|
if (pollers) await pollers.stopAll();
|
|
19290
19820
|
await new Promise((resolve2, reject) => {
|
|
@@ -19336,7 +19866,69 @@ async function handleGitRpc(reg, method, params) {
|
|
|
19336
19866
|
if (typeof a2 === "string") argv.push(a2);
|
|
19337
19867
|
}
|
|
19338
19868
|
}
|
|
19339
|
-
|
|
19869
|
+
const result = await runHostCommand(argv);
|
|
19870
|
+
if (method === "git.push" && result.exitCode === 0 && !worktree.branch.startsWith("agentbox/")) {
|
|
19871
|
+
await runHostCommand([
|
|
19872
|
+
"git",
|
|
19873
|
+
"-C",
|
|
19874
|
+
worktree.hostMainRepo,
|
|
19875
|
+
"branch",
|
|
19876
|
+
`--set-upstream-to=${remote}/${worktree.branch}`,
|
|
19877
|
+
worktree.branch
|
|
19878
|
+
]);
|
|
19879
|
+
}
|
|
19880
|
+
return result;
|
|
19881
|
+
}
|
|
19882
|
+
async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiatedTokens) {
|
|
19883
|
+
const mergeBypass = refuseMergeBypass(op);
|
|
19884
|
+
if (mergeBypass) return mergeBypass;
|
|
19885
|
+
const checkoutOptIn = refuseCheckoutByDefault(op);
|
|
19886
|
+
if (checkoutOptIn) return checkoutOptIn;
|
|
19887
|
+
const containerPath = params?.path ?? "/workspace";
|
|
19888
|
+
const worktree = resolveWorktree(reg, containerPath);
|
|
19889
|
+
if (!worktree) {
|
|
19890
|
+
return {
|
|
19891
|
+
exitCode: 64,
|
|
19892
|
+
stdout: "",
|
|
19893
|
+
stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
|
|
19894
|
+
};
|
|
19895
|
+
}
|
|
19896
|
+
const ghReady = await assertGhReady();
|
|
19897
|
+
if (ghReady) return ghReady;
|
|
19898
|
+
const args = Array.isArray(params?.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
|
|
19899
|
+
if (op === "checkout") {
|
|
19900
|
+
const branches = (reg.worktrees ?? []).map((w) => w.branch);
|
|
19901
|
+
const guard = await checkoutGuards(worktree.hostMainRepo, branches);
|
|
19902
|
+
if (guard) return guard;
|
|
19903
|
+
}
|
|
19904
|
+
const tokenClaimedGh = typeof params?.hostInitiated === "string";
|
|
19905
|
+
const incomingHashGh = hashRpcParams(params);
|
|
19906
|
+
const hostInitiatedOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, `gh.pr.${op}`, incomingHashGh);
|
|
19907
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && !hostInitiatedOk) {
|
|
19908
|
+
return {
|
|
19909
|
+
exitCode: 10,
|
|
19910
|
+
stdout: "",
|
|
19911
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
19912
|
+
};
|
|
19913
|
+
}
|
|
19914
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedOk) {
|
|
19915
|
+
const detail = args.join(" ").slice(0, 200);
|
|
19916
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
19917
|
+
kind: "confirm",
|
|
19918
|
+
message: `Allow gh pr ${op} from box ${reg.name}?`,
|
|
19919
|
+
detail,
|
|
19920
|
+
defaultAnswer: "n",
|
|
19921
|
+
context: {
|
|
19922
|
+
command: `gh pr ${op}`,
|
|
19923
|
+
cwd: containerPath,
|
|
19924
|
+
argv: args
|
|
19925
|
+
}
|
|
19926
|
+
});
|
|
19927
|
+
if (verdict.answer !== "y") {
|
|
19928
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
19929
|
+
}
|
|
19930
|
+
}
|
|
19931
|
+
return runHostGh(["pr", op, ...args], worktree.hostMainRepo);
|
|
19340
19932
|
}
|
|
19341
19933
|
async function handleCpRpc(reg, method, params) {
|
|
19342
19934
|
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
@@ -19397,7 +19989,7 @@ function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
|
|
|
19397
19989
|
resolve2({ exitCode: 64, stdout: "", stderr: "empty command" });
|
|
19398
19990
|
return;
|
|
19399
19991
|
}
|
|
19400
|
-
const child = (0,
|
|
19992
|
+
const child = (0, import_node_child_process7.spawn)(cmd, rest, {
|
|
19401
19993
|
env: process.env,
|
|
19402
19994
|
stdio: ["ignore", "pipe", "pipe"]
|
|
19403
19995
|
});
|
|
@@ -19446,7 +20038,7 @@ async function startRelayServer(opts) {
|
|
|
19446
20038
|
}
|
|
19447
20039
|
|
|
19448
20040
|
// src/autopause.ts
|
|
19449
|
-
var
|
|
20041
|
+
var import_node_child_process8 = require("child_process");
|
|
19450
20042
|
var import_promises16 = require("fs/promises");
|
|
19451
20043
|
|
|
19452
20044
|
// ../config/dist/index.js
|
|
@@ -19475,7 +20067,8 @@ var BUILT_IN_DEFAULTS = {
|
|
|
19475
20067
|
memory: 0,
|
|
19476
20068
|
cpus: 0,
|
|
19477
20069
|
pidsLimit: 0,
|
|
19478
|
-
disk: ""
|
|
20070
|
+
disk: "",
|
|
20071
|
+
bundleDepth: void 0
|
|
19479
20072
|
},
|
|
19480
20073
|
checkpoint: {
|
|
19481
20074
|
maxLayers: 3
|
|
@@ -19524,6 +20117,10 @@ var BUILT_IN_DEFAULTS = {
|
|
|
19524
20117
|
maxRunningBoxes: 5,
|
|
19525
20118
|
idleMinutes: 5
|
|
19526
20119
|
},
|
|
20120
|
+
queue: {
|
|
20121
|
+
enabled: true,
|
|
20122
|
+
maxConcurrent: 5
|
|
20123
|
+
},
|
|
19527
20124
|
maintenance: {
|
|
19528
20125
|
pruneProjectConfigs: true,
|
|
19529
20126
|
pruneProjectConfigsEvery: 50
|
|
@@ -19632,6 +20229,11 @@ var KEY_REGISTRY = [
|
|
|
19632
20229
|
description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
|
|
19633
20230
|
advanced: true
|
|
19634
20231
|
},
|
|
20232
|
+
{
|
|
20233
|
+
key: "box.bundleDepth",
|
|
20234
|
+
type: "int",
|
|
20235
|
+
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
|
+
},
|
|
19635
20237
|
{
|
|
19636
20238
|
key: "claude.sessionName",
|
|
19637
20239
|
type: "string",
|
|
@@ -19739,6 +20341,16 @@ var KEY_REGISTRY = [
|
|
|
19739
20341
|
type: "int",
|
|
19740
20342
|
description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
|
|
19741
20343
|
},
|
|
20344
|
+
{
|
|
20345
|
+
key: "queue.enabled",
|
|
20346
|
+
type: "bool",
|
|
20347
|
+
description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
|
|
20348
|
+
},
|
|
20349
|
+
{
|
|
20350
|
+
key: "queue.maxConcurrent",
|
|
20351
|
+
type: "int",
|
|
20352
|
+
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
|
+
},
|
|
19742
20354
|
{
|
|
19743
20355
|
key: "maintenance.pruneProjectConfigs",
|
|
19744
20356
|
type: "bool",
|
|
@@ -19833,6 +20445,15 @@ function parseUserConfigObject(doc, where) {
|
|
|
19833
20445
|
}
|
|
19834
20446
|
const out = {};
|
|
19835
20447
|
for (const [branchName, branchRaw] of Object.entries(doc)) {
|
|
20448
|
+
if (branchName === "schema") {
|
|
20449
|
+
if (branchRaw !== void 0 && branchRaw !== null) {
|
|
20450
|
+
if (typeof branchRaw !== "number" || !Number.isInteger(branchRaw)) {
|
|
20451
|
+
throw new UserConfigError(`${where}.schema: must be an integer (got ${String(branchRaw)})`);
|
|
20452
|
+
}
|
|
20453
|
+
out.schema = branchRaw;
|
|
20454
|
+
}
|
|
20455
|
+
continue;
|
|
20456
|
+
}
|
|
19836
20457
|
const branchSpec = BRANCHES.get(branchName);
|
|
19837
20458
|
if (!branchSpec) {
|
|
19838
20459
|
throw new UserConfigError(
|
|
@@ -20005,7 +20626,7 @@ var INSPECT_TIMEOUT_MS = 15e3;
|
|
|
20005
20626
|
var PAUSE_TIMEOUT_MS = 3e4;
|
|
20006
20627
|
function runDocker(args, timeoutMs) {
|
|
20007
20628
|
return new Promise((resolve2) => {
|
|
20008
|
-
const child = (0,
|
|
20629
|
+
const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
20009
20630
|
let stdout = "";
|
|
20010
20631
|
let stderr = "";
|
|
20011
20632
|
let settled = false;
|
|
@@ -20063,6 +20684,269 @@ async function pauseContainer(name) {
|
|
|
20063
20684
|
}
|
|
20064
20685
|
}
|
|
20065
20686
|
|
|
20687
|
+
// src/queue.ts
|
|
20688
|
+
var import_node_child_process9 = require("child_process");
|
|
20689
|
+
var import_promises17 = require("fs/promises");
|
|
20690
|
+
var import_node_fs6 = require("fs");
|
|
20691
|
+
var import_node_path8 = require("path");
|
|
20692
|
+
var import_promises18 = require("timers/promises");
|
|
20693
|
+
var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
|
|
20694
|
+
async function loadQueueConfig() {
|
|
20695
|
+
const d = BUILT_IN_DEFAULTS.queue;
|
|
20696
|
+
let global3 = {};
|
|
20697
|
+
try {
|
|
20698
|
+
global3 = parseUserConfig(await (0, import_promises17.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
|
|
20699
|
+
} catch {
|
|
20700
|
+
}
|
|
20701
|
+
const q = global3.queue ?? {};
|
|
20702
|
+
return {
|
|
20703
|
+
enabled: q.enabled ?? d.enabled,
|
|
20704
|
+
maxConcurrent: q.maxConcurrent ?? d.maxConcurrent
|
|
20705
|
+
};
|
|
20706
|
+
}
|
|
20707
|
+
async function writeJob(job) {
|
|
20708
|
+
await (0, import_promises17.mkdir)(QUEUE_DIR, { recursive: true });
|
|
20709
|
+
const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
|
|
20710
|
+
const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
|
|
20711
|
+
await (0, import_promises17.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
|
|
20712
|
+
await (0, import_promises17.rename)(tmp, final);
|
|
20713
|
+
}
|
|
20714
|
+
async function readJob(id) {
|
|
20715
|
+
try {
|
|
20716
|
+
const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
|
|
20717
|
+
return JSON.parse(raw);
|
|
20718
|
+
} catch (err) {
|
|
20719
|
+
if (err.code === "ENOENT") return null;
|
|
20720
|
+
throw err;
|
|
20721
|
+
}
|
|
20722
|
+
}
|
|
20723
|
+
async function loadQueue() {
|
|
20724
|
+
let entries;
|
|
20725
|
+
try {
|
|
20726
|
+
entries = await (0, import_promises17.readdir)(QUEUE_DIR);
|
|
20727
|
+
} catch (err) {
|
|
20728
|
+
if (err.code === "ENOENT") return [];
|
|
20729
|
+
throw err;
|
|
20730
|
+
}
|
|
20731
|
+
const out = [];
|
|
20732
|
+
for (const name of entries) {
|
|
20733
|
+
if (!name.endsWith(".json")) continue;
|
|
20734
|
+
try {
|
|
20735
|
+
const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
|
|
20736
|
+
out.push(JSON.parse(raw));
|
|
20737
|
+
} catch {
|
|
20738
|
+
}
|
|
20739
|
+
}
|
|
20740
|
+
out.sort((a2, b) => a2.createdAt < b.createdAt ? -1 : a2.createdAt > b.createdAt ? 1 : 0);
|
|
20741
|
+
return out;
|
|
20742
|
+
}
|
|
20743
|
+
function selectNextRunnable(jobs, runningCount) {
|
|
20744
|
+
for (const j of jobs) {
|
|
20745
|
+
if (j.status !== "queued") continue;
|
|
20746
|
+
if (runningCount < j.maxConcurrent) return j;
|
|
20747
|
+
}
|
|
20748
|
+
return null;
|
|
20749
|
+
}
|
|
20750
|
+
var DEFAULT_INTERVAL_MS2 = 2e3;
|
|
20751
|
+
function startQueueLoop(deps) {
|
|
20752
|
+
const loadConfig = deps.loadConfig ?? loadQueueConfig;
|
|
20753
|
+
const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
|
|
20754
|
+
const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
|
|
20755
|
+
const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
|
|
20756
|
+
const { log, onStatusChange } = deps;
|
|
20757
|
+
let ticking = false;
|
|
20758
|
+
let stopped = false;
|
|
20759
|
+
let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
|
|
20760
|
+
log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20761
|
+
});
|
|
20762
|
+
async function tick() {
|
|
20763
|
+
if (ticking) return;
|
|
20764
|
+
ticking = true;
|
|
20765
|
+
try {
|
|
20766
|
+
const cfg = await loadConfig();
|
|
20767
|
+
if (!cfg.enabled) return;
|
|
20768
|
+
const jobs = await loadQueue();
|
|
20769
|
+
const hasQueued = jobs.some((j) => j.status === "queued");
|
|
20770
|
+
if (!hasQueued) return;
|
|
20771
|
+
while (!stopped) {
|
|
20772
|
+
const running = await countRunning();
|
|
20773
|
+
const fresh = await loadQueue();
|
|
20774
|
+
const next = selectNextRunnable(fresh, running);
|
|
20775
|
+
if (!next) return;
|
|
20776
|
+
const current = await readJob(next.id);
|
|
20777
|
+
if (!current || current.status !== "queued") continue;
|
|
20778
|
+
const updated = {
|
|
20779
|
+
...current,
|
|
20780
|
+
status: "running",
|
|
20781
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20782
|
+
};
|
|
20783
|
+
await writeJob(updated);
|
|
20784
|
+
onStatusChange?.(updated);
|
|
20785
|
+
try {
|
|
20786
|
+
const pid = await spawnWorker(updated);
|
|
20787
|
+
if (typeof pid === "number") {
|
|
20788
|
+
const withPid = { ...updated, pid };
|
|
20789
|
+
await writeJob(withPid);
|
|
20790
|
+
onStatusChange?.(withPid);
|
|
20791
|
+
log(
|
|
20792
|
+
`queue: started job ${updated.id} (${updated.agent}) as pid ${String(pid)}; running ${String(running + 1)}/${String(updated.maxConcurrent)}`
|
|
20793
|
+
);
|
|
20794
|
+
} else {
|
|
20795
|
+
log(`queue: started job ${updated.id} (${updated.agent}); pid unknown`);
|
|
20796
|
+
}
|
|
20797
|
+
} catch (err) {
|
|
20798
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
20799
|
+
const failed = {
|
|
20800
|
+
...updated,
|
|
20801
|
+
status: "failed",
|
|
20802
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20803
|
+
reason: `worker-spawn-failed: ${msg}`
|
|
20804
|
+
};
|
|
20805
|
+
await writeJob(failed);
|
|
20806
|
+
onStatusChange?.(failed);
|
|
20807
|
+
log(`queue: spawn for job ${updated.id} failed: ${msg}`);
|
|
20808
|
+
}
|
|
20809
|
+
}
|
|
20810
|
+
} catch (err) {
|
|
20811
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
20812
|
+
log(`queue: tick error: ${msg}`);
|
|
20813
|
+
} finally {
|
|
20814
|
+
ticking = false;
|
|
20815
|
+
}
|
|
20816
|
+
}
|
|
20817
|
+
function poke() {
|
|
20818
|
+
if (stopped) return;
|
|
20819
|
+
inFlight = tick();
|
|
20820
|
+
}
|
|
20821
|
+
const handle = {
|
|
20822
|
+
poke,
|
|
20823
|
+
stop: async () => {
|
|
20824
|
+
stopped = true;
|
|
20825
|
+
clearInterval(timer);
|
|
20826
|
+
await inFlight.catch(() => {
|
|
20827
|
+
});
|
|
20828
|
+
}
|
|
20829
|
+
};
|
|
20830
|
+
const timer = setInterval(() => {
|
|
20831
|
+
if (stopped) return;
|
|
20832
|
+
inFlight = tick();
|
|
20833
|
+
}, intervalMs);
|
|
20834
|
+
timer.unref();
|
|
20835
|
+
return handle;
|
|
20836
|
+
}
|
|
20837
|
+
async function recoverOrphanedWorkers(log, onChange) {
|
|
20838
|
+
const jobs = await loadQueue();
|
|
20839
|
+
for (const j of jobs) {
|
|
20840
|
+
if (j.status !== "running") continue;
|
|
20841
|
+
if (typeof j.pid === "number" && processAlive(j.pid)) continue;
|
|
20842
|
+
const failed = {
|
|
20843
|
+
...j,
|
|
20844
|
+
status: "failed",
|
|
20845
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20846
|
+
reason: "worker-died"
|
|
20847
|
+
};
|
|
20848
|
+
await writeJob(failed);
|
|
20849
|
+
onChange?.(failed);
|
|
20850
|
+
log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
|
|
20851
|
+
}
|
|
20852
|
+
}
|
|
20853
|
+
function processAlive(pid) {
|
|
20854
|
+
try {
|
|
20855
|
+
process.kill(pid, 0);
|
|
20856
|
+
return true;
|
|
20857
|
+
} catch {
|
|
20858
|
+
return false;
|
|
20859
|
+
}
|
|
20860
|
+
}
|
|
20861
|
+
var RUNNING_COUNT_CACHE_MS = 3e3;
|
|
20862
|
+
var runningCountCache = null;
|
|
20863
|
+
async function defaultCountRunningBoxes() {
|
|
20864
|
+
const now = Date.now();
|
|
20865
|
+
if (runningCountCache && runningCountCache.expiresAt > now) {
|
|
20866
|
+
return runningCountCache.value;
|
|
20867
|
+
}
|
|
20868
|
+
const value = await uncachedCountRunningBoxes();
|
|
20869
|
+
runningCountCache = { value, expiresAt: now + RUNNING_COUNT_CACHE_MS };
|
|
20870
|
+
return value;
|
|
20871
|
+
}
|
|
20872
|
+
async function uncachedCountRunningBoxes() {
|
|
20873
|
+
let boxes;
|
|
20874
|
+
try {
|
|
20875
|
+
boxes = (await readState(STATE_FILE)).boxes;
|
|
20876
|
+
} catch {
|
|
20877
|
+
return 0;
|
|
20878
|
+
}
|
|
20879
|
+
if (boxes.length === 0) return 0;
|
|
20880
|
+
let count2 = 0;
|
|
20881
|
+
const dockerBoxes = [];
|
|
20882
|
+
for (const b of boxes) {
|
|
20883
|
+
const provider = b.provider ?? "docker";
|
|
20884
|
+
if (provider === "docker") {
|
|
20885
|
+
dockerBoxes.push(b);
|
|
20886
|
+
} else {
|
|
20887
|
+
count2 += 1;
|
|
20888
|
+
}
|
|
20889
|
+
}
|
|
20890
|
+
if (dockerBoxes.length > 0) {
|
|
20891
|
+
const states = await Promise.all(dockerBoxes.map((b) => inspectDockerState(b.container)));
|
|
20892
|
+
for (const s of states) {
|
|
20893
|
+
if (s === "running") count2 += 1;
|
|
20894
|
+
}
|
|
20895
|
+
}
|
|
20896
|
+
return count2;
|
|
20897
|
+
}
|
|
20898
|
+
function inspectDockerState(containerName) {
|
|
20899
|
+
return new Promise((resolveP) => {
|
|
20900
|
+
const child = (0, import_node_child_process9.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
|
|
20901
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
20902
|
+
});
|
|
20903
|
+
let out = "";
|
|
20904
|
+
let settled = false;
|
|
20905
|
+
const finish = (state) => {
|
|
20906
|
+
if (settled) return;
|
|
20907
|
+
settled = true;
|
|
20908
|
+
resolveP(state);
|
|
20909
|
+
};
|
|
20910
|
+
const timer = setTimeout(() => {
|
|
20911
|
+
child.kill("SIGTERM");
|
|
20912
|
+
finish("other");
|
|
20913
|
+
}, 1e4);
|
|
20914
|
+
child.stdout?.on("data", (c3) => {
|
|
20915
|
+
out += c3.toString("utf8");
|
|
20916
|
+
});
|
|
20917
|
+
child.on("error", () => {
|
|
20918
|
+
clearTimeout(timer);
|
|
20919
|
+
finish("other");
|
|
20920
|
+
});
|
|
20921
|
+
child.on("close", () => {
|
|
20922
|
+
clearTimeout(timer);
|
|
20923
|
+
finish(out.trim() === "running" ? "running" : "other");
|
|
20924
|
+
});
|
|
20925
|
+
});
|
|
20926
|
+
}
|
|
20927
|
+
async function defaultSpawnWorker(job) {
|
|
20928
|
+
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
20929
|
+
if (!entry || !(0, import_node_fs6.existsSync)(entry)) {
|
|
20930
|
+
throw new Error(
|
|
20931
|
+
`AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
|
|
20932
|
+
);
|
|
20933
|
+
}
|
|
20934
|
+
await (0, import_promises17.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
|
|
20935
|
+
const fd = (0, import_node_fs6.openSync)(job.logPath, "a");
|
|
20936
|
+
const child = (0, import_node_child_process9.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
|
|
20937
|
+
detached: true,
|
|
20938
|
+
stdio: ["ignore", fd, fd],
|
|
20939
|
+
env: process.env
|
|
20940
|
+
});
|
|
20941
|
+
child.unref();
|
|
20942
|
+
try {
|
|
20943
|
+
(0, import_node_fs6.closeSync)(fd);
|
|
20944
|
+
} catch {
|
|
20945
|
+
}
|
|
20946
|
+
return typeof child.pid === "number" ? child.pid : null;
|
|
20947
|
+
}
|
|
20948
|
+
var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
|
|
20949
|
+
|
|
20066
20950
|
// src/bin.ts
|
|
20067
20951
|
var program2 = new Command();
|
|
20068
20952
|
program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");
|
|
@@ -20088,10 +20972,17 @@ program2.command("serve").description("Run the HTTP relay in the foreground").op
|
|
|
20088
20972
|
log: (line) => process.stdout.write(`agentbox-relay: ${line}
|
|
20089
20973
|
`)
|
|
20090
20974
|
});
|
|
20975
|
+
const queue = startQueueLoop({
|
|
20976
|
+
log: (line) => process.stdout.write(`agentbox-relay: ${line}
|
|
20977
|
+
`)
|
|
20978
|
+
});
|
|
20979
|
+
handle.setQueuePoke(() => {
|
|
20980
|
+
queue.poke?.();
|
|
20981
|
+
});
|
|
20091
20982
|
const shutdown = (signal) => {
|
|
20092
20983
|
process.stdout.write(`agentbox-relay: ${signal} \u2014 shutting down
|
|
20093
20984
|
`);
|
|
20094
|
-
autopause.stop().finally(() => handle.close()).finally(() => process.exit(0));
|
|
20985
|
+
Promise.allSettled([autopause.stop(), queue.stop()]).finally(() => handle.close()).finally(() => process.exit(0));
|
|
20095
20986
|
};
|
|
20096
20987
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
20097
20988
|
process.on("SIGINT", () => shutdown("SIGINT"));
|