@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/hetzner/ctl.cjs
CHANGED
|
@@ -3033,10 +3033,16 @@ var require_commander = __commonJS({
|
|
|
3033
3033
|
}
|
|
3034
3034
|
});
|
|
3035
3035
|
|
|
3036
|
-
// ../relay/dist/chunk-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3036
|
+
// ../relay/dist/chunk-YVJLTAM3.js
|
|
3037
|
+
function isConnectionLevelError(err) {
|
|
3038
|
+
const code = err?.code;
|
|
3039
|
+
if (code && CONNECTION_LEVEL_CODES.has(code)) return true;
|
|
3040
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3041
|
+
return /\b(ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ECONNRESET|EPIPE)\b/.test(msg);
|
|
3042
|
+
}
|
|
3043
|
+
var import_http, import_https, import_promises, 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;
|
|
3044
|
+
var init_chunk_YVJLTAM3 = __esm({
|
|
3045
|
+
"../relay/dist/chunk-YVJLTAM3.js"() {
|
|
3040
3046
|
"use strict";
|
|
3041
3047
|
import_http = require("http");
|
|
3042
3048
|
import_https = require("https");
|
|
@@ -3047,9 +3053,17 @@ var init_chunk_SJUIVWA3 = __esm({
|
|
|
3047
3053
|
FAST_REQUEST_TIMEOUT_MS = 8e3;
|
|
3048
3054
|
FAST_MODE_DECAY_POLLS = 5;
|
|
3049
3055
|
STOPPED_TICK_MS = 250;
|
|
3056
|
+
CONNECTION_LEVEL_CODES = /* @__PURE__ */ new Set([
|
|
3057
|
+
"ECONNREFUSED",
|
|
3058
|
+
"ENOTFOUND",
|
|
3059
|
+
"EHOSTUNREACH",
|
|
3060
|
+
"ECONNRESET",
|
|
3061
|
+
"EPIPE"
|
|
3062
|
+
]);
|
|
3050
3063
|
CloudBoxPoller = class {
|
|
3051
3064
|
constructor(deps) {
|
|
3052
3065
|
this.deps = deps;
|
|
3066
|
+
this.currentPreviewUrl = deps.previewUrl;
|
|
3053
3067
|
}
|
|
3054
3068
|
deps;
|
|
3055
3069
|
stopped = false;
|
|
@@ -3063,6 +3077,15 @@ var init_chunk_SJUIVWA3 = __esm({
|
|
|
3063
3077
|
* within ~5 successful round-trips.
|
|
3064
3078
|
*/
|
|
3065
3079
|
fastModePolls = 0;
|
|
3080
|
+
/**
|
|
3081
|
+
* Mutable copy of `deps.previewUrl`. We don't read `deps.previewUrl`
|
|
3082
|
+
* directly after construction because `recoverPreviewUrl` may hand us a
|
|
3083
|
+
* new URL (e.g. Hetzner reopens its SSH ControlMaster and the `-L`
|
|
3084
|
+
* forward gets a new ephemeral local port).
|
|
3085
|
+
*/
|
|
3086
|
+
currentPreviewUrl;
|
|
3087
|
+
/** Guards against recovery storms — at most one recovery attempt in flight. */
|
|
3088
|
+
recovering = null;
|
|
3066
3089
|
start() {
|
|
3067
3090
|
if (this.loopPromise) return;
|
|
3068
3091
|
this.loopPromise = this.run().catch((err) => {
|
|
@@ -3079,7 +3102,7 @@ var init_chunk_SJUIVWA3 = __esm({
|
|
|
3079
3102
|
* and the in-box agent finally sees the answer.
|
|
3080
3103
|
*/
|
|
3081
3104
|
async respond(actionId, result) {
|
|
3082
|
-
const base = this.
|
|
3105
|
+
const base = this.currentPreviewUrl.replace(/\/+$/, "");
|
|
3083
3106
|
const url = new URL(`${base}/bridge/action-result`);
|
|
3084
3107
|
const isHttps = url.protocol === "https:";
|
|
3085
3108
|
const transport = isHttps ? import_https.request : import_http.request;
|
|
@@ -3169,6 +3192,9 @@ var init_chunk_SJUIVWA3 = __esm({
|
|
|
3169
3192
|
this.fastModePolls = FAST_MODE_DECAY_POLLS;
|
|
3170
3193
|
}
|
|
3171
3194
|
this.log(`poll error: ${msg}`);
|
|
3195
|
+
if (this.deps.recoverPreviewUrl && isConnectionLevelError(err)) {
|
|
3196
|
+
await this.tryRecoverPreviewUrl();
|
|
3197
|
+
}
|
|
3172
3198
|
await this.backoff();
|
|
3173
3199
|
}
|
|
3174
3200
|
if (this.currentBackoffMs === 0 && this.fastModePolls > 0) {
|
|
@@ -3181,8 +3207,33 @@ var init_chunk_SJUIVWA3 = __esm({
|
|
|
3181
3207
|
this.currentBackoffMs = this.currentBackoffMs === 0 ? BACKOFF_BASE_MS : Math.min(this.currentBackoffMs * 2, BACKOFF_MAX_MS);
|
|
3182
3208
|
await (0, import_promises.setTimeout)(this.currentBackoffMs);
|
|
3183
3209
|
}
|
|
3210
|
+
async tryRecoverPreviewUrl() {
|
|
3211
|
+
if (!this.deps.recoverPreviewUrl) return;
|
|
3212
|
+
if (this.recovering) {
|
|
3213
|
+
await this.recovering;
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
this.recovering = (async () => {
|
|
3217
|
+
try {
|
|
3218
|
+
const next = await this.deps.recoverPreviewUrl();
|
|
3219
|
+
if (typeof next === "string" && next.length > 0 && next !== this.currentPreviewUrl) {
|
|
3220
|
+
this.log(`preview URL recovered: ${this.currentPreviewUrl} \u2192 ${next}`);
|
|
3221
|
+
this.currentPreviewUrl = next;
|
|
3222
|
+
this.currentBackoffMs = 0;
|
|
3223
|
+
} else if (typeof next === "string" && next === this.currentPreviewUrl) {
|
|
3224
|
+
this.log("preview URL recovered (unchanged)");
|
|
3225
|
+
this.currentBackoffMs = 0;
|
|
3226
|
+
}
|
|
3227
|
+
} catch (err) {
|
|
3228
|
+
this.log(`preview URL recover failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3229
|
+
} finally {
|
|
3230
|
+
this.recovering = null;
|
|
3231
|
+
}
|
|
3232
|
+
})();
|
|
3233
|
+
await this.recovering;
|
|
3234
|
+
}
|
|
3184
3235
|
async pollOnce() {
|
|
3185
|
-
const base = this.
|
|
3236
|
+
const base = this.currentPreviewUrl.replace(/\/+$/, "");
|
|
3186
3237
|
const url = new URL(`${base}/bridge/poll?since=${String(this.cursor)}`);
|
|
3187
3238
|
const isHttps = url.protocol === "https:";
|
|
3188
3239
|
const transport = isHttps ? import_https.request : import_http.request;
|
|
@@ -3750,7 +3801,7 @@ var require_cross_spawn = __commonJS({
|
|
|
3750
3801
|
var cp = require("child_process");
|
|
3751
3802
|
var parse = require_parse();
|
|
3752
3803
|
var enoent = require_enoent();
|
|
3753
|
-
function
|
|
3804
|
+
function spawn10(command, args, options) {
|
|
3754
3805
|
const parsed = parse(command, args, options);
|
|
3755
3806
|
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
3756
3807
|
enoent.hookChildProcess(spawned, parsed);
|
|
@@ -3762,8 +3813,8 @@ var require_cross_spawn = __commonJS({
|
|
|
3762
3813
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
3763
3814
|
return result;
|
|
3764
3815
|
}
|
|
3765
|
-
module2.exports =
|
|
3766
|
-
module2.exports.spawn =
|
|
3816
|
+
module2.exports = spawn10;
|
|
3817
|
+
module2.exports.spawn = spawn10;
|
|
3767
3818
|
module2.exports.sync = spawnSync2;
|
|
3768
3819
|
module2.exports._parse = parse;
|
|
3769
3820
|
module2.exports._enoent = enoent;
|
|
@@ -11097,16 +11148,16 @@ var require_dist = __commonJS({
|
|
|
11097
11148
|
}
|
|
11098
11149
|
});
|
|
11099
11150
|
|
|
11100
|
-
// ../relay/dist/cloud-poller-
|
|
11101
|
-
var
|
|
11102
|
-
__export(
|
|
11151
|
+
// ../relay/dist/cloud-poller-SUNA6ZQC.js
|
|
11152
|
+
var cloud_poller_SUNA6ZQC_exports = {};
|
|
11153
|
+
__export(cloud_poller_SUNA6ZQC_exports, {
|
|
11103
11154
|
CloudBoxPoller: () => CloudBoxPoller,
|
|
11104
11155
|
CloudBoxPollers: () => CloudBoxPollers
|
|
11105
11156
|
});
|
|
11106
|
-
var
|
|
11107
|
-
"../relay/dist/cloud-poller-
|
|
11157
|
+
var init_cloud_poller_SUNA6ZQC = __esm({
|
|
11158
|
+
"../relay/dist/cloud-poller-SUNA6ZQC.js"() {
|
|
11108
11159
|
"use strict";
|
|
11109
|
-
|
|
11160
|
+
init_chunk_YVJLTAM3();
|
|
11110
11161
|
}
|
|
11111
11162
|
});
|
|
11112
11163
|
|
|
@@ -11231,12 +11282,21 @@ async function claudeSession(opts) {
|
|
|
11231
11282
|
sessionName: opts.sessionName
|
|
11232
11283
|
});
|
|
11233
11284
|
}
|
|
11234
|
-
async function claudeState(opts, state) {
|
|
11235
|
-
return sendOneShot(opts, {
|
|
11285
|
+
async function claudeState(opts, state, payload) {
|
|
11286
|
+
return sendOneShot(opts, {
|
|
11287
|
+
op: "claude-state",
|
|
11288
|
+
state,
|
|
11289
|
+
...payload?.plan ? { plan: payload.plan } : {},
|
|
11290
|
+
...payload?.question ? { question: payload.question } : {},
|
|
11291
|
+
...payload?.clearPending ? { clearPending: true } : {}
|
|
11292
|
+
});
|
|
11236
11293
|
}
|
|
11237
11294
|
async function codexState(opts, state) {
|
|
11238
11295
|
return sendOneShot(opts, { op: "codex-state", state });
|
|
11239
11296
|
}
|
|
11297
|
+
async function opencodeState(opts, state) {
|
|
11298
|
+
return sendOneShot(opts, { op: "opencode-state", state });
|
|
11299
|
+
}
|
|
11240
11300
|
async function logs(opts, args) {
|
|
11241
11301
|
const sock = await connect(opts);
|
|
11242
11302
|
sock.write(`${JSON.stringify({ op: "logs", ...args })}
|
|
@@ -11291,6 +11351,10 @@ var CLAUDE_ACTIVITY_STATES = [
|
|
|
11291
11351
|
"working",
|
|
11292
11352
|
"idle",
|
|
11293
11353
|
"waiting",
|
|
11354
|
+
"end-plan",
|
|
11355
|
+
"question",
|
|
11356
|
+
"compacting",
|
|
11357
|
+
"error",
|
|
11294
11358
|
"unknown"
|
|
11295
11359
|
];
|
|
11296
11360
|
var BOX_STATUS_SCHEMA = 1;
|
|
@@ -11324,18 +11388,90 @@ var claudeSessionCommand = new Command("claude-session").description("Report whe
|
|
|
11324
11388
|
});
|
|
11325
11389
|
|
|
11326
11390
|
// src/commands/claude-state.ts
|
|
11327
|
-
var claudeStateCommand = new Command("claude-state").description("Report Claude activity state to the box supervisor (used by hooks)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).action(async (state, opts) => {
|
|
11391
|
+
var claudeStateCommand = new Command("claude-state").description("Report Claude activity state to the box supervisor (used by hooks)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).option("--payload-stdin", "parse Claude Code's hook JSON from stdin (PreToolUse plan/question)").option("--clear-pending", "force-clear a sticky end-plan/question state (PostToolUse cleanup)").action(async (state, opts) => {
|
|
11328
11392
|
try {
|
|
11329
|
-
if (CLAUDE_ACTIVITY_STATES.includes(state)) {
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
}
|
|
11393
|
+
if (!CLAUDE_ACTIVITY_STATES.includes(state)) {
|
|
11394
|
+
process.exit(0);
|
|
11395
|
+
}
|
|
11396
|
+
const typedState = state;
|
|
11397
|
+
const extracted = opts.payloadStdin ? await extractPayload(typedState, await readStdinJson()) : void 0;
|
|
11398
|
+
const payload = { ...extracted ?? {} };
|
|
11399
|
+
if (opts.clearPending) payload.clearPending = true;
|
|
11400
|
+
const hasField = payload.plan !== void 0 || payload.question !== void 0 || payload.clearPending !== void 0;
|
|
11401
|
+
await claudeState(
|
|
11402
|
+
{ socketPath: opts.socket, timeoutMs: 1500 },
|
|
11403
|
+
typedState,
|
|
11404
|
+
hasField ? payload : void 0
|
|
11405
|
+
);
|
|
11335
11406
|
} catch {
|
|
11336
11407
|
}
|
|
11337
11408
|
process.exit(0);
|
|
11338
11409
|
});
|
|
11410
|
+
function extractPayload(state, raw) {
|
|
11411
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
11412
|
+
const tool = raw.tool_input ?? {};
|
|
11413
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11414
|
+
if (state === "end-plan" && typeof tool.plan === "string") {
|
|
11415
|
+
const plan = { plan: tool.plan, capturedAt };
|
|
11416
|
+
return { plan };
|
|
11417
|
+
}
|
|
11418
|
+
if (state === "question" && Array.isArray(tool.questions)) {
|
|
11419
|
+
const questions = tool.questions.map((q) => normalizeQuestion(q)).filter((q) => q !== null);
|
|
11420
|
+
if (questions.length === 0) return void 0;
|
|
11421
|
+
const question = { questions, capturedAt };
|
|
11422
|
+
return { question };
|
|
11423
|
+
}
|
|
11424
|
+
return void 0;
|
|
11425
|
+
}
|
|
11426
|
+
function normalizeQuestion(raw) {
|
|
11427
|
+
if (!raw || typeof raw !== "object") return null;
|
|
11428
|
+
const q = raw;
|
|
11429
|
+
if (typeof q.question !== "string") return null;
|
|
11430
|
+
const opts = Array.isArray(q.options) ? q.options : [];
|
|
11431
|
+
const options = opts.map((o2) => {
|
|
11432
|
+
if (!o2 || typeof o2 !== "object") return null;
|
|
11433
|
+
const oo = o2;
|
|
11434
|
+
if (typeof oo.label !== "string") return null;
|
|
11435
|
+
const entry = { label: oo.label };
|
|
11436
|
+
if (typeof oo.description === "string") entry.description = oo.description;
|
|
11437
|
+
return entry;
|
|
11438
|
+
}).filter((o2) => o2 !== null);
|
|
11439
|
+
const out = { question: q.question, options };
|
|
11440
|
+
if (typeof q.header === "string") out.header = q.header;
|
|
11441
|
+
if (typeof q.multiSelect === "boolean") out.multiSelect = q.multiSelect;
|
|
11442
|
+
return out;
|
|
11443
|
+
}
|
|
11444
|
+
function readStdinJson() {
|
|
11445
|
+
return new Promise((resolve2) => {
|
|
11446
|
+
if (process.stdin.isTTY) {
|
|
11447
|
+
resolve2(null);
|
|
11448
|
+
return;
|
|
11449
|
+
}
|
|
11450
|
+
const chunks = [];
|
|
11451
|
+
const cap = setTimeout(() => {
|
|
11452
|
+
process.stdin.removeAllListeners();
|
|
11453
|
+
resolve2(null);
|
|
11454
|
+
}, 1e3);
|
|
11455
|
+
cap.unref();
|
|
11456
|
+
process.stdin.on("data", (b) => chunks.push(b));
|
|
11457
|
+
process.stdin.on("end", () => {
|
|
11458
|
+
clearTimeout(cap);
|
|
11459
|
+
if (chunks.length === 0) {
|
|
11460
|
+
resolve2(null);
|
|
11461
|
+
return;
|
|
11462
|
+
}
|
|
11463
|
+
try {
|
|
11464
|
+
resolve2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
11465
|
+
} catch {
|
|
11466
|
+
resolve2(null);
|
|
11467
|
+
}
|
|
11468
|
+
});
|
|
11469
|
+
process.stdin.on("error", () => {
|
|
11470
|
+
clearTimeout(cap);
|
|
11471
|
+
resolve2(null);
|
|
11472
|
+
});
|
|
11473
|
+
});
|
|
11474
|
+
}
|
|
11339
11475
|
|
|
11340
11476
|
// src/commands/codex-state.ts
|
|
11341
11477
|
var codexStateCommand = new Command("codex-state").description("Report Codex activity state to the box supervisor (used by hooks)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).action(async (state, opts) => {
|
|
@@ -11450,15 +11586,31 @@ var cpCommand = new Command("cp").description("Copy a file/dir between this box
|
|
|
11450
11586
|
})
|
|
11451
11587
|
);
|
|
11452
11588
|
|
|
11589
|
+
// src/commands/opencode-state.ts
|
|
11590
|
+
var opencodeStateCommand = new Command("opencode-state").description("Report OpenCode activity state to the box supervisor (used by the plugin)").argument("<state>", `one of: ${CLAUDE_ACTIVITY_STATES.join(", ")}`).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).action(async (state, opts) => {
|
|
11591
|
+
try {
|
|
11592
|
+
if (CLAUDE_ACTIVITY_STATES.includes(state)) {
|
|
11593
|
+
await opencodeState(
|
|
11594
|
+
{ socketPath: opts.socket, timeoutMs: 1500 },
|
|
11595
|
+
state
|
|
11596
|
+
);
|
|
11597
|
+
}
|
|
11598
|
+
} catch {
|
|
11599
|
+
}
|
|
11600
|
+
process.exit(0);
|
|
11601
|
+
});
|
|
11602
|
+
|
|
11453
11603
|
// ../relay/dist/index.js
|
|
11454
|
-
|
|
11604
|
+
init_chunk_YVJLTAM3();
|
|
11455
11605
|
var import_crypto = require("crypto");
|
|
11456
11606
|
var import_crypto2 = require("crypto");
|
|
11457
11607
|
var import_crypto3 = require("crypto");
|
|
11608
|
+
var import_crypto4 = require("crypto");
|
|
11609
|
+
var import_child_process = require("child_process");
|
|
11458
11610
|
var import_promises14 = require("fs/promises");
|
|
11459
11611
|
var import_os3 = require("os");
|
|
11460
11612
|
var import_path4 = require("path");
|
|
11461
|
-
var
|
|
11613
|
+
var import_child_process2 = require("child_process");
|
|
11462
11614
|
var import_http2 = require("http");
|
|
11463
11615
|
|
|
11464
11616
|
// ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
|
|
@@ -18284,6 +18436,8 @@ function projectDockerFields(box) {
|
|
|
18284
18436
|
webHostPort: box.webHostPort,
|
|
18285
18437
|
portlessAlias: box.portlessAlias,
|
|
18286
18438
|
portlessUrl: box.portlessUrl,
|
|
18439
|
+
portlessVncAlias: box.portlessVncAlias,
|
|
18440
|
+
portlessVncUrl: box.portlessVncUrl,
|
|
18287
18441
|
dockerVolume: box.dockerVolume,
|
|
18288
18442
|
dockerCacheShared: box.dockerCacheShared,
|
|
18289
18443
|
checkpointImage: box.checkpointImage
|
|
@@ -18414,6 +18568,11 @@ var KEY_REGISTRY = [
|
|
|
18414
18568
|
description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
|
|
18415
18569
|
advanced: true
|
|
18416
18570
|
},
|
|
18571
|
+
{
|
|
18572
|
+
key: "box.bundleDepth",
|
|
18573
|
+
type: "int",
|
|
18574
|
+
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/)."
|
|
18575
|
+
},
|
|
18417
18576
|
{
|
|
18418
18577
|
key: "claude.sessionName",
|
|
18419
18578
|
type: "string",
|
|
@@ -18521,6 +18680,16 @@ var KEY_REGISTRY = [
|
|
|
18521
18680
|
type: "int",
|
|
18522
18681
|
description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
|
|
18523
18682
|
},
|
|
18683
|
+
{
|
|
18684
|
+
key: "queue.enabled",
|
|
18685
|
+
type: "bool",
|
|
18686
|
+
description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
|
|
18687
|
+
},
|
|
18688
|
+
{
|
|
18689
|
+
key: "queue.maxConcurrent",
|
|
18690
|
+
type: "int",
|
|
18691
|
+
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>`."
|
|
18692
|
+
},
|
|
18524
18693
|
{
|
|
18525
18694
|
key: "maintenance.pruneProjectConfigs",
|
|
18526
18695
|
type: "bool",
|
|
@@ -18557,7 +18726,8 @@ var PROJECTS_DIR = (0, import_path2.join)(STATE_DIR2, "projects");
|
|
|
18557
18726
|
var PROJECT_GC_COUNTER_FILE = (0, import_path3.join)(PROJECTS_DIR, ".gc.json");
|
|
18558
18727
|
|
|
18559
18728
|
// ../relay/dist/index.js
|
|
18560
|
-
var
|
|
18729
|
+
var import_path6 = require("path");
|
|
18730
|
+
var DEFAULT_BOX_RELAY_PORT = 8788;
|
|
18561
18731
|
var RELAY_EVENT_RING_SIZE = 1e3;
|
|
18562
18732
|
var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
|
|
18563
18733
|
var HostActionQueue = class {
|
|
@@ -18870,6 +19040,237 @@ var BoxNotices = class {
|
|
|
18870
19040
|
return this.entries.size;
|
|
18871
19041
|
}
|
|
18872
19042
|
};
|
|
19043
|
+
var DEFAULT_TTL_MS = 12e4;
|
|
19044
|
+
function hashRpcParams(params) {
|
|
19045
|
+
return (0, import_crypto4.createHash)("sha256").update(canonicalJson(params)).digest("hex");
|
|
19046
|
+
}
|
|
19047
|
+
function canonicalJson(v) {
|
|
19048
|
+
if (v === null) return "null";
|
|
19049
|
+
if (typeof v === "undefined") return "null";
|
|
19050
|
+
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
|
|
19051
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
19052
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
19053
|
+
if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
|
|
19054
|
+
if (typeof v === "object") {
|
|
19055
|
+
const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a2], [b]) => a2 < b ? -1 : a2 > b ? 1 : 0);
|
|
19056
|
+
return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
|
|
19057
|
+
}
|
|
19058
|
+
return "null";
|
|
19059
|
+
}
|
|
19060
|
+
var HostInitiatedTokens = class {
|
|
19061
|
+
store = /* @__PURE__ */ new Map();
|
|
19062
|
+
/**
|
|
19063
|
+
* Mint a fresh one-time token scoped to (boxId, method, paramsHash).
|
|
19064
|
+
* `paramsHash` MUST be supplied for any call surface where the box can
|
|
19065
|
+
* influence the eventual RPC params. Pass `null` only when there are no
|
|
19066
|
+
* params (no current call sites use this).
|
|
19067
|
+
*/
|
|
19068
|
+
mint(boxId, method, paramsHash, ttlMs = DEFAULT_TTL_MS) {
|
|
19069
|
+
const token = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
19070
|
+
this.store.set(token, { boxId, method, paramsHash, expiresAt: Date.now() + ttlMs });
|
|
19071
|
+
return token;
|
|
19072
|
+
}
|
|
19073
|
+
/**
|
|
19074
|
+
* Returns true exactly once if `token` is a valid, unexpired token for the
|
|
19075
|
+
* given `(boxId, method)` AND the supplied `incomingParamsHash` matches
|
|
19076
|
+
* the hash bound at mint time. The token is removed on a successful match
|
|
19077
|
+
* (one-shot semantics). All failure modes return false — callers fall back
|
|
19078
|
+
* to the normal prompt path.
|
|
19079
|
+
*/
|
|
19080
|
+
consume(token, boxId, method, incomingParamsHash) {
|
|
19081
|
+
if (!token || typeof token !== "string") return false;
|
|
19082
|
+
const record = this.store.get(token);
|
|
19083
|
+
if (!record) return false;
|
|
19084
|
+
if (record.expiresAt < Date.now()) {
|
|
19085
|
+
this.store.delete(token);
|
|
19086
|
+
return false;
|
|
19087
|
+
}
|
|
19088
|
+
if (record.boxId !== boxId || record.method !== method) return false;
|
|
19089
|
+
if (record.paramsHash !== null && record.paramsHash !== incomingParamsHash) {
|
|
19090
|
+
return false;
|
|
19091
|
+
}
|
|
19092
|
+
this.store.delete(token);
|
|
19093
|
+
return true;
|
|
19094
|
+
}
|
|
19095
|
+
/** Drop expired entries. Cheap; safe to call periodically. */
|
|
19096
|
+
gc() {
|
|
19097
|
+
const now = Date.now();
|
|
19098
|
+
for (const [token, record] of this.store) {
|
|
19099
|
+
if (record.expiresAt < now) this.store.delete(token);
|
|
19100
|
+
}
|
|
19101
|
+
}
|
|
19102
|
+
/** Test-only: number of live tokens. */
|
|
19103
|
+
size() {
|
|
19104
|
+
return this.store.size;
|
|
19105
|
+
}
|
|
19106
|
+
};
|
|
19107
|
+
var GH_PR_OPS = [
|
|
19108
|
+
"create",
|
|
19109
|
+
"view",
|
|
19110
|
+
"list",
|
|
19111
|
+
"comment",
|
|
19112
|
+
"review",
|
|
19113
|
+
"merge",
|
|
19114
|
+
"checkout",
|
|
19115
|
+
"close",
|
|
19116
|
+
"reopen"
|
|
19117
|
+
];
|
|
19118
|
+
function isGhPrOp(value) {
|
|
19119
|
+
return GH_PR_OPS.includes(value);
|
|
19120
|
+
}
|
|
19121
|
+
var GH_PR_READ_ONLY_OPS = /* @__PURE__ */ new Set(["view", "list"]);
|
|
19122
|
+
var GH_RPC_TIMEOUT_MS = 12e4;
|
|
19123
|
+
var GH_READY_CACHE_TTL_MS = 6e4;
|
|
19124
|
+
var ghReadyCache;
|
|
19125
|
+
async function assertGhReady() {
|
|
19126
|
+
const now = Date.now();
|
|
19127
|
+
if (ghReadyCache && ghReadyCache.expiresAt > now) {
|
|
19128
|
+
return ghReadyCache.result;
|
|
19129
|
+
}
|
|
19130
|
+
const result = await probeGh();
|
|
19131
|
+
ghReadyCache = { result, expiresAt: now + GH_READY_CACHE_TTL_MS };
|
|
19132
|
+
return result;
|
|
19133
|
+
}
|
|
19134
|
+
async function probeGh() {
|
|
19135
|
+
const version = await runHostGh(["--version"], process.cwd(), 1e4);
|
|
19136
|
+
if (version.exitCode === 127 || /ENOENT/.test(version.stderr)) {
|
|
19137
|
+
return {
|
|
19138
|
+
exitCode: 127,
|
|
19139
|
+
stdout: "",
|
|
19140
|
+
stderr: "gh not installed on host (https://cli.github.com)\n"
|
|
19141
|
+
};
|
|
19142
|
+
}
|
|
19143
|
+
if (version.exitCode !== 0) {
|
|
19144
|
+
return {
|
|
19145
|
+
exitCode: version.exitCode,
|
|
19146
|
+
stdout: "",
|
|
19147
|
+
stderr: `gh --version failed: ${version.stderr || version.stdout}`.trimEnd() + "\n"
|
|
19148
|
+
};
|
|
19149
|
+
}
|
|
19150
|
+
const auth = await runHostGh(["auth", "status"], process.cwd(), 15e3);
|
|
19151
|
+
if (auth.exitCode !== 0) {
|
|
19152
|
+
return {
|
|
19153
|
+
exitCode: 4,
|
|
19154
|
+
stdout: "",
|
|
19155
|
+
stderr: "gh not authenticated on host (run `gh auth login`)\n"
|
|
19156
|
+
};
|
|
19157
|
+
}
|
|
19158
|
+
return null;
|
|
19159
|
+
}
|
|
19160
|
+
function runHostGh(args, cwd, timeoutMs = GH_RPC_TIMEOUT_MS) {
|
|
19161
|
+
return new Promise((resolve2) => {
|
|
19162
|
+
const child = (0, import_child_process.spawn)("gh", args, {
|
|
19163
|
+
cwd,
|
|
19164
|
+
env: process.env,
|
|
19165
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
19166
|
+
});
|
|
19167
|
+
let stdout = "";
|
|
19168
|
+
let stderr = "";
|
|
19169
|
+
let settled = false;
|
|
19170
|
+
const finish = (exitCode) => {
|
|
19171
|
+
if (settled) return;
|
|
19172
|
+
settled = true;
|
|
19173
|
+
resolve2({ exitCode, stdout, stderr });
|
|
19174
|
+
};
|
|
19175
|
+
const timer = setTimeout(() => {
|
|
19176
|
+
child.kill("SIGTERM");
|
|
19177
|
+
stderr += `
|
|
19178
|
+
relay: gh command timed out after ${String(timeoutMs)}ms
|
|
19179
|
+
`;
|
|
19180
|
+
finish(124);
|
|
19181
|
+
}, timeoutMs);
|
|
19182
|
+
child.stdout?.on("data", (chunk) => {
|
|
19183
|
+
stdout += chunk.toString("utf8");
|
|
19184
|
+
});
|
|
19185
|
+
child.stderr?.on("data", (chunk) => {
|
|
19186
|
+
stderr += chunk.toString("utf8");
|
|
19187
|
+
});
|
|
19188
|
+
child.on("error", (err) => {
|
|
19189
|
+
clearTimeout(timer);
|
|
19190
|
+
const code = err.code;
|
|
19191
|
+
stderr += String(err.message ?? err);
|
|
19192
|
+
finish(code === "ENOENT" ? 127 : 1);
|
|
19193
|
+
});
|
|
19194
|
+
child.on("close", (code) => {
|
|
19195
|
+
clearTimeout(timer);
|
|
19196
|
+
finish(code ?? -1);
|
|
19197
|
+
});
|
|
19198
|
+
});
|
|
19199
|
+
}
|
|
19200
|
+
async function checkoutGuards(hostMainRepo, registeredBranches) {
|
|
19201
|
+
const status2 = await runGitProbe(["-C", hostMainRepo, "status", "--porcelain"]);
|
|
19202
|
+
if (status2.exitCode !== 0) {
|
|
19203
|
+
return {
|
|
19204
|
+
exitCode: status2.exitCode,
|
|
19205
|
+
stdout: "",
|
|
19206
|
+
stderr: `gh pr checkout: failed to inspect host repo: ${status2.stderr || status2.stdout}`.trimEnd() + "\n"
|
|
19207
|
+
};
|
|
19208
|
+
}
|
|
19209
|
+
if (status2.stdout.trim().length > 0) {
|
|
19210
|
+
return {
|
|
19211
|
+
exitCode: 12,
|
|
19212
|
+
stdout: "",
|
|
19213
|
+
stderr: `gh pr checkout: ${hostMainRepo} has uncommitted changes; refusing to switch branches
|
|
19214
|
+
`
|
|
19215
|
+
};
|
|
19216
|
+
}
|
|
19217
|
+
const head = await runGitProbe(["-C", hostMainRepo, "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
19218
|
+
if (head.exitCode !== 0) {
|
|
19219
|
+
return {
|
|
19220
|
+
exitCode: head.exitCode,
|
|
19221
|
+
stdout: "",
|
|
19222
|
+
stderr: `gh pr checkout: failed to resolve HEAD: ${head.stderr || head.stdout}`.trimEnd() + "\n"
|
|
19223
|
+
};
|
|
19224
|
+
}
|
|
19225
|
+
const currentBranch = head.stdout.trim();
|
|
19226
|
+
if (registeredBranches.includes(currentBranch)) {
|
|
19227
|
+
return {
|
|
19228
|
+
exitCode: 12,
|
|
19229
|
+
stdout: "",
|
|
19230
|
+
stderr: `gh pr checkout: ${hostMainRepo} is on registered box branch ${currentBranch}; refusing (would corrupt the bind-mounted box HEAD)
|
|
19231
|
+
`
|
|
19232
|
+
};
|
|
19233
|
+
}
|
|
19234
|
+
return null;
|
|
19235
|
+
}
|
|
19236
|
+
function runGitProbe(args) {
|
|
19237
|
+
return new Promise((resolve2) => {
|
|
19238
|
+
const child = (0, import_child_process.spawn)("git", args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
19239
|
+
let stdout = "";
|
|
19240
|
+
let stderr = "";
|
|
19241
|
+
child.stdout?.on("data", (c3) => {
|
|
19242
|
+
stdout += c3.toString("utf8");
|
|
19243
|
+
});
|
|
19244
|
+
child.stderr?.on("data", (c3) => {
|
|
19245
|
+
stderr += c3.toString("utf8");
|
|
19246
|
+
});
|
|
19247
|
+
child.on("error", (err) => {
|
|
19248
|
+
resolve2({ exitCode: 127, stdout, stderr: stderr + String(err.message ?? err) });
|
|
19249
|
+
});
|
|
19250
|
+
child.on("close", (code) => {
|
|
19251
|
+
resolve2({ exitCode: code ?? -1, stdout, stderr });
|
|
19252
|
+
});
|
|
19253
|
+
});
|
|
19254
|
+
}
|
|
19255
|
+
function refuseMergeBypass(op) {
|
|
19256
|
+
if (op !== "merge") return null;
|
|
19257
|
+
if (process.env["AGENTBOX_PROMPT"] !== "off") return null;
|
|
19258
|
+
if (process.env["AGENTBOX_GH_FORCE"] === "1") return null;
|
|
19259
|
+
return {
|
|
19260
|
+
exitCode: 10,
|
|
19261
|
+
stdout: "",
|
|
19262
|
+
stderr: "gh pr merge: AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1 (merge is irreversible)\n"
|
|
19263
|
+
};
|
|
19264
|
+
}
|
|
19265
|
+
function refuseCheckoutByDefault(op) {
|
|
19266
|
+
if (op !== "checkout") return null;
|
|
19267
|
+
if (process.env["AGENTBOX_GH_PR_CHECKOUT"] === "allow") return null;
|
|
19268
|
+
return {
|
|
19269
|
+
exitCode: 13,
|
|
19270
|
+
stdout: "",
|
|
19271
|
+
stderr: "gh pr checkout: disabled by default; set AGENTBOX_GH_PR_CHECKOUT=allow to enable\n"
|
|
19272
|
+
};
|
|
19273
|
+
}
|
|
18873
19274
|
function sanitizeMnemonic(raw) {
|
|
18874
19275
|
return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
|
|
18875
19276
|
}
|
|
@@ -18949,6 +19350,18 @@ async function resolveCloudBackend(name) {
|
|
|
18949
19350
|
}
|
|
18950
19351
|
throw new Error(`no host executor for cloud backend '${name}'`);
|
|
18951
19352
|
}
|
|
19353
|
+
async function refreshCloudPreviewUrl(backendName, boxId, port) {
|
|
19354
|
+
try {
|
|
19355
|
+
const backend = await resolveCloudBackend(backendName);
|
|
19356
|
+
if (!backend.refreshPreviewUrl) return null;
|
|
19357
|
+
const lookup = await lookupCloudBox(boxId);
|
|
19358
|
+
const handle = { sandboxId: lookup.cloudSandboxId };
|
|
19359
|
+
const url = await backend.refreshPreviewUrl(handle, port);
|
|
19360
|
+
return url.url;
|
|
19361
|
+
} catch {
|
|
19362
|
+
return null;
|
|
19363
|
+
}
|
|
19364
|
+
}
|
|
18952
19365
|
async function executeCloudAction(action, deps) {
|
|
18953
19366
|
const log = deps.log ?? (() => {
|
|
18954
19367
|
});
|
|
@@ -18968,6 +19381,17 @@ async function executeCloudAction(action, deps) {
|
|
|
18968
19381
|
if (action.method === "browser.open.mirror") {
|
|
18969
19382
|
return runBrowserOpenMirror(action, deps);
|
|
18970
19383
|
}
|
|
19384
|
+
if (action.method.startsWith("gh.pr.")) {
|
|
19385
|
+
return runGhPrRpc(action, deps);
|
|
19386
|
+
}
|
|
19387
|
+
if (action.method === "git.clone" || action.method === "gh.repo.clone") {
|
|
19388
|
+
return {
|
|
19389
|
+
exitCode: 64,
|
|
19390
|
+
stdout: "",
|
|
19391
|
+
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.
|
|
19392
|
+
`
|
|
19393
|
+
};
|
|
19394
|
+
}
|
|
18971
19395
|
return {
|
|
18972
19396
|
exitCode: 1,
|
|
18973
19397
|
stdout: "",
|
|
@@ -18975,6 +19399,86 @@ async function executeCloudAction(action, deps) {
|
|
|
18975
19399
|
`
|
|
18976
19400
|
};
|
|
18977
19401
|
}
|
|
19402
|
+
async function runGhPrRpc(action, deps) {
|
|
19403
|
+
const op = action.method.slice("gh.pr.".length);
|
|
19404
|
+
if (!isGhPrOp(op)) {
|
|
19405
|
+
return {
|
|
19406
|
+
exitCode: 64,
|
|
19407
|
+
stdout: "",
|
|
19408
|
+
stderr: `unknown gh.pr.* op: ${op}
|
|
19409
|
+
`
|
|
19410
|
+
};
|
|
19411
|
+
}
|
|
19412
|
+
const mergeBypass = refuseMergeBypass(op);
|
|
19413
|
+
if (mergeBypass) return mergeBypass;
|
|
19414
|
+
const checkoutOptIn = refuseCheckoutByDefault(op);
|
|
19415
|
+
if (checkoutOptIn) return checkoutOptIn;
|
|
19416
|
+
const params = action.params ?? {};
|
|
19417
|
+
const args = Array.isArray(params.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
|
|
19418
|
+
const ghReady = await assertGhReady();
|
|
19419
|
+
if (ghReady) return ghReady;
|
|
19420
|
+
const lookup = await lookupCloudBox(deps.boxId);
|
|
19421
|
+
if (op === "checkout") {
|
|
19422
|
+
const guard = await checkoutGuards(lookup.workspacePath, []);
|
|
19423
|
+
if (guard) return guard;
|
|
19424
|
+
}
|
|
19425
|
+
const tokenClaimedGhCloud = typeof params.hostInitiated === "string";
|
|
19426
|
+
const incomingHashGhCloud = hashRpcParams(params);
|
|
19427
|
+
const hostInitiatedGhOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && (deps.hostInitiatedTokens?.consume(
|
|
19428
|
+
params.hostInitiated,
|
|
19429
|
+
deps.boxId,
|
|
19430
|
+
`gh.pr.${op}`,
|
|
19431
|
+
incomingHashGhCloud
|
|
19432
|
+
) ?? false);
|
|
19433
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGhCloud && !hostInitiatedGhOk) {
|
|
19434
|
+
return {
|
|
19435
|
+
exitCode: 10,
|
|
19436
|
+
stdout: "",
|
|
19437
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
19438
|
+
};
|
|
19439
|
+
}
|
|
19440
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedGhOk && deps.prompts && deps.subscribers) {
|
|
19441
|
+
const detail = args.join(" ").slice(0, 200);
|
|
19442
|
+
const ctx = {
|
|
19443
|
+
kind: "confirm",
|
|
19444
|
+
message: `Allow gh pr ${op} from cloud box ${deps.boxName ?? deps.boxId}?`,
|
|
19445
|
+
detail,
|
|
19446
|
+
defaultAnswer: "n",
|
|
19447
|
+
context: {
|
|
19448
|
+
command: `gh pr ${op}`,
|
|
19449
|
+
cwd: params.path,
|
|
19450
|
+
argv: args
|
|
19451
|
+
}
|
|
19452
|
+
};
|
|
19453
|
+
const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
|
|
19454
|
+
if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
|
|
19455
|
+
const noSubMode = (process.env["AGENTBOX_GH_NO_SUB"] ?? "deny").toLowerCase();
|
|
19456
|
+
if (noSubMode === "deny") {
|
|
19457
|
+
return {
|
|
19458
|
+
exitCode: 10,
|
|
19459
|
+
stdout: "",
|
|
19460
|
+
stderr: "denied automatically \u2014 no attached wrapper to confirm. Attach `agentbox claude` (or similar) and retry, or set AGENTBOX_GH_NO_SUB=allow.\n"
|
|
19461
|
+
};
|
|
19462
|
+
}
|
|
19463
|
+
if (noSubMode === "allow") {
|
|
19464
|
+
deps.log?.(`gh.pr.${op} auto-approved (no subscribers, AGENTBOX_GH_NO_SUB=allow)`);
|
|
19465
|
+
} else {
|
|
19466
|
+
const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx, {
|
|
19467
|
+
ttlMs: 5 * 60 * 1e3
|
|
19468
|
+
});
|
|
19469
|
+
if (verdict.answer !== "y") {
|
|
19470
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
19471
|
+
}
|
|
19472
|
+
}
|
|
19473
|
+
} else {
|
|
19474
|
+
const verdict = await askPrompt(deps.prompts, deps.subscribers, deps.boxId, ctx);
|
|
19475
|
+
if (verdict.answer !== "y") {
|
|
19476
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
19477
|
+
}
|
|
19478
|
+
}
|
|
19479
|
+
}
|
|
19480
|
+
return runHostGh(["pr", op, ...args], lookup.workspacePath);
|
|
19481
|
+
}
|
|
18978
19482
|
async function runBrowserOpenMirror(action, deps) {
|
|
18979
19483
|
const params = action.params ?? {};
|
|
18980
19484
|
const url = typeof params.url === "string" ? params.url.trim() : "";
|
|
@@ -19001,8 +19505,8 @@ async function runBrowserOpenMirror(action, deps) {
|
|
|
19001
19505
|
{ ttlMs: TTL_MS }
|
|
19002
19506
|
);
|
|
19003
19507
|
if (verdict.answer === "y" && !verdict.cancelled) {
|
|
19004
|
-
const { spawn:
|
|
19005
|
-
const child =
|
|
19508
|
+
const { spawn: spawn52 } = await import("child_process");
|
|
19509
|
+
const child = spawn52("open", [url], { stdio: "ignore", detached: true });
|
|
19006
19510
|
child.unref();
|
|
19007
19511
|
}
|
|
19008
19512
|
} catch (err) {
|
|
@@ -19173,7 +19677,23 @@ async function runGitRpc(action, deps) {
|
|
|
19173
19677
|
stderr: `failed to resolve branch in sandbox ${containerPath}: ${branchProbe.stderr || branch}`
|
|
19174
19678
|
};
|
|
19175
19679
|
}
|
|
19176
|
-
|
|
19680
|
+
const isAgentboxBranch = branch.startsWith("agentbox/");
|
|
19681
|
+
const tokenClaimedGit = typeof params.hostInitiated === "string";
|
|
19682
|
+
const incomingHashGit = hashRpcParams(params);
|
|
19683
|
+
const hostInitiatedOk = !isAgentboxBranch && tokenClaimedGit && (deps.hostInitiatedTokens?.consume(
|
|
19684
|
+
params.hostInitiated,
|
|
19685
|
+
deps.boxId,
|
|
19686
|
+
"git.push",
|
|
19687
|
+
incomingHashGit
|
|
19688
|
+
) ?? false);
|
|
19689
|
+
if (action.method === "git.push" && !isAgentboxBranch && tokenClaimedGit && !hostInitiatedOk) {
|
|
19690
|
+
return {
|
|
19691
|
+
exitCode: 10,
|
|
19692
|
+
stdout: "",
|
|
19693
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
19694
|
+
};
|
|
19695
|
+
}
|
|
19696
|
+
if (action.method === "git.push" && !isAgentboxBranch && !hostInitiatedOk && deps.prompts && deps.subscribers) {
|
|
19177
19697
|
const hasSubscriber = deps.subscribers.forBox(deps.boxId).length > 0;
|
|
19178
19698
|
if (!hasSubscriber && process.env["AGENTBOX_PROMPT"] !== "off") {
|
|
19179
19699
|
const noSubMode = (process.env["AGENTBOX_GIT_PUSH_NO_SUB"] ?? "deny").toLowerCase();
|
|
@@ -19248,10 +19768,45 @@ async function runGitRpc(action, deps) {
|
|
|
19248
19768
|
for (const a2 of params.args) if (typeof a2 === "string") argv.push(a2);
|
|
19249
19769
|
}
|
|
19250
19770
|
const push = await execa("git", argv, { reject: false });
|
|
19771
|
+
let pushStderr = push.stderr ?? "";
|
|
19772
|
+
if ((push.exitCode ?? 1) === 0 && !branch.startsWith("agentbox/")) {
|
|
19773
|
+
try {
|
|
19774
|
+
const sha = await execa(
|
|
19775
|
+
"git",
|
|
19776
|
+
["-C", lookup.workspacePath, "rev-parse", branch],
|
|
19777
|
+
{ reject: false }
|
|
19778
|
+
);
|
|
19779
|
+
const shaText = (sha.stdout ?? "").trim();
|
|
19780
|
+
if (sha.exitCode === 0 && shaText.length > 0) {
|
|
19781
|
+
const updateRef = await backend.exec(
|
|
19782
|
+
handle,
|
|
19783
|
+
`git -C ${shellQuote(containerPath)} update-ref refs/remotes/${remote2}/${branch} ${shellQuote(shaText)}`
|
|
19784
|
+
);
|
|
19785
|
+
if (updateRef.exitCode !== 0) {
|
|
19786
|
+
pushStderr += `
|
|
19787
|
+
relay: post-push in-box update-ref refs/remotes/${remote2}/${branch} failed: ${updateRef.stderr || updateRef.stdout}`;
|
|
19788
|
+
}
|
|
19789
|
+
const setUpstream = await backend.exec(
|
|
19790
|
+
handle,
|
|
19791
|
+
`git -C ${shellQuote(containerPath)} branch --set-upstream-to=${remote2}/${branch} ${shellQuote(branch)}`
|
|
19792
|
+
);
|
|
19793
|
+
if (setUpstream.exitCode !== 0) {
|
|
19794
|
+
pushStderr += `
|
|
19795
|
+
relay: post-push in-box --set-upstream-to=${remote2}/${branch} failed: ${setUpstream.stderr || setUpstream.stdout}`;
|
|
19796
|
+
}
|
|
19797
|
+
} else {
|
|
19798
|
+
pushStderr += `
|
|
19799
|
+
relay: post-push rev-parse ${branch} failed on host; skipping in-box origin/upstream sync`;
|
|
19800
|
+
}
|
|
19801
|
+
} catch (err) {
|
|
19802
|
+
pushStderr += `
|
|
19803
|
+
relay: post-push in-box origin/upstream sync threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
19804
|
+
}
|
|
19805
|
+
}
|
|
19251
19806
|
return {
|
|
19252
19807
|
exitCode: push.exitCode ?? 1,
|
|
19253
19808
|
stdout: push.stdout ?? "",
|
|
19254
|
-
stderr:
|
|
19809
|
+
stderr: pushStderr
|
|
19255
19810
|
};
|
|
19256
19811
|
}
|
|
19257
19812
|
const remote = params.remote ?? "origin";
|
|
@@ -19363,6 +19918,8 @@ function createRelayServer(opts) {
|
|
|
19363
19918
|
const prompts = new PendingPrompts();
|
|
19364
19919
|
const subscribers = new PromptSubscribers();
|
|
19365
19920
|
const notices = new BoxNotices(subscribers);
|
|
19921
|
+
const hostInitiatedTokens = new HostInitiatedTokens();
|
|
19922
|
+
let queuePoke = null;
|
|
19366
19923
|
const host = opts.host ?? "0.0.0.0";
|
|
19367
19924
|
const mode = opts.mode ?? "host";
|
|
19368
19925
|
const hostActions = mode === "box" ? new HostActionQueue() : null;
|
|
@@ -19373,7 +19930,7 @@ function createRelayServer(opts) {
|
|
|
19373
19930
|
let pollers = null;
|
|
19374
19931
|
async function getPollers() {
|
|
19375
19932
|
if (!pollers) {
|
|
19376
|
-
const mod = await Promise.resolve().then(() => (
|
|
19933
|
+
const mod = await Promise.resolve().then(() => (init_cloud_poller_SUNA6ZQC(), cloud_poller_SUNA6ZQC_exports));
|
|
19377
19934
|
pollers = new mod.CloudBoxPollers();
|
|
19378
19935
|
}
|
|
19379
19936
|
return pollers;
|
|
@@ -19495,21 +20052,36 @@ function createRelayServer(opts) {
|
|
|
19495
20052
|
if (body.method === "git.push" || body.method === "git.fetch") {
|
|
19496
20053
|
if (body.method === "git.push") {
|
|
19497
20054
|
const params = body.params;
|
|
19498
|
-
const
|
|
19499
|
-
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19505
|
-
|
|
19506
|
-
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
if (verdict.answer !== "y") {
|
|
19510
|
-
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
20055
|
+
const worktree = resolveWorktree(reg, params?.path ?? "/workspace");
|
|
20056
|
+
const isAgentboxBranch = worktree?.branch.startsWith("agentbox/") ?? false;
|
|
20057
|
+
const tokenClaimed = typeof params?.hostInitiated === "string";
|
|
20058
|
+
const incomingHash = hashRpcParams(params);
|
|
20059
|
+
const hostInitiatedOk = !isAgentboxBranch && tokenClaimed && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, "git.push", incomingHash);
|
|
20060
|
+
if (!isAgentboxBranch && tokenClaimed && !hostInitiatedOk) {
|
|
20061
|
+
send(res, 500, {
|
|
20062
|
+
exitCode: 10,
|
|
20063
|
+
stdout: "",
|
|
20064
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
20065
|
+
});
|
|
19511
20066
|
return;
|
|
19512
20067
|
}
|
|
20068
|
+
if (!isAgentboxBranch && !hostInitiatedOk) {
|
|
20069
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
20070
|
+
kind: "confirm",
|
|
20071
|
+
message: `Allow git push from box ${reg.name}?`,
|
|
20072
|
+
detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
|
|
20073
|
+
defaultAnswer: "n",
|
|
20074
|
+
context: {
|
|
20075
|
+
command: "git push",
|
|
20076
|
+
cwd: params?.path,
|
|
20077
|
+
argv: params?.args
|
|
20078
|
+
}
|
|
20079
|
+
});
|
|
20080
|
+
if (verdict.answer !== "y") {
|
|
20081
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
20082
|
+
return;
|
|
20083
|
+
}
|
|
20084
|
+
}
|
|
19513
20085
|
}
|
|
19514
20086
|
const result = await handleGitRpc(reg, body.method, body.params);
|
|
19515
20087
|
const status2 = result.exitCode === 0 ? 200 : 500;
|
|
@@ -19542,6 +20114,33 @@ function createRelayServer(opts) {
|
|
|
19542
20114
|
send(res, status2, result);
|
|
19543
20115
|
return;
|
|
19544
20116
|
}
|
|
20117
|
+
if (body.method.startsWith("gh.pr.")) {
|
|
20118
|
+
const op = body.method.slice("gh.pr.".length);
|
|
20119
|
+
if (!isGhPrOp(op)) {
|
|
20120
|
+
send(res, 400, { error: `unknown gh.pr.* op: ${op}` });
|
|
20121
|
+
return;
|
|
20122
|
+
}
|
|
20123
|
+
const result = await handleGhPrRpc(
|
|
20124
|
+
op,
|
|
20125
|
+
reg,
|
|
20126
|
+
body.params,
|
|
20127
|
+
prompts,
|
|
20128
|
+
subscribers,
|
|
20129
|
+
hostInitiatedTokens
|
|
20130
|
+
);
|
|
20131
|
+
const status2 = result.exitCode === 0 ? 200 : 500;
|
|
20132
|
+
send(res, status2, result);
|
|
20133
|
+
return;
|
|
20134
|
+
}
|
|
20135
|
+
if (body.method === "git.clone" || body.method === "gh.repo.clone") {
|
|
20136
|
+
send(res, 501, {
|
|
20137
|
+
exitCode: 64,
|
|
20138
|
+
stdout: "",
|
|
20139
|
+
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.
|
|
20140
|
+
`
|
|
20141
|
+
});
|
|
20142
|
+
return;
|
|
20143
|
+
}
|
|
19545
20144
|
if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
|
|
19546
20145
|
const params = body.params;
|
|
19547
20146
|
const kind = body.method.split(".")[1] ?? "workspace";
|
|
@@ -19678,6 +20277,7 @@ function createRelayServer(opts) {
|
|
|
19678
20277
|
boxName: reg.name,
|
|
19679
20278
|
prompts,
|
|
19680
20279
|
subscribers,
|
|
20280
|
+
hostInitiatedTokens,
|
|
19681
20281
|
log
|
|
19682
20282
|
});
|
|
19683
20283
|
await respond(result);
|
|
@@ -19690,6 +20290,11 @@ function createRelayServer(opts) {
|
|
|
19690
20290
|
});
|
|
19691
20291
|
}
|
|
19692
20292
|
} : void 0,
|
|
20293
|
+
// Self-heal a dead preview transport (hetzner SSH `-L` after a
|
|
20294
|
+
// ControlMaster death). The relay strips the `cloud:` prefix
|
|
20295
|
+
// the cloud-provider tags onto BoxRecord.container — what the
|
|
20296
|
+
// backend's `get(sandboxId)` expects is the bare sandbox id.
|
|
20297
|
+
recoverPreviewUrl: reg.backend ? async () => refreshCloudPreviewUrl(reg.backend, reg.boxId, DEFAULT_BOX_RELAY_PORT) : void 0,
|
|
19693
20298
|
logger: log
|
|
19694
20299
|
});
|
|
19695
20300
|
} catch (err) {
|
|
@@ -19804,6 +20409,27 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19804
20409
|
send(res, 204, null);
|
|
19805
20410
|
return;
|
|
19806
20411
|
}
|
|
20412
|
+
if (route === "POST /admin/host-initiated/mint") {
|
|
20413
|
+
const body = await readJsonBody(req);
|
|
20414
|
+
if (!body || typeof body.boxId !== "string" || body.boxId.length === 0 || typeof body.method !== "string" || body.method.length === 0) {
|
|
20415
|
+
send(res, 400, { error: "expected {boxId, method, paramsHash, ttlMs?}" });
|
|
20416
|
+
return;
|
|
20417
|
+
}
|
|
20418
|
+
let paramsHash;
|
|
20419
|
+
if (body.paramsHash === null || body.paramsHash === void 0) {
|
|
20420
|
+
paramsHash = null;
|
|
20421
|
+
} else if (typeof body.paramsHash === "string" && /^[0-9a-f]{64}$/.test(body.paramsHash)) {
|
|
20422
|
+
paramsHash = body.paramsHash;
|
|
20423
|
+
} else {
|
|
20424
|
+
send(res, 400, { error: "paramsHash must be a 64-hex sha256 string or null" });
|
|
20425
|
+
return;
|
|
20426
|
+
}
|
|
20427
|
+
const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
|
|
20428
|
+
const token = hostInitiatedTokens.mint(body.boxId, body.method, paramsHash, ttlMs);
|
|
20429
|
+
log(`host-initiated-mint box=${body.boxId} method=${body.method} paramsBound=${paramsHash !== null}`);
|
|
20430
|
+
send(res, 200, { token });
|
|
20431
|
+
return;
|
|
20432
|
+
}
|
|
19807
20433
|
if (route === "POST /admin/notices/set") {
|
|
19808
20434
|
const body = await readJsonBody(req);
|
|
19809
20435
|
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) {
|
|
@@ -19816,6 +20442,17 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19816
20442
|
send(res, 200, { id });
|
|
19817
20443
|
return;
|
|
19818
20444
|
}
|
|
20445
|
+
if (route === "POST /admin/queue/enqueue") {
|
|
20446
|
+
const body = await readJsonBody(req);
|
|
20447
|
+
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
20448
|
+
send(res, 400, { error: "expected {id}" });
|
|
20449
|
+
return;
|
|
20450
|
+
}
|
|
20451
|
+
log(`queue-enqueue id=${body.id}`);
|
|
20452
|
+
queuePoke?.();
|
|
20453
|
+
send(res, 204, null);
|
|
20454
|
+
return;
|
|
20455
|
+
}
|
|
19819
20456
|
if (route === "POST /admin/notices/clear") {
|
|
19820
20457
|
const body = await readJsonBody(req);
|
|
19821
20458
|
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
@@ -19852,6 +20489,9 @@ data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
|
19852
20489
|
notices,
|
|
19853
20490
|
hostActions: hostActions ?? void 0,
|
|
19854
20491
|
url: `http://${host}:${String(opts.port)}`,
|
|
20492
|
+
setQueuePoke: (fn) => {
|
|
20493
|
+
queuePoke = fn;
|
|
20494
|
+
},
|
|
19855
20495
|
close: async () => {
|
|
19856
20496
|
if (pollers) await pollers.stopAll();
|
|
19857
20497
|
await new Promise((resolve2, reject) => {
|
|
@@ -19903,7 +20543,69 @@ async function handleGitRpc(reg, method, params) {
|
|
|
19903
20543
|
if (typeof a2 === "string") argv.push(a2);
|
|
19904
20544
|
}
|
|
19905
20545
|
}
|
|
19906
|
-
|
|
20546
|
+
const result = await runHostCommand(argv);
|
|
20547
|
+
if (method === "git.push" && result.exitCode === 0 && !worktree.branch.startsWith("agentbox/")) {
|
|
20548
|
+
await runHostCommand([
|
|
20549
|
+
"git",
|
|
20550
|
+
"-C",
|
|
20551
|
+
worktree.hostMainRepo,
|
|
20552
|
+
"branch",
|
|
20553
|
+
`--set-upstream-to=${remote}/${worktree.branch}`,
|
|
20554
|
+
worktree.branch
|
|
20555
|
+
]);
|
|
20556
|
+
}
|
|
20557
|
+
return result;
|
|
20558
|
+
}
|
|
20559
|
+
async function handleGhPrRpc(op, reg, params, prompts, subscribers, hostInitiatedTokens) {
|
|
20560
|
+
const mergeBypass = refuseMergeBypass(op);
|
|
20561
|
+
if (mergeBypass) return mergeBypass;
|
|
20562
|
+
const checkoutOptIn = refuseCheckoutByDefault(op);
|
|
20563
|
+
if (checkoutOptIn) return checkoutOptIn;
|
|
20564
|
+
const containerPath = params?.path ?? "/workspace";
|
|
20565
|
+
const worktree = resolveWorktree(reg, containerPath);
|
|
20566
|
+
if (!worktree) {
|
|
20567
|
+
return {
|
|
20568
|
+
exitCode: 64,
|
|
20569
|
+
stdout: "",
|
|
20570
|
+
stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
|
|
20571
|
+
};
|
|
20572
|
+
}
|
|
20573
|
+
const ghReady = await assertGhReady();
|
|
20574
|
+
if (ghReady) return ghReady;
|
|
20575
|
+
const args = Array.isArray(params?.args) ? params.args.filter((a2) => typeof a2 === "string") : [];
|
|
20576
|
+
if (op === "checkout") {
|
|
20577
|
+
const branches = (reg.worktrees ?? []).map((w) => w.branch);
|
|
20578
|
+
const guard = await checkoutGuards(worktree.hostMainRepo, branches);
|
|
20579
|
+
if (guard) return guard;
|
|
20580
|
+
}
|
|
20581
|
+
const tokenClaimedGh = typeof params?.hostInitiated === "string";
|
|
20582
|
+
const incomingHashGh = hashRpcParams(params);
|
|
20583
|
+
const hostInitiatedOk = !GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && hostInitiatedTokens.consume(params?.hostInitiated, reg.boxId, `gh.pr.${op}`, incomingHashGh);
|
|
20584
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && tokenClaimedGh && !hostInitiatedOk) {
|
|
20585
|
+
return {
|
|
20586
|
+
exitCode: 10,
|
|
20587
|
+
stdout: "",
|
|
20588
|
+
stderr: "host-initiated token rejected: invalid, expired, or bound to different params\n"
|
|
20589
|
+
};
|
|
20590
|
+
}
|
|
20591
|
+
if (!GH_PR_READ_ONLY_OPS.has(op) && !hostInitiatedOk) {
|
|
20592
|
+
const detail = args.join(" ").slice(0, 200);
|
|
20593
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
20594
|
+
kind: "confirm",
|
|
20595
|
+
message: `Allow gh pr ${op} from box ${reg.name}?`,
|
|
20596
|
+
detail,
|
|
20597
|
+
defaultAnswer: "n",
|
|
20598
|
+
context: {
|
|
20599
|
+
command: `gh pr ${op}`,
|
|
20600
|
+
cwd: containerPath,
|
|
20601
|
+
argv: args
|
|
20602
|
+
}
|
|
20603
|
+
});
|
|
20604
|
+
if (verdict.answer !== "y") {
|
|
20605
|
+
return { exitCode: 10, stdout: "", stderr: "denied by user\n" };
|
|
20606
|
+
}
|
|
20607
|
+
}
|
|
20608
|
+
return runHostGh(["pr", op, ...args], worktree.hostMainRepo);
|
|
19907
20609
|
}
|
|
19908
20610
|
async function handleCpRpc(reg, method, params) {
|
|
19909
20611
|
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
@@ -19964,7 +20666,7 @@ function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
|
|
|
19964
20666
|
resolve2({ exitCode: 64, stdout: "", stderr: "empty command" });
|
|
19965
20667
|
return;
|
|
19966
20668
|
}
|
|
19967
|
-
const child = (0,
|
|
20669
|
+
const child = (0, import_child_process2.spawn)(cmd, rest, {
|
|
19968
20670
|
env: process.env,
|
|
19969
20671
|
stdio: ["ignore", "pipe", "pipe"]
|
|
19970
20672
|
});
|
|
@@ -20011,6 +20713,8 @@ async function startRelayServer(opts) {
|
|
|
20011
20713
|
});
|
|
20012
20714
|
return handle;
|
|
20013
20715
|
}
|
|
20716
|
+
var QUEUE_DIR = (0, import_path6.join)(STATE_DIR, "queue");
|
|
20717
|
+
var QUEUE_LOGS_DIR = (0, import_path6.join)(STATE_DIR, "logs");
|
|
20014
20718
|
|
|
20015
20719
|
// src/config.ts
|
|
20016
20720
|
var import_promises16 = require("fs/promises");
|
|
@@ -20321,7 +21025,7 @@ function assertBool(raw, where) {
|
|
|
20321
21025
|
if (typeof raw !== "boolean") throw new ConfigError(`${where} must be a boolean`);
|
|
20322
21026
|
return raw;
|
|
20323
21027
|
}
|
|
20324
|
-
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults"]);
|
|
21028
|
+
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults", "carry"]);
|
|
20325
21029
|
function validateUnitGraph(tasks, services) {
|
|
20326
21030
|
const names = /* @__PURE__ */ new Set();
|
|
20327
21031
|
for (const t of tasks) {
|
|
@@ -20441,8 +21145,95 @@ function describeCommand(cmd) {
|
|
|
20441
21145
|
return Array.isArray(cmd) ? cmd.join(" ") : cmd;
|
|
20442
21146
|
}
|
|
20443
21147
|
|
|
20444
|
-
// src/
|
|
21148
|
+
// src/codex-scraper.ts
|
|
20445
21149
|
var import_node_child_process7 = require("child_process");
|
|
21150
|
+
var DEFAULT_INTERVAL_MS = 1e3;
|
|
21151
|
+
var DEFAULT_SESSION = "codex";
|
|
21152
|
+
var PATTERNS = [
|
|
21153
|
+
// Permission / approval prompts (highest priority — these mean codex is blocked).
|
|
21154
|
+
{ re: /Hooks need review|Trust all and continue/i, state: "waiting" },
|
|
21155
|
+
{ re: /Do you trust the contents of this directory/i, state: "waiting" },
|
|
21156
|
+
{ re: /Allow this command\?|Approve this (command|tool)\?|Press y\/n|\[Y\/n\]/m, state: "waiting" },
|
|
21157
|
+
{ re: /Waiting for (your |user )?(response|input|approval|permission)/i, state: "waiting" },
|
|
21158
|
+
// Compaction (codex's `/compact` command and auto-compaction).
|
|
21159
|
+
{ re: /Compacting (conversation|context)|Summariz(e|ing) (the )?conversation/i, state: "compacting" },
|
|
21160
|
+
// Failure / fatal-error frames.
|
|
21161
|
+
{ re: /\bError:|\bFailed:|^Traceback /m, state: "error" },
|
|
21162
|
+
// Active work signals — pinned to specific codex TUI fragments to avoid
|
|
21163
|
+
// matching every line of english that contains "working" or "running"
|
|
21164
|
+
// (e.g. the directory-trust prompt's "Working with untrusted contents"
|
|
21165
|
+
// warning is NOT a working state).
|
|
21166
|
+
{
|
|
21167
|
+
re: /\b(Thinking\.\.\.|Worked for \d|Streaming response|tool call \w|Running command|Generating response|Reasoning\.\.\.|Editing \w)/m,
|
|
21168
|
+
state: "working"
|
|
21169
|
+
},
|
|
21170
|
+
// Idle: codex shows a status line `gpt-5.5 high · /workspace` (or similar
|
|
21171
|
+
// model · cwd footer) at the bottom of the input prompt when ready for
|
|
21172
|
+
// input. Lower priority than every "busy" pattern above so an in-flight
|
|
21173
|
+
// turn that still shows the footer correctly registers as working.
|
|
21174
|
+
{ re: /gpt-\d+(\.\d+)?(-\w+)?\s+(low|medium|high|xhigh)\b|OpenAI Codex \(v\d/i, state: "idle" }
|
|
21175
|
+
];
|
|
21176
|
+
function startCodexScraper(opts) {
|
|
21177
|
+
const sessionName = opts.sessionName ?? DEFAULT_SESSION;
|
|
21178
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
21179
|
+
const capture = opts.capturePane ?? defaultCapturePane;
|
|
21180
|
+
let lastState = null;
|
|
21181
|
+
let lastSessionPresent = false;
|
|
21182
|
+
let stopped = false;
|
|
21183
|
+
const tick = async () => {
|
|
21184
|
+
if (stopped) return;
|
|
21185
|
+
try {
|
|
21186
|
+
const pane = await capture(sessionName);
|
|
21187
|
+
if (pane === null) {
|
|
21188
|
+
lastSessionPresent = false;
|
|
21189
|
+
return;
|
|
21190
|
+
}
|
|
21191
|
+
if (!lastSessionPresent) {
|
|
21192
|
+
opts.reporter.setCodexState("idle");
|
|
21193
|
+
lastState = "idle";
|
|
21194
|
+
lastSessionPresent = true;
|
|
21195
|
+
}
|
|
21196
|
+
const matched = matchState(pane);
|
|
21197
|
+
if (matched !== null && matched !== lastState) {
|
|
21198
|
+
opts.reporter.setCodexState(matched);
|
|
21199
|
+
lastState = matched;
|
|
21200
|
+
}
|
|
21201
|
+
} catch {
|
|
21202
|
+
}
|
|
21203
|
+
};
|
|
21204
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
21205
|
+
timer.unref();
|
|
21206
|
+
void tick();
|
|
21207
|
+
return {
|
|
21208
|
+
stop() {
|
|
21209
|
+
stopped = true;
|
|
21210
|
+
clearInterval(timer);
|
|
21211
|
+
}
|
|
21212
|
+
};
|
|
21213
|
+
}
|
|
21214
|
+
function matchState(pane) {
|
|
21215
|
+
for (const { re, state } of PATTERNS) {
|
|
21216
|
+
if (re.test(pane)) return state;
|
|
21217
|
+
}
|
|
21218
|
+
return null;
|
|
21219
|
+
}
|
|
21220
|
+
function defaultCapturePane(sessionName) {
|
|
21221
|
+
return new Promise((resolve2) => {
|
|
21222
|
+
const child = (0, import_node_child_process7.spawn)("tmux", ["capture-pane", "-p", "-t", sessionName], {
|
|
21223
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
21224
|
+
});
|
|
21225
|
+
let stdout = "";
|
|
21226
|
+
child.stdout.on("data", (b) => stdout += b.toString("utf8"));
|
|
21227
|
+
child.on("error", () => resolve2(null));
|
|
21228
|
+
child.on("close", (code) => {
|
|
21229
|
+
if (code === 0) resolve2(stdout);
|
|
21230
|
+
else resolve2(null);
|
|
21231
|
+
});
|
|
21232
|
+
});
|
|
21233
|
+
}
|
|
21234
|
+
|
|
21235
|
+
// src/supervisor.ts
|
|
21236
|
+
var import_node_child_process8 = require("child_process");
|
|
20446
21237
|
var import_node_events15 = require("events");
|
|
20447
21238
|
var import_node_fs7 = require("fs");
|
|
20448
21239
|
var import_promises18 = require("fs/promises");
|
|
@@ -20708,7 +21499,7 @@ var cachedLoginPath;
|
|
|
20708
21499
|
function loginShellPath() {
|
|
20709
21500
|
if (cachedLoginPath !== void 0) return cachedLoginPath;
|
|
20710
21501
|
try {
|
|
20711
|
-
const out = (0,
|
|
21502
|
+
const out = (0, import_node_child_process8.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
|
|
20712
21503
|
encoding: "utf8",
|
|
20713
21504
|
timeout: 5e3
|
|
20714
21505
|
}).trim();
|
|
@@ -20723,7 +21514,7 @@ var ServiceRunner = class extends import_node_events15.EventEmitter {
|
|
|
20723
21514
|
super();
|
|
20724
21515
|
this.spec = spec;
|
|
20725
21516
|
this.opts = opts;
|
|
20726
|
-
this.spawnFn = opts.spawn ??
|
|
21517
|
+
this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
|
|
20727
21518
|
this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
20728
21519
|
this.clearTimer = opts.clearTimer ?? ((h2) => {
|
|
20729
21520
|
clearTimeout(h2);
|
|
@@ -20954,7 +21745,7 @@ var TaskRunner = class extends import_node_events15.EventEmitter {
|
|
|
20954
21745
|
super();
|
|
20955
21746
|
this.spec = spec;
|
|
20956
21747
|
this.opts = opts;
|
|
20957
|
-
this.spawnFn = opts.spawn ??
|
|
21748
|
+
this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
|
|
20958
21749
|
}
|
|
20959
21750
|
spec;
|
|
20960
21751
|
opts;
|
|
@@ -21502,10 +22293,10 @@ var import_promises19 = require("fs/promises");
|
|
|
21502
22293
|
var import_node_path7 = require("path");
|
|
21503
22294
|
|
|
21504
22295
|
// src/status-reporter.ts
|
|
21505
|
-
var
|
|
22296
|
+
var import_node_child_process10 = require("child_process");
|
|
21506
22297
|
|
|
21507
22298
|
// src/tmux.ts
|
|
21508
|
-
var
|
|
22299
|
+
var import_node_child_process9 = require("child_process");
|
|
21509
22300
|
var import_node_os4 = require("os");
|
|
21510
22301
|
var MAX_TITLE_LEN = 120;
|
|
21511
22302
|
function sanitizePaneTitle(raw, ctx) {
|
|
@@ -21519,7 +22310,7 @@ function sanitizePaneTitle(raw, ctx) {
|
|
|
21519
22310
|
}
|
|
21520
22311
|
function runTool(cmd, args) {
|
|
21521
22312
|
return new Promise((resolve2) => {
|
|
21522
|
-
const child = (0,
|
|
22313
|
+
const child = (0, import_node_child_process9.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
21523
22314
|
let stdout = "";
|
|
21524
22315
|
let stderr = "";
|
|
21525
22316
|
child.stdout.on("data", (b) => stdout += b.toString("utf8"));
|
|
@@ -21565,8 +22356,12 @@ var StatusReporter = class {
|
|
|
21565
22356
|
periodicMs;
|
|
21566
22357
|
claudeState = "unknown";
|
|
21567
22358
|
claudeUpdatedAt = null;
|
|
22359
|
+
claudePlan;
|
|
22360
|
+
claudeQuestion;
|
|
21568
22361
|
codexState = "unknown";
|
|
21569
22362
|
codexUpdatedAt = null;
|
|
22363
|
+
opencodeState = "unknown";
|
|
22364
|
+
opencodeUpdatedAt = null;
|
|
21570
22365
|
debounceTimer = null;
|
|
21571
22366
|
periodicTimer = null;
|
|
21572
22367
|
onChange = () => this.schedulePush();
|
|
@@ -21595,9 +22390,21 @@ var StatusReporter = class {
|
|
|
21595
22390
|
this.periodicTimer = null;
|
|
21596
22391
|
}
|
|
21597
22392
|
}
|
|
21598
|
-
setClaudeState(state) {
|
|
22393
|
+
setClaudeState(state, payload) {
|
|
22394
|
+
const sticky = this.claudeState === "end-plan" || this.claudeState === "question";
|
|
22395
|
+
if (state === "working" && sticky && !payload?.clearPending) return;
|
|
21599
22396
|
this.claudeState = state;
|
|
21600
22397
|
this.claudeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
22398
|
+
if (payload?.clearPending) {
|
|
22399
|
+
this.claudePlan = void 0;
|
|
22400
|
+
this.claudeQuestion = void 0;
|
|
22401
|
+
}
|
|
22402
|
+
if (state === "end-plan" && payload?.plan) {
|
|
22403
|
+
this.claudePlan = payload.plan;
|
|
22404
|
+
}
|
|
22405
|
+
if (state === "question" && payload?.question) {
|
|
22406
|
+
this.claudeQuestion = payload.question;
|
|
22407
|
+
}
|
|
21601
22408
|
this.schedulePush();
|
|
21602
22409
|
}
|
|
21603
22410
|
setCodexState(state) {
|
|
@@ -21605,6 +22412,11 @@ var StatusReporter = class {
|
|
|
21605
22412
|
this.codexUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21606
22413
|
this.schedulePush();
|
|
21607
22414
|
}
|
|
22415
|
+
setOpencodeState(state) {
|
|
22416
|
+
this.opencodeState = state;
|
|
22417
|
+
this.opencodeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
22418
|
+
this.schedulePush();
|
|
22419
|
+
}
|
|
21608
22420
|
/** Forced immediate push (used on shutdown). */
|
|
21609
22421
|
flush() {
|
|
21610
22422
|
if (this.debounceTimer) {
|
|
@@ -21654,7 +22466,9 @@ var StatusReporter = class {
|
|
|
21654
22466
|
state: this.claudeState,
|
|
21655
22467
|
updatedAt: this.claudeUpdatedAt,
|
|
21656
22468
|
sessionRunning: claudeSession2.running,
|
|
21657
|
-
...claudeSession2.title ? { sessionTitle: claudeSession2.title } : {}
|
|
22469
|
+
...claudeSession2.title ? { sessionTitle: claudeSession2.title } : {},
|
|
22470
|
+
...this.claudePlan ? { plan: this.claudePlan } : {},
|
|
22471
|
+
...this.claudeQuestion ? { question: this.claudeQuestion } : {}
|
|
21658
22472
|
}
|
|
21659
22473
|
};
|
|
21660
22474
|
if (codexSession.running || this.codexState !== "unknown") {
|
|
@@ -21665,9 +22479,11 @@ var StatusReporter = class {
|
|
|
21665
22479
|
...codexSession.title ? { sessionTitle: codexSession.title } : {}
|
|
21666
22480
|
};
|
|
21667
22481
|
}
|
|
21668
|
-
if (opencodeSession.running) {
|
|
22482
|
+
if (opencodeSession.running || this.opencodeState !== "unknown") {
|
|
21669
22483
|
status2.opencode = {
|
|
21670
|
-
|
|
22484
|
+
state: this.opencodeState,
|
|
22485
|
+
updatedAt: this.opencodeUpdatedAt,
|
|
22486
|
+
sessionRunning: opencodeSession.running,
|
|
21671
22487
|
...opencodeSession.title ? { sessionTitle: opencodeSession.title } : {}
|
|
21672
22488
|
};
|
|
21673
22489
|
}
|
|
@@ -21687,7 +22503,7 @@ async function collectPorts(supervisor) {
|
|
|
21687
22503
|
}
|
|
21688
22504
|
function run(cmd, args) {
|
|
21689
22505
|
return new Promise((resolve2) => {
|
|
21690
|
-
const child = (0,
|
|
22506
|
+
const child = (0, import_node_child_process10.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
21691
22507
|
let stdout = "";
|
|
21692
22508
|
child.stdout.on("data", (b) => stdout += b.toString("utf8"));
|
|
21693
22509
|
child.on("error", () => resolve2({ exitCode: 127, stdout }));
|
|
@@ -21844,7 +22660,11 @@ async function handleConnection(sock, opts) {
|
|
|
21844
22660
|
if (!CLAUDE_ACTIVITY_STATES.includes(req.state)) {
|
|
21845
22661
|
writeLine(sock, { ok: false, error: `invalid claude state: ${String(req.state)}` });
|
|
21846
22662
|
} else {
|
|
21847
|
-
opts.reporter?.setClaudeState(req.state
|
|
22663
|
+
opts.reporter?.setClaudeState(req.state, {
|
|
22664
|
+
plan: req.plan,
|
|
22665
|
+
question: req.question,
|
|
22666
|
+
clearPending: req.clearPending
|
|
22667
|
+
});
|
|
21848
22668
|
writeLine(sock, { ok: true, data: "ok" });
|
|
21849
22669
|
}
|
|
21850
22670
|
sock.end();
|
|
@@ -21860,6 +22680,16 @@ async function handleConnection(sock, opts) {
|
|
|
21860
22680
|
sock.end();
|
|
21861
22681
|
return;
|
|
21862
22682
|
}
|
|
22683
|
+
case "opencode-state": {
|
|
22684
|
+
if (!CLAUDE_ACTIVITY_STATES.includes(req.state)) {
|
|
22685
|
+
writeLine(sock, { ok: false, error: `invalid opencode state: ${String(req.state)}` });
|
|
22686
|
+
} else {
|
|
22687
|
+
opts.reporter?.setOpencodeState(req.state);
|
|
22688
|
+
writeLine(sock, { ok: true, data: "ok" });
|
|
22689
|
+
}
|
|
22690
|
+
sock.end();
|
|
22691
|
+
return;
|
|
22692
|
+
}
|
|
21863
22693
|
default: {
|
|
21864
22694
|
writeLine(sock, { ok: false, error: `unknown op` });
|
|
21865
22695
|
sock.end();
|
|
@@ -21913,7 +22743,85 @@ async function* createLineReader(sock) {
|
|
|
21913
22743
|
if (buf.length > 0) yield buf;
|
|
21914
22744
|
}
|
|
21915
22745
|
|
|
22746
|
+
// src/box-relay-forwarder.ts
|
|
22747
|
+
var import_node_http3 = require("http");
|
|
22748
|
+
var ALLOWED_PATHS = /* @__PURE__ */ new Set(["/rpc", "/events"]);
|
|
22749
|
+
function startBoxRelayForwarder(opts) {
|
|
22750
|
+
const log = opts.logger ?? (() => {
|
|
22751
|
+
});
|
|
22752
|
+
const upstream = opts.upstream;
|
|
22753
|
+
const upstreamPort = upstream.port.length > 0 ? Number.parseInt(upstream.port, 10) : upstream.protocol === "https:" ? 443 : 80;
|
|
22754
|
+
const server = (0, import_node_http3.createServer)((req, res) => {
|
|
22755
|
+
const path6 = (req.url ?? "").split("?")[0] ?? "";
|
|
22756
|
+
if (req.method !== "POST" || !ALLOWED_PATHS.has(path6)) {
|
|
22757
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
22758
|
+
res.end("not found");
|
|
22759
|
+
return;
|
|
22760
|
+
}
|
|
22761
|
+
const headers = { ...req.headers };
|
|
22762
|
+
delete headers.host;
|
|
22763
|
+
delete headers.connection;
|
|
22764
|
+
const upstreamReq = (0, import_node_http3.request)(
|
|
22765
|
+
{
|
|
22766
|
+
host: upstream.hostname,
|
|
22767
|
+
port: upstreamPort,
|
|
22768
|
+
method: "POST",
|
|
22769
|
+
path: `${upstream.pathname.replace(/\/$/, "")}${path6}`,
|
|
22770
|
+
headers,
|
|
22771
|
+
// No keep-alive: the relay holds /rpc open for the lifetime of a
|
|
22772
|
+
// host prompt (potentially many seconds). Reusing sockets across
|
|
22773
|
+
// such calls invites mid-stream resets on Node version drift.
|
|
22774
|
+
agent: false
|
|
22775
|
+
},
|
|
22776
|
+
(upstreamRes) => {
|
|
22777
|
+
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
22778
|
+
upstreamRes.pipe(res);
|
|
22779
|
+
}
|
|
22780
|
+
);
|
|
22781
|
+
upstreamReq.on("error", (err) => {
|
|
22782
|
+
log(`upstream error on ${path6}: ${err.message}`);
|
|
22783
|
+
if (!res.headersSent) {
|
|
22784
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
22785
|
+
}
|
|
22786
|
+
res.end();
|
|
22787
|
+
});
|
|
22788
|
+
req.on("error", (err) => {
|
|
22789
|
+
log(`client error on ${path6}: ${err.message}`);
|
|
22790
|
+
upstreamReq.destroy();
|
|
22791
|
+
});
|
|
22792
|
+
req.pipe(upstreamReq);
|
|
22793
|
+
});
|
|
22794
|
+
return new Promise((resolve2, reject) => {
|
|
22795
|
+
const onError = (err) => {
|
|
22796
|
+
reject(err);
|
|
22797
|
+
};
|
|
22798
|
+
server.once("error", onError);
|
|
22799
|
+
server.listen(opts.port, "127.0.0.1", () => {
|
|
22800
|
+
server.removeListener("error", onError);
|
|
22801
|
+
resolve2({
|
|
22802
|
+
url: `http://127.0.0.1:${String(opts.port)}`,
|
|
22803
|
+
close: () => new Promise((res) => {
|
|
22804
|
+
server.close(() => res());
|
|
22805
|
+
})
|
|
22806
|
+
});
|
|
22807
|
+
});
|
|
22808
|
+
});
|
|
22809
|
+
}
|
|
22810
|
+
|
|
21916
22811
|
// src/commands/daemon.ts
|
|
22812
|
+
function resolveBoxRelayPort() {
|
|
22813
|
+
const raw = process.env.AGENTBOX_BOX_RELAY_PORT;
|
|
22814
|
+
if (raw === void 0 || raw.length === 0) return DEFAULT_BOX_RELAY_PORT;
|
|
22815
|
+
const n2 = Number.parseInt(raw, 10);
|
|
22816
|
+
if (!Number.isFinite(n2) || n2 < 1 || n2 > 65535) {
|
|
22817
|
+
process.stderr.write(
|
|
22818
|
+
`agentbox-ctl: AGENTBOX_BOX_RELAY_PORT=${raw} is not a valid port; falling back to ${String(DEFAULT_BOX_RELAY_PORT)}
|
|
22819
|
+
`
|
|
22820
|
+
);
|
|
22821
|
+
return DEFAULT_BOX_RELAY_PORT;
|
|
22822
|
+
}
|
|
22823
|
+
return n2;
|
|
22824
|
+
}
|
|
21917
22825
|
var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supervisor in the foreground").option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).option("--config <path>", "path to agentbox.yaml", DEFAULT_CONFIG_PATH).option("--log-dir <path>", "where per-service log files are written", DEFAULT_LOG_DIR).option("--workspace <path>", "cwd for service processes", "/workspace").action(async (opts) => {
|
|
21918
22826
|
const cfg = await loadConfig(opts.config);
|
|
21919
22827
|
const sup = new Supervisor({ workspace: opts.workspace, logDir: opts.logDir });
|
|
@@ -21925,6 +22833,14 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
|
|
|
21925
22833
|
sessionName: DEFAULT_CLAUDE_SESSION_NAME
|
|
21926
22834
|
});
|
|
21927
22835
|
reporter.start();
|
|
22836
|
+
let codexScraper = null;
|
|
22837
|
+
try {
|
|
22838
|
+
codexScraper = startCodexScraper({ reporter });
|
|
22839
|
+
} catch (err) {
|
|
22840
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22841
|
+
process.stderr.write(`agentbox-ctl: codex scraper failed to start: ${msg}
|
|
22842
|
+
`);
|
|
22843
|
+
}
|
|
21928
22844
|
const server = await startServer({
|
|
21929
22845
|
socketPath: opts.socket,
|
|
21930
22846
|
supervisor: sup,
|
|
@@ -21938,7 +22854,9 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
|
|
|
21938
22854
|
`agentbox-ctl: ${String(cfg.services.length)} service(s), ${String(cfg.tasks.length)} task(s) configured
|
|
21939
22855
|
`
|
|
21940
22856
|
);
|
|
22857
|
+
const boxRelayPort = resolveBoxRelayPort();
|
|
21941
22858
|
let inBoxRelay = null;
|
|
22859
|
+
let inBoxForwarder = null;
|
|
21942
22860
|
if (process.env.AGENTBOX_BOX_KIND === "cloud") {
|
|
21943
22861
|
const bridgeToken = process.env.AGENTBOX_BRIDGE_TOKEN ?? "";
|
|
21944
22862
|
const boxId = process.env.AGENTBOX_BOX_ID ?? "";
|
|
@@ -21951,7 +22869,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
|
|
|
21951
22869
|
} else {
|
|
21952
22870
|
try {
|
|
21953
22871
|
inBoxRelay = await startRelayServer({
|
|
21954
|
-
port:
|
|
22872
|
+
port: boxRelayPort,
|
|
21955
22873
|
host: "0.0.0.0",
|
|
21956
22874
|
mode: "box",
|
|
21957
22875
|
bridgeToken,
|
|
@@ -21966,7 +22884,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
|
|
|
21966
22884
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21967
22885
|
});
|
|
21968
22886
|
process.stdout.write(
|
|
21969
|
-
`agentbox-ctl: in-sandbox relay (mode=box) listening on :${String(
|
|
22887
|
+
`agentbox-ctl: in-sandbox relay (mode=box) listening on :${String(boxRelayPort)}
|
|
21970
22888
|
`
|
|
21971
22889
|
);
|
|
21972
22890
|
} catch (err) {
|
|
@@ -21975,15 +22893,35 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
|
|
|
21975
22893
|
`);
|
|
21976
22894
|
}
|
|
21977
22895
|
}
|
|
22896
|
+
} else {
|
|
22897
|
+
const upstreamUrl = process.env.AGENTBOX_HOST_RELAY_URL ?? "http://host.docker.internal:8787";
|
|
22898
|
+
try {
|
|
22899
|
+
inBoxForwarder = await startBoxRelayForwarder({
|
|
22900
|
+
port: boxRelayPort,
|
|
22901
|
+
upstream: new URL(upstreamUrl),
|
|
22902
|
+
logger: (line) => process.stdout.write(`relay(fwd): ${line}
|
|
22903
|
+
`)
|
|
22904
|
+
});
|
|
22905
|
+
process.stdout.write(
|
|
22906
|
+
`agentbox-ctl: in-box relay forwarder listening on :${String(boxRelayPort)} -> ${upstreamUrl}
|
|
22907
|
+
`
|
|
22908
|
+
);
|
|
22909
|
+
} catch (err) {
|
|
22910
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22911
|
+
process.stderr.write(`agentbox-ctl: in-box relay forwarder failed to start: ${msg}
|
|
22912
|
+
`);
|
|
22913
|
+
}
|
|
21978
22914
|
}
|
|
21979
22915
|
const shutdown = async (signal) => {
|
|
21980
22916
|
process.stdout.write(`agentbox-ctl: ${signal} \u2014 shutting down
|
|
21981
22917
|
`);
|
|
22918
|
+
if (codexScraper) codexScraper.stop();
|
|
21982
22919
|
reporter.stop();
|
|
21983
22920
|
reporter.flush();
|
|
21984
22921
|
server.close();
|
|
21985
22922
|
await sup.stopAll();
|
|
21986
22923
|
if (inBoxRelay) await inBoxRelay.close();
|
|
22924
|
+
if (inBoxForwarder) await inBoxForwarder.close();
|
|
21987
22925
|
process.exit(0);
|
|
21988
22926
|
};
|
|
21989
22927
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
@@ -22030,17 +22968,95 @@ var checkpointCommand = new Command("checkpoint").description("Capture this box
|
|
|
22030
22968
|
}
|
|
22031
22969
|
);
|
|
22032
22970
|
|
|
22971
|
+
// src/commands/pr-subcommands.ts
|
|
22972
|
+
var PR_SUBCOMMANDS = [
|
|
22973
|
+
{
|
|
22974
|
+
op: "create",
|
|
22975
|
+
description: "Run `gh pr create` on the host (creates a PR for this box's branch). User is prompted on the host wrapper."
|
|
22976
|
+
},
|
|
22977
|
+
{ op: "view", description: "Run `gh pr view` on the host (read-only; no prompt)." },
|
|
22978
|
+
{ op: "list", description: "Run `gh pr list` on the host (read-only; no prompt)." },
|
|
22979
|
+
{ op: "comment", description: "Run `gh pr comment` on the host (prompted; visible to others)." },
|
|
22980
|
+
{ op: "review", description: "Run `gh pr review` on the host (prompted; visible to others)." },
|
|
22981
|
+
{
|
|
22982
|
+
op: "merge",
|
|
22983
|
+
description: "Run `gh pr merge` on the host (prompted; destructive \u2014 AGENTBOX_PROMPT=off bypass requires AGENTBOX_GH_FORCE=1)."
|
|
22984
|
+
},
|
|
22985
|
+
{
|
|
22986
|
+
op: "checkout",
|
|
22987
|
+
description: "Run `gh pr checkout` on the host (prompted + clean-tree guard; opt-in via AGENTBOX_GH_PR_CHECKOUT=allow because it switches the host main repo branch)."
|
|
22988
|
+
},
|
|
22989
|
+
{ op: "close", description: "Run `gh pr close` on the host (prompted)." },
|
|
22990
|
+
{ op: "reopen", description: "Run `gh pr reopen` on the host (prompted)." }
|
|
22991
|
+
];
|
|
22992
|
+
function buildPrCommand(errorPrefix) {
|
|
22993
|
+
const prCommand = new Command("pr").description(
|
|
22994
|
+
"PR operations via the host `gh` CLI (requires `gh` installed and `gh auth login` on the host)"
|
|
22995
|
+
);
|
|
22996
|
+
for (const spec of PR_SUBCOMMANDS) {
|
|
22997
|
+
prCommand.addCommand(
|
|
22998
|
+
new Command(spec.op).description(spec.description).option("--cwd <path>", "container path identifying which registered worktree to use").addOption(
|
|
22999
|
+
new Option(
|
|
23000
|
+
"--host-initiated-token <token>",
|
|
23001
|
+
"internal: one-time token from the host CLI; skips relay confirm prompt when valid"
|
|
23002
|
+
).hideHelp()
|
|
23003
|
+
).allowExcessArguments(true).allowUnknownOption(true).argument(
|
|
23004
|
+
"[args...]",
|
|
23005
|
+
"extra flags forwarded to `gh pr <op>` verbatim (e.g. `--title`, `--body`, `--label`, `--draft`, `--json`)."
|
|
23006
|
+
).action(async (args, opts) => {
|
|
23007
|
+
const params = { path: opts.cwd ?? process.cwd() };
|
|
23008
|
+
if (args.length > 0) params.args = args;
|
|
23009
|
+
if (opts.hostInitiatedToken) params.hostInitiated = opts.hostInitiatedToken;
|
|
23010
|
+
const code = await postRpcAndExit(`gh.pr.${spec.op}`, params, { errorPrefix });
|
|
23011
|
+
process.exit(code);
|
|
23012
|
+
})
|
|
23013
|
+
);
|
|
23014
|
+
}
|
|
23015
|
+
return prCommand;
|
|
23016
|
+
}
|
|
23017
|
+
|
|
23018
|
+
// src/commands/gh.ts
|
|
23019
|
+
var repoCommand = new Command("repo").description("GitHub repo operations via the host `gh` CLI (host runs `gh repo \u2026` then ships results to the box)").addCommand(
|
|
23020
|
+
new Command("clone").description(
|
|
23021
|
+
"Clone a github repo into the box via host `gh repo clone`. The host clones into a tmpdir with its creds, bundles, and ships the bundle back; the box materialises the working copy and resets origin to the original URL."
|
|
23022
|
+
).option("--cwd <path>", "container path identifying which registered worktree to use (default: cwd)").option("--branch <name>", "pass --branch <name> to host gh repo clone").option("--depth <n>", "pass --depth <n> to host gh repo clone").argument("<repo>", "github repo: owner/name shorthand or full URL").argument("[dir]", "target directory inside the box (default: derived from repo)").action(
|
|
23023
|
+
async (repo, dir, opts) => {
|
|
23024
|
+
const params = {
|
|
23025
|
+
path: opts.cwd ?? process.cwd(),
|
|
23026
|
+
repo
|
|
23027
|
+
};
|
|
23028
|
+
if (dir) params.targetPath = dir;
|
|
23029
|
+
const extra = [];
|
|
23030
|
+
if (opts.branch) extra.push("--branch", opts.branch);
|
|
23031
|
+
if (opts.depth) extra.push("--depth", opts.depth);
|
|
23032
|
+
if (extra.length > 0) params.args = extra;
|
|
23033
|
+
const code = await postRpcAndExit("gh.repo.clone", params, {
|
|
23034
|
+
errorPrefix: "agentbox-ctl gh repo clone"
|
|
23035
|
+
});
|
|
23036
|
+
process.exit(code);
|
|
23037
|
+
}
|
|
23038
|
+
)
|
|
23039
|
+
);
|
|
23040
|
+
var ghCommand = new Command("gh").description("GitHub CLI operations routed through the relay (host `gh` runs with host creds; box never sees a token)").addCommand(buildPrCommand("agentbox-ctl gh pr")).addCommand(repoCommand);
|
|
23041
|
+
|
|
22033
23042
|
// src/commands/git.ts
|
|
22034
|
-
var
|
|
23043
|
+
var import_node_child_process11 = require("child_process");
|
|
23044
|
+
function hostInitiatedOption() {
|
|
23045
|
+
return new Option(
|
|
23046
|
+
"--host-initiated-token <token>",
|
|
23047
|
+
"internal: one-time token from the host CLI; skips relay confirm prompt when valid"
|
|
23048
|
+
).hideHelp();
|
|
23049
|
+
}
|
|
22035
23050
|
function buildParams(opts, extra) {
|
|
22036
23051
|
const params = { path: opts.cwd ?? process.cwd() };
|
|
22037
23052
|
if (opts.remote) params.remote = opts.remote;
|
|
22038
23053
|
if (extra.length > 0) params.args = extra;
|
|
23054
|
+
if (opts.hostInitiatedToken) params.hostInitiated = opts.hostInitiatedToken;
|
|
22039
23055
|
return params;
|
|
22040
23056
|
}
|
|
22041
23057
|
function runLocalGit(args, cwd) {
|
|
22042
23058
|
return new Promise((resolve2) => {
|
|
22043
|
-
const child = (0,
|
|
23059
|
+
const child = (0, import_node_child_process11.spawn)("git", args, { cwd, stdio: "inherit" });
|
|
22044
23060
|
child.on("close", (code) => resolve2(code ?? 1));
|
|
22045
23061
|
child.on("error", (err) => {
|
|
22046
23062
|
process.stderr.write(`agentbox-ctl git: ${String(err.message ?? err)}
|
|
@@ -22050,14 +23066,20 @@ function runLocalGit(args, cwd) {
|
|
|
22050
23066
|
});
|
|
22051
23067
|
}
|
|
22052
23068
|
var gitCommand = new Command("git").description("Git operations that need host credentials (routed through the agentbox relay)").addCommand(
|
|
22053
|
-
new Command("push").description("Run `git push` on the host main repo against this box's branch (user is prompted on the host wrapper to confirm)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument(
|
|
23069
|
+
new Command("push").description("Run `git push` on the host main repo against this box's branch (user is prompted on the host wrapper to confirm)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").addOption(hostInitiatedOption()).allowExcessArguments(true).allowUnknownOption(true).argument(
|
|
23070
|
+
"[args...]",
|
|
23071
|
+
"extra flags appended to the host-built `git push <remote> <branch>` (e.g. `--force-with-lease`, `--tags`). Do NOT re-pass the remote or branch \u2014 they are taken from --remote and the registered worktree; appending them as positionals makes git treat them as refspecs and fail with `refs/remotes/origin/HEAD cannot be resolved to branch`. Use --remote to change the remote."
|
|
23072
|
+
).action(async (args, opts) => {
|
|
22054
23073
|
const code = await postRpcAndExit("git.push", buildParams(opts, args), {
|
|
22055
23074
|
errorPrefix: "agentbox-ctl git"
|
|
22056
23075
|
});
|
|
22057
23076
|
process.exit(code);
|
|
22058
23077
|
})
|
|
22059
23078
|
).addCommand(
|
|
22060
|
-
new Command("fetch").description("Run `git fetch` on the host main repo (refs land in the shared .git)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument(
|
|
23079
|
+
new Command("fetch").description("Run `git fetch` on the host main repo (refs land in the shared .git)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").addOption(hostInitiatedOption()).allowExcessArguments(true).allowUnknownOption(true).argument(
|
|
23080
|
+
"[args...]",
|
|
23081
|
+
"extra flags appended to the host-built `git fetch <remote> <branch>` (e.g. `--prune`, `--tags`). Do NOT re-pass the remote or branch; they come from --remote and the registered worktree (same gotcha as `push`)."
|
|
23082
|
+
).action(async (args, opts) => {
|
|
22061
23083
|
const code = await postRpcAndExit("git.fetch", buildParams(opts, args), {
|
|
22062
23084
|
errorPrefix: "agentbox-ctl git"
|
|
22063
23085
|
});
|
|
@@ -22066,7 +23088,10 @@ var gitCommand = new Command("git").description("Git operations that need host c
|
|
|
22066
23088
|
).addCommand(
|
|
22067
23089
|
new Command("pull").description(
|
|
22068
23090
|
"Fetch via the relay (host creds), then merge into the in-container working tree locally"
|
|
22069
|
-
).option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").option("--ff-only", "pass --ff-only to the local merge").allowExcessArguments(true).allowUnknownOption(true).argument(
|
|
23091
|
+
).option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").option("--ff-only", "pass --ff-only to the local merge").addOption(hostInitiatedOption()).allowExcessArguments(true).allowUnknownOption(true).argument(
|
|
23092
|
+
"[args...]",
|
|
23093
|
+
"extra flags appended to the host-built `git fetch <remote> <branch>` (e.g. `--prune`). Do NOT re-pass the remote or branch; they come from --remote and the registered worktree (same gotcha as `push`)."
|
|
23094
|
+
).action(
|
|
22070
23095
|
async (args, opts) => {
|
|
22071
23096
|
const fetchCode = await postRpcAndExit("git.fetch", buildParams(opts, args), {
|
|
22072
23097
|
errorPrefix: "agentbox-ctl git"
|
|
@@ -22081,7 +23106,27 @@ var gitCommand = new Command("git").description("Git operations that need host c
|
|
|
22081
23106
|
process.exit(mergeCode);
|
|
22082
23107
|
}
|
|
22083
23108
|
)
|
|
22084
|
-
)
|
|
23109
|
+
).addCommand(
|
|
23110
|
+
new Command("clone").description(
|
|
23111
|
+
"Clone a github repo into the box. Host runs `git clone` with its creds into a tmpdir, bundles, and ships the bundle back; the box materialises the working copy and resets origin to the original URL."
|
|
23112
|
+
).option("--cwd <path>", "container path identifying which registered worktree to use (default: cwd)").option("--branch <name>", "pass --branch <name> to host git clone").option("--depth <n>", "pass --depth <n> to host git clone").argument("<url>", "github URL or owner/name shorthand").argument("[dir]", "target directory inside the box (default: derived from url)").action(
|
|
23113
|
+
async (url, dir, opts) => {
|
|
23114
|
+
const params = {
|
|
23115
|
+
path: opts.cwd ?? process.cwd(),
|
|
23116
|
+
url
|
|
23117
|
+
};
|
|
23118
|
+
if (dir) params.targetPath = dir;
|
|
23119
|
+
const extra = [];
|
|
23120
|
+
if (opts.branch) extra.push("--branch", opts.branch);
|
|
23121
|
+
if (opts.depth) extra.push("--depth", opts.depth);
|
|
23122
|
+
if (extra.length > 0) params.args = extra;
|
|
23123
|
+
const code = await postRpcAndExit("git.clone", params, {
|
|
23124
|
+
errorPrefix: "agentbox-ctl git clone"
|
|
23125
|
+
});
|
|
23126
|
+
process.exit(code);
|
|
23127
|
+
}
|
|
23128
|
+
)
|
|
23129
|
+
).addCommand(buildPrCommand("agentbox-ctl git pr"));
|
|
22085
23130
|
|
|
22086
23131
|
// src/commands/notify.ts
|
|
22087
23132
|
async function reportState(opts, state) {
|
|
@@ -22103,7 +23148,7 @@ var notifyCommand = new Command("notify").description(
|
|
|
22103
23148
|
);
|
|
22104
23149
|
|
|
22105
23150
|
// src/commands/open.ts
|
|
22106
|
-
var
|
|
23151
|
+
var import_node_child_process12 = require("child_process");
|
|
22107
23152
|
var OPEN_TIMEOUT_MS = 3e4;
|
|
22108
23153
|
function isHttpUrl(value) {
|
|
22109
23154
|
try {
|
|
@@ -22115,7 +23160,7 @@ function isHttpUrl(value) {
|
|
|
22115
23160
|
}
|
|
22116
23161
|
function openInBoxBrowser(url) {
|
|
22117
23162
|
return new Promise((resolve2) => {
|
|
22118
|
-
const child = (0,
|
|
23163
|
+
const child = (0, import_node_child_process12.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
|
|
22119
23164
|
const timer = setTimeout(() => {
|
|
22120
23165
|
child.kill("SIGTERM");
|
|
22121
23166
|
process.stderr.write(
|
|
@@ -22334,9 +23379,11 @@ program2.addCommand(reloadCommand);
|
|
|
22334
23379
|
program2.addCommand(claudeSessionCommand);
|
|
22335
23380
|
program2.addCommand(claudeStateCommand);
|
|
22336
23381
|
program2.addCommand(codexStateCommand);
|
|
23382
|
+
program2.addCommand(opencodeStateCommand);
|
|
22337
23383
|
program2.addCommand(waitReadyCommand);
|
|
22338
23384
|
program2.addCommand(runTaskCommand);
|
|
22339
23385
|
program2.addCommand(gitCommand);
|
|
23386
|
+
program2.addCommand(ghCommand);
|
|
22340
23387
|
program2.addCommand(checkpointCommand);
|
|
22341
23388
|
program2.addCommand(cpCommand);
|
|
22342
23389
|
program2.addCommand(downloadCommand);
|