@madarco/agentbox 0.4.1 → 0.6.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/{chunk-J35IH7W5.js → chunk-BBZMA2K6.js} +61 -23
- package/dist/chunk-BBZMA2K6.js.map +1 -0
- package/dist/{chunk-SOMIKEN2.js → chunk-HHMWQNLF.js} +272 -214
- package/dist/chunk-HHMWQNLF.js.map +1 -0
- package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
- package/dist/chunk-HPZMD5DE.js.map +1 -0
- package/dist/{chunk-NSIECUCS.js → chunk-HTTKML3C.js} +705 -289
- package/dist/chunk-HTTKML3C.js.map +1 -0
- package/dist/{chunk-WR5FFGE5.js → chunk-KJNZP6I3.js} +218 -128
- package/dist/chunk-KJNZP6I3.js.map +1 -0
- package/dist/{chunk-FQD6ZWYW.js → chunk-M7I247BK.js} +68 -65
- package/dist/chunk-M7I247BK.js.map +1 -0
- package/dist/create-6PWXI6HO-OWAMHBAK.js +15 -0
- package/dist/index.js +2394 -1283
- package/dist/index.js.map +1 -1
- package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-EMXR46DI-DUVBXNTV.js} +5 -5
- package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
- package/dist/stats-SZXOJE3D-N7OODCHW.js +19 -0
- package/package.json +3 -2
- package/runtime/docker/Dockerfile.box +65 -25
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +52 -55
- package/runtime/docker/packages/ctl/dist/bin.cjs +272 -160
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +52 -0
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +21 -15
- package/runtime/relay/bin.cjs +407 -12
- package/share/agentbox-setup/SKILL.md +52 -55
- package/dist/chunk-FQD6ZWYW.js.map +0 -1
- package/dist/chunk-IDR4HVIC.js.map +0 -1
- package/dist/chunk-J35IH7W5.js.map +0 -1
- package/dist/chunk-NSIECUCS.js.map +0 -1
- package/dist/chunk-SOMIKEN2.js.map +0 -1
- package/dist/chunk-WR5FFGE5.js.map +0 -1
- package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
- package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
- /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
- /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
- /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
- /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
package/runtime/relay/bin.cjs
CHANGED
|
@@ -10381,6 +10381,171 @@ var RELAY_EVENT_RING_SIZE = 1e3;
|
|
|
10381
10381
|
var import_node_child_process = require("child_process");
|
|
10382
10382
|
var import_node_http = require("http");
|
|
10383
10383
|
|
|
10384
|
+
// src/notices.ts
|
|
10385
|
+
var import_node_crypto = require("crypto");
|
|
10386
|
+
var DEFAULT_NOTICE_TTL_MS = 66e4;
|
|
10387
|
+
var BoxNotices = class {
|
|
10388
|
+
constructor(subscribers) {
|
|
10389
|
+
this.subscribers = subscribers;
|
|
10390
|
+
}
|
|
10391
|
+
subscribers;
|
|
10392
|
+
/** keyed by notice id. */
|
|
10393
|
+
entries = /* @__PURE__ */ new Map();
|
|
10394
|
+
/**
|
|
10395
|
+
* Register a notice for `boxId` and broadcast `notice-set`. At most one
|
|
10396
|
+
* notice per (box, kind) is kept — a fresh `set` for the same kind
|
|
10397
|
+
* replaces the previous one (and cancels its TTL timer so a stale timer
|
|
10398
|
+
* can't later fire a `notice-clear` racing the replacement). Returns the
|
|
10399
|
+
* generated notice id.
|
|
10400
|
+
*/
|
|
10401
|
+
set(boxId, kind, message, ttlMs) {
|
|
10402
|
+
for (const [id, entry] of this.entries) {
|
|
10403
|
+
if (entry.boxId === boxId && entry.ev.kind === kind) {
|
|
10404
|
+
clearTimeout(entry.timer);
|
|
10405
|
+
this.entries.delete(id);
|
|
10406
|
+
}
|
|
10407
|
+
}
|
|
10408
|
+
const ev = { id: (0, import_node_crypto.randomUUID)(), kind, message };
|
|
10409
|
+
const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : DEFAULT_NOTICE_TTL_MS;
|
|
10410
|
+
const timer = setTimeout(() => {
|
|
10411
|
+
if (this.entries.delete(ev.id)) {
|
|
10412
|
+
this.subscribers.broadcast(boxId, "notice-clear", { id: ev.id });
|
|
10413
|
+
}
|
|
10414
|
+
}, ttl);
|
|
10415
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
10416
|
+
this.entries.set(ev.id, { ev, boxId, timer });
|
|
10417
|
+
this.subscribers.broadcast(boxId, "notice-set", ev);
|
|
10418
|
+
return ev.id;
|
|
10419
|
+
}
|
|
10420
|
+
/**
|
|
10421
|
+
* Clear a notice by id. Idempotent: returns false when no such notice
|
|
10422
|
+
* exists (already cleared / expired). Broadcasts `notice-clear` on a hit.
|
|
10423
|
+
*/
|
|
10424
|
+
clear(id) {
|
|
10425
|
+
const entry = this.entries.get(id);
|
|
10426
|
+
if (!entry) return false;
|
|
10427
|
+
clearTimeout(entry.timer);
|
|
10428
|
+
this.entries.delete(id);
|
|
10429
|
+
this.subscribers.broadcast(entry.boxId, "notice-clear", { id });
|
|
10430
|
+
return true;
|
|
10431
|
+
}
|
|
10432
|
+
/** Snapshot of active notices for a box; replayed to a new SSE subscriber. */
|
|
10433
|
+
forBox(boxId) {
|
|
10434
|
+
const out = [];
|
|
10435
|
+
for (const entry of this.entries.values()) {
|
|
10436
|
+
if (entry.boxId === boxId) out.push(entry.ev);
|
|
10437
|
+
}
|
|
10438
|
+
return out;
|
|
10439
|
+
}
|
|
10440
|
+
size() {
|
|
10441
|
+
return this.entries.size;
|
|
10442
|
+
}
|
|
10443
|
+
};
|
|
10444
|
+
|
|
10445
|
+
// src/prompts.ts
|
|
10446
|
+
var import_node_crypto2 = require("crypto");
|
|
10447
|
+
var PendingPrompts = class {
|
|
10448
|
+
entries = /* @__PURE__ */ new Map();
|
|
10449
|
+
add(boxId, ev) {
|
|
10450
|
+
return new Promise((resolve2) => {
|
|
10451
|
+
this.entries.set(ev.id, {
|
|
10452
|
+
ev,
|
|
10453
|
+
boxId,
|
|
10454
|
+
resolve: resolve2,
|
|
10455
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10456
|
+
});
|
|
10457
|
+
});
|
|
10458
|
+
}
|
|
10459
|
+
/**
|
|
10460
|
+
* Idempotent: returns true if a pending entry was found + resolved, false
|
|
10461
|
+
* otherwise. The /admin/prompts/answer handler uses the bool to decide
|
|
10462
|
+
* 204 vs 404 — the wrapper treats both as "we're done."
|
|
10463
|
+
*/
|
|
10464
|
+
resolve(id, answer, cancelled) {
|
|
10465
|
+
const entry = this.entries.get(id);
|
|
10466
|
+
if (!entry) return false;
|
|
10467
|
+
this.entries.delete(id);
|
|
10468
|
+
entry.resolve({ answer, cancelled });
|
|
10469
|
+
return true;
|
|
10470
|
+
}
|
|
10471
|
+
/**
|
|
10472
|
+
* Snapshot of all pending prompts for a given box; used to flush the
|
|
10473
|
+
* backlog to a newly-attached SSE subscriber.
|
|
10474
|
+
*/
|
|
10475
|
+
forBox(boxId) {
|
|
10476
|
+
const out = [];
|
|
10477
|
+
for (const entry of this.entries.values()) {
|
|
10478
|
+
if (entry.boxId === boxId) out.push(entry.ev);
|
|
10479
|
+
}
|
|
10480
|
+
return out;
|
|
10481
|
+
}
|
|
10482
|
+
/** boxId that owns a pending prompt id, or null when unknown. */
|
|
10483
|
+
boxFor(id) {
|
|
10484
|
+
const entry = this.entries.get(id);
|
|
10485
|
+
return entry ? entry.boxId : null;
|
|
10486
|
+
}
|
|
10487
|
+
size() {
|
|
10488
|
+
return this.entries.size;
|
|
10489
|
+
}
|
|
10490
|
+
};
|
|
10491
|
+
var PromptSubscribers = class {
|
|
10492
|
+
byBox = /* @__PURE__ */ new Map();
|
|
10493
|
+
add(boxId, res) {
|
|
10494
|
+
let set = this.byBox.get(boxId);
|
|
10495
|
+
if (!set) {
|
|
10496
|
+
set = /* @__PURE__ */ new Set();
|
|
10497
|
+
this.byBox.set(boxId, set);
|
|
10498
|
+
}
|
|
10499
|
+
set.add(res);
|
|
10500
|
+
}
|
|
10501
|
+
remove(boxId, res) {
|
|
10502
|
+
const set = this.byBox.get(boxId);
|
|
10503
|
+
if (!set) return;
|
|
10504
|
+
set.delete(res);
|
|
10505
|
+
if (set.size === 0) this.byBox.delete(boxId);
|
|
10506
|
+
}
|
|
10507
|
+
forBox(boxId) {
|
|
10508
|
+
const set = this.byBox.get(boxId);
|
|
10509
|
+
return set ? Array.from(set) : [];
|
|
10510
|
+
}
|
|
10511
|
+
/**
|
|
10512
|
+
* Fire-and-forget broadcast. SSE writes that fail (closed socket) are
|
|
10513
|
+
* swallowed — the `res.on('close')` handler in the server route already
|
|
10514
|
+
* deregisters the dead subscriber.
|
|
10515
|
+
*/
|
|
10516
|
+
broadcast(boxId, event, data) {
|
|
10517
|
+
const set = this.byBox.get(boxId);
|
|
10518
|
+
if (!set) return;
|
|
10519
|
+
const payload = `event: ${event}
|
|
10520
|
+
data: ${JSON.stringify(data)}
|
|
10521
|
+
|
|
10522
|
+
`;
|
|
10523
|
+
for (const res of set) {
|
|
10524
|
+
try {
|
|
10525
|
+
res.write(payload);
|
|
10526
|
+
} catch {
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
};
|
|
10531
|
+
async function askPrompt(prompts, subscribers, boxId, params) {
|
|
10532
|
+
if (process.env.AGENTBOX_PROMPT === "off") {
|
|
10533
|
+
return { answer: "y" };
|
|
10534
|
+
}
|
|
10535
|
+
const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
|
|
10536
|
+
const promise = prompts.add(boxId, ev);
|
|
10537
|
+
subscribers.broadcast(boxId, "prompt-ask", ev);
|
|
10538
|
+
return promise;
|
|
10539
|
+
}
|
|
10540
|
+
function isPromptAnswerBody(v) {
|
|
10541
|
+
if (!v || typeof v !== "object") return false;
|
|
10542
|
+
const o = v;
|
|
10543
|
+
if (typeof o.id !== "string" || o.id.length === 0) return false;
|
|
10544
|
+
if (o.answer !== "y" && o.answer !== "n") return false;
|
|
10545
|
+
if (o.cancelled !== void 0 && typeof o.cancelled !== "boolean") return false;
|
|
10546
|
+
return true;
|
|
10547
|
+
}
|
|
10548
|
+
|
|
10384
10549
|
// src/registry.ts
|
|
10385
10550
|
var BoxRegistry = class {
|
|
10386
10551
|
map = /* @__PURE__ */ new Map();
|
|
@@ -10447,8 +10612,16 @@ var EventBuffer = class {
|
|
|
10447
10612
|
var import_promises = require("fs/promises");
|
|
10448
10613
|
var import_node_os = require("os");
|
|
10449
10614
|
var import_node_path = require("path");
|
|
10450
|
-
function
|
|
10451
|
-
return (
|
|
10615
|
+
function sanitizeMnemonic(raw) {
|
|
10616
|
+
return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
|
|
10617
|
+
}
|
|
10618
|
+
function boxRunDirFor(boxId, name, projectIndex) {
|
|
10619
|
+
const mnemonic = sanitizeMnemonic(name);
|
|
10620
|
+
const segment = typeof projectIndex === "number" && Number.isFinite(projectIndex) && projectIndex > 0 ? `${boxId}-${String(projectIndex)}-${mnemonic}` : `${boxId}-${mnemonic}`;
|
|
10621
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentbox", "boxes", segment);
|
|
10622
|
+
}
|
|
10623
|
+
function boxStatusPathFor(boxId, name, projectIndex) {
|
|
10624
|
+
return (0, import_node_path.join)(boxRunDirFor(boxId, name, projectIndex), "status.json");
|
|
10452
10625
|
}
|
|
10453
10626
|
function isValidBoxStatus(payload) {
|
|
10454
10627
|
if (typeof payload !== "object" || payload === null) return false;
|
|
@@ -10460,13 +10633,19 @@ var BoxStatusStore = class {
|
|
|
10460
10633
|
get(boxId) {
|
|
10461
10634
|
return this.map.get(boxId);
|
|
10462
10635
|
}
|
|
10463
|
-
/**
|
|
10464
|
-
|
|
10636
|
+
/**
|
|
10637
|
+
* Update the in-memory entry and best-effort persist it to disk. `name` is
|
|
10638
|
+
* the box's user-facing name (from the registry); `projectIndex` is the
|
|
10639
|
+
* 1-based per-project `N`. Together they form the on-disk dir
|
|
10640
|
+
* `~/.agentbox/boxes/<id>-<n>-<mnemonic>/status.json` (or
|
|
10641
|
+
* `<id>-<mnemonic>/` if N is absent — legacy boxes).
|
|
10642
|
+
*/
|
|
10643
|
+
async set(boxId, name, projectIndex, status) {
|
|
10465
10644
|
this.map.set(boxId, status);
|
|
10466
|
-
const target = boxStatusPathFor(boxId);
|
|
10645
|
+
const target = boxStatusPathFor(boxId, name, projectIndex);
|
|
10467
10646
|
const tmp = `${target}.${String(process.pid)}.tmp`;
|
|
10468
10647
|
try {
|
|
10469
|
-
await (0, import_promises.mkdir)((
|
|
10648
|
+
await (0, import_promises.mkdir)(boxRunDirFor(boxId, name, projectIndex), { recursive: true });
|
|
10470
10649
|
await (0, import_promises.writeFile)(tmp, JSON.stringify(status), "utf8");
|
|
10471
10650
|
await (0, import_promises.rename)(tmp, target);
|
|
10472
10651
|
} catch {
|
|
@@ -10485,6 +10664,10 @@ var BOX_STATUS_EVENT = "box-status";
|
|
|
10485
10664
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
10486
10665
|
var GIT_RPC_TIMEOUT_MS = 12e4;
|
|
10487
10666
|
var CHECKPOINT_RPC_TIMEOUT_MS = 6e5;
|
|
10667
|
+
var DOWNLOAD_RPC_TIMEOUT_MS = 6e5;
|
|
10668
|
+
var CP_RPC_TIMEOUT_MS = 3e5;
|
|
10669
|
+
var BROWSER_OPEN_RPC_TIMEOUT_MS = 15e3;
|
|
10670
|
+
var SSE_HEARTBEAT_MS = 15e3;
|
|
10488
10671
|
function send(res, status, body, contentType = "application/json") {
|
|
10489
10672
|
const text = body == null ? "" : typeof body === "string" ? body : JSON.stringify(body);
|
|
10490
10673
|
res.statusCode = status;
|
|
@@ -10540,6 +10723,9 @@ function createRelayServer(opts) {
|
|
|
10540
10723
|
const registry = new BoxRegistry();
|
|
10541
10724
|
const events = new EventBuffer();
|
|
10542
10725
|
const statusStore = new BoxStatusStore();
|
|
10726
|
+
const prompts = new PendingPrompts();
|
|
10727
|
+
const subscribers = new PromptSubscribers();
|
|
10728
|
+
const notices = new BoxNotices(subscribers);
|
|
10543
10729
|
const host = opts.host ?? "0.0.0.0";
|
|
10544
10730
|
const server = (0, import_node_http.createServer)((req, res) => {
|
|
10545
10731
|
handle(req, res).catch((err) => {
|
|
@@ -10575,7 +10761,7 @@ function createRelayServer(opts) {
|
|
|
10575
10761
|
send(res, 400, { error: "invalid box-status payload" });
|
|
10576
10762
|
return;
|
|
10577
10763
|
}
|
|
10578
|
-
await statusStore.set(reg.boxId, body.payload);
|
|
10764
|
+
await statusStore.set(reg.boxId, reg.name, reg.projectIndex, body.payload);
|
|
10579
10765
|
log(`box-status box=${reg.boxId}`);
|
|
10580
10766
|
send(res, 202, { ok: true });
|
|
10581
10767
|
return;
|
|
@@ -10599,12 +10785,78 @@ function createRelayServer(opts) {
|
|
|
10599
10785
|
return;
|
|
10600
10786
|
}
|
|
10601
10787
|
log(`rpc box=${reg.boxId} method=${body.method}`);
|
|
10602
|
-
if (body.method === "git.
|
|
10788
|
+
if (body.method === "git.push" || body.method === "git.fetch") {
|
|
10789
|
+
if (body.method === "git.push") {
|
|
10790
|
+
const params = body.params;
|
|
10791
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
10792
|
+
kind: "confirm",
|
|
10793
|
+
message: `Allow git push from box ${reg.name}?`,
|
|
10794
|
+
detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
|
|
10795
|
+
defaultAnswer: "n",
|
|
10796
|
+
context: {
|
|
10797
|
+
command: "git push",
|
|
10798
|
+
cwd: params?.path,
|
|
10799
|
+
argv: params?.args
|
|
10800
|
+
}
|
|
10801
|
+
});
|
|
10802
|
+
if (verdict.answer !== "y") {
|
|
10803
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
10804
|
+
return;
|
|
10805
|
+
}
|
|
10806
|
+
}
|
|
10603
10807
|
const result = await handleGitRpc(reg, body.method, body.params);
|
|
10604
10808
|
const status = result.exitCode === 0 ? 200 : 500;
|
|
10605
10809
|
send(res, status, result);
|
|
10606
10810
|
return;
|
|
10607
10811
|
}
|
|
10812
|
+
if (body.method === "cp.toHost" || body.method === "cp.fromHost") {
|
|
10813
|
+
const params = body.params;
|
|
10814
|
+
if (!params || typeof params.boxPath !== "string" || typeof params.hostPath !== "string") {
|
|
10815
|
+
send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
|
|
10816
|
+
return;
|
|
10817
|
+
}
|
|
10818
|
+
const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
|
|
10819
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
10820
|
+
kind: "confirm",
|
|
10821
|
+
message: `Allow cp (${direction}) on ${reg.name}?`,
|
|
10822
|
+
detail: body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`,
|
|
10823
|
+
defaultAnswer: "n",
|
|
10824
|
+
context: {
|
|
10825
|
+
command: body.method,
|
|
10826
|
+
argv: [params.boxPath, params.hostPath]
|
|
10827
|
+
}
|
|
10828
|
+
});
|
|
10829
|
+
if (verdict.answer !== "y") {
|
|
10830
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
10831
|
+
return;
|
|
10832
|
+
}
|
|
10833
|
+
const result = await handleCpRpc(reg, body.method, params);
|
|
10834
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
10835
|
+
send(res, status, result);
|
|
10836
|
+
return;
|
|
10837
|
+
}
|
|
10838
|
+
if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
|
|
10839
|
+
const params = body.params;
|
|
10840
|
+
const kind = body.method.split(".")[1] ?? "workspace";
|
|
10841
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
10842
|
+
kind: "confirm",
|
|
10843
|
+
message: `Allow download (${kind}) from ${reg.name}?`,
|
|
10844
|
+
detail: params?.hostPath ?? "(default host location)",
|
|
10845
|
+
defaultAnswer: "n",
|
|
10846
|
+
context: {
|
|
10847
|
+
command: body.method,
|
|
10848
|
+
argv: params?.hostPath ? [params.hostPath] : []
|
|
10849
|
+
}
|
|
10850
|
+
});
|
|
10851
|
+
if (verdict.answer !== "y") {
|
|
10852
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
10853
|
+
return;
|
|
10854
|
+
}
|
|
10855
|
+
const result = await handleDownloadRpc(reg, kind);
|
|
10856
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
10857
|
+
send(res, status, result);
|
|
10858
|
+
return;
|
|
10859
|
+
}
|
|
10608
10860
|
if (body.method === "checkpoint.create") {
|
|
10609
10861
|
const result = await handleCheckpointRpc(
|
|
10610
10862
|
reg,
|
|
@@ -10614,6 +10866,23 @@ function createRelayServer(opts) {
|
|
|
10614
10866
|
send(res, status, result);
|
|
10615
10867
|
return;
|
|
10616
10868
|
}
|
|
10869
|
+
if (body.method === "browser.open") {
|
|
10870
|
+
const params = body.params;
|
|
10871
|
+
const url2 = typeof params?.url === "string" ? params.url.trim() : "";
|
|
10872
|
+
if (!isOpenableUrl(url2)) {
|
|
10873
|
+
send(res, 400, {
|
|
10874
|
+
exitCode: 64,
|
|
10875
|
+
stdout: "",
|
|
10876
|
+
stderr: "browser.open: only http/https URLs are allowed\n"
|
|
10877
|
+
});
|
|
10878
|
+
return;
|
|
10879
|
+
}
|
|
10880
|
+
events.append({ boxId: reg.boxId, type: "browser-open", payload: { url: url2 } });
|
|
10881
|
+
const result = await runHostCommand(["open", url2], BROWSER_OPEN_RPC_TIMEOUT_MS);
|
|
10882
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
10883
|
+
send(res, status, result);
|
|
10884
|
+
return;
|
|
10885
|
+
}
|
|
10617
10886
|
events.append({
|
|
10618
10887
|
boxId: reg.boxId,
|
|
10619
10888
|
type: "rpc-unknown",
|
|
@@ -10629,6 +10898,7 @@ function createRelayServer(opts) {
|
|
|
10629
10898
|
return;
|
|
10630
10899
|
}
|
|
10631
10900
|
const worktrees = sanitizeWorktrees(body.worktrees);
|
|
10901
|
+
const projectIndex = typeof body.projectIndex === "number" && Number.isFinite(body.projectIndex) && body.projectIndex > 0 ? Math.trunc(body.projectIndex) : void 0;
|
|
10632
10902
|
const reg = {
|
|
10633
10903
|
boxId: body.boxId,
|
|
10634
10904
|
token: body.token,
|
|
@@ -10636,6 +10906,7 @@ function createRelayServer(opts) {
|
|
|
10636
10906
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10637
10907
|
containerName: typeof body.containerName === "string" && body.containerName.length > 0 ? body.containerName : void 0,
|
|
10638
10908
|
createdAt: typeof body.createdAt === "string" && body.createdAt.length > 0 ? body.createdAt : void 0,
|
|
10909
|
+
projectIndex,
|
|
10639
10910
|
worktrees
|
|
10640
10911
|
};
|
|
10641
10912
|
registry.register(reg);
|
|
@@ -10681,11 +10952,95 @@ function createRelayServer(opts) {
|
|
|
10681
10952
|
registeredAt: r.registeredAt,
|
|
10682
10953
|
containerName: r.containerName,
|
|
10683
10954
|
createdAt: r.createdAt,
|
|
10955
|
+
projectIndex: r.projectIndex,
|
|
10684
10956
|
worktrees: r.worktrees ?? []
|
|
10685
10957
|
}));
|
|
10686
10958
|
send(res, 200, { boxes: redacted });
|
|
10687
10959
|
return;
|
|
10688
10960
|
}
|
|
10961
|
+
if (route === "GET /admin/prompts/stream") {
|
|
10962
|
+
const boxId = url.searchParams.get("boxId") ?? "";
|
|
10963
|
+
if (boxId.length === 0) {
|
|
10964
|
+
send(res, 400, { error: "missing boxId query param" });
|
|
10965
|
+
return;
|
|
10966
|
+
}
|
|
10967
|
+
res.statusCode = 200;
|
|
10968
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
10969
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
10970
|
+
res.setHeader("Connection", "keep-alive");
|
|
10971
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
10972
|
+
if (typeof res.flushHeaders === "function") res.flushHeaders();
|
|
10973
|
+
res.write(": connected\n\n");
|
|
10974
|
+
subscribers.add(boxId, res);
|
|
10975
|
+
for (const ev of prompts.forBox(boxId)) {
|
|
10976
|
+
res.write(`event: prompt-ask
|
|
10977
|
+
data: ${JSON.stringify(ev)}
|
|
10978
|
+
|
|
10979
|
+
`);
|
|
10980
|
+
}
|
|
10981
|
+
for (const ev of notices.forBox(boxId)) {
|
|
10982
|
+
res.write(`event: notice-set
|
|
10983
|
+
data: ${JSON.stringify(ev)}
|
|
10984
|
+
|
|
10985
|
+
`);
|
|
10986
|
+
}
|
|
10987
|
+
const heartbeat = setInterval(() => {
|
|
10988
|
+
try {
|
|
10989
|
+
res.write(`event: ping
|
|
10990
|
+
data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
10991
|
+
|
|
10992
|
+
`);
|
|
10993
|
+
} catch {
|
|
10994
|
+
}
|
|
10995
|
+
}, SSE_HEARTBEAT_MS);
|
|
10996
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
10997
|
+
res.on("close", () => {
|
|
10998
|
+
clearInterval(heartbeat);
|
|
10999
|
+
subscribers.remove(boxId, res);
|
|
11000
|
+
});
|
|
11001
|
+
return;
|
|
11002
|
+
}
|
|
11003
|
+
if (route === "POST /admin/prompts/answer") {
|
|
11004
|
+
const body = await readJsonBody(req);
|
|
11005
|
+
if (!isPromptAnswerBody(body)) {
|
|
11006
|
+
send(res, 400, { error: 'expected {id, answer:"y"|"n", cancelled?}' });
|
|
11007
|
+
return;
|
|
11008
|
+
}
|
|
11009
|
+
const targetBox = prompts.boxFor(body.id);
|
|
11010
|
+
const hit = prompts.resolve(body.id, body.answer, body.cancelled);
|
|
11011
|
+
if (!hit) {
|
|
11012
|
+
send(res, 404, { error: "no pending prompt with that id" });
|
|
11013
|
+
return;
|
|
11014
|
+
}
|
|
11015
|
+
if (targetBox) {
|
|
11016
|
+
subscribers.broadcast(targetBox, "prompt-resolved", { id: body.id });
|
|
11017
|
+
}
|
|
11018
|
+
send(res, 204, null);
|
|
11019
|
+
return;
|
|
11020
|
+
}
|
|
11021
|
+
if (route === "POST /admin/notices/set") {
|
|
11022
|
+
const body = await readJsonBody(req);
|
|
11023
|
+
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) {
|
|
11024
|
+
send(res, 400, { error: "expected {boxId, kind, message}" });
|
|
11025
|
+
return;
|
|
11026
|
+
}
|
|
11027
|
+
const ttlMs = typeof body.ttlMs === "number" && Number.isFinite(body.ttlMs) && body.ttlMs > 0 ? body.ttlMs : void 0;
|
|
11028
|
+
const id = notices.set(body.boxId, body.kind, body.message, ttlMs);
|
|
11029
|
+
log(`notice-set box=${body.boxId} kind=${body.kind} id=${id}`);
|
|
11030
|
+
send(res, 200, { id });
|
|
11031
|
+
return;
|
|
11032
|
+
}
|
|
11033
|
+
if (route === "POST /admin/notices/clear") {
|
|
11034
|
+
const body = await readJsonBody(req);
|
|
11035
|
+
if (!body || typeof body.id !== "string" || body.id.length === 0) {
|
|
11036
|
+
send(res, 400, { error: "expected {boxId, id}" });
|
|
11037
|
+
return;
|
|
11038
|
+
}
|
|
11039
|
+
notices.clear(body.id);
|
|
11040
|
+
log(`notice-clear id=${body.id}`);
|
|
11041
|
+
send(res, 204, null);
|
|
11042
|
+
return;
|
|
11043
|
+
}
|
|
10689
11044
|
send(res, 404, { error: "not found", route });
|
|
10690
11045
|
}
|
|
10691
11046
|
function authBox(req, res, reg) {
|
|
@@ -10706,6 +11061,9 @@ function createRelayServer(opts) {
|
|
|
10706
11061
|
registry,
|
|
10707
11062
|
events,
|
|
10708
11063
|
statusStore,
|
|
11064
|
+
prompts,
|
|
11065
|
+
subscribers,
|
|
11066
|
+
notices,
|
|
10709
11067
|
url: `http://${host}:${String(opts.port)}`,
|
|
10710
11068
|
close: () => new Promise((resolve2, reject) => {
|
|
10711
11069
|
server.close((err) => {
|
|
@@ -10719,10 +11077,10 @@ function sanitizeWorktrees(input) {
|
|
|
10719
11077
|
if (!Array.isArray(input)) return void 0;
|
|
10720
11078
|
const out = [];
|
|
10721
11079
|
for (const w of input) {
|
|
10722
|
-
if (w && typeof w.containerPath === "string" && typeof w.
|
|
11080
|
+
if (w && typeof w.containerPath === "string" && typeof w.hostMainRepo === "string" && typeof w.branch === "string") {
|
|
10723
11081
|
out.push({
|
|
10724
11082
|
containerPath: w.containerPath,
|
|
10725
|
-
|
|
11083
|
+
hostMainRepo: w.hostMainRepo,
|
|
10726
11084
|
branch: w.branch
|
|
10727
11085
|
});
|
|
10728
11086
|
}
|
|
@@ -10747,9 +11105,9 @@ async function handleGitRpc(reg, method, params) {
|
|
|
10747
11105
|
stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
|
|
10748
11106
|
};
|
|
10749
11107
|
}
|
|
10750
|
-
const op = method === "git.
|
|
11108
|
+
const op = method === "git.push" ? "push" : "fetch";
|
|
10751
11109
|
const remote = params?.remote ?? "origin";
|
|
10752
|
-
const argv = ["git", "-C", worktree.
|
|
11110
|
+
const argv = ["git", "-C", worktree.hostMainRepo, op, remote, worktree.branch];
|
|
10753
11111
|
if (Array.isArray(params?.args)) {
|
|
10754
11112
|
for (const a of params.args) {
|
|
10755
11113
|
if (typeof a === "string") argv.push(a);
|
|
@@ -10757,6 +11115,33 @@ async function handleGitRpc(reg, method, params) {
|
|
|
10757
11115
|
}
|
|
10758
11116
|
return runHostCommand(argv);
|
|
10759
11117
|
}
|
|
11118
|
+
async function handleCpRpc(reg, method, params) {
|
|
11119
|
+
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
11120
|
+
if (!entry) {
|
|
11121
|
+
return {
|
|
11122
|
+
exitCode: 64,
|
|
11123
|
+
stdout: "",
|
|
11124
|
+
stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run cp host-side"
|
|
11125
|
+
};
|
|
11126
|
+
}
|
|
11127
|
+
const boxRef = `${reg.name}:${params.boxPath}`;
|
|
11128
|
+
const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath] : [process.execPath, entry, "cp", params.hostPath, boxRef];
|
|
11129
|
+
return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
|
|
11130
|
+
}
|
|
11131
|
+
async function handleDownloadRpc(reg, kind) {
|
|
11132
|
+
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
11133
|
+
if (!entry) {
|
|
11134
|
+
return {
|
|
11135
|
+
exitCode: 64,
|
|
11136
|
+
stdout: "",
|
|
11137
|
+
stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run download host-side"
|
|
11138
|
+
};
|
|
11139
|
+
}
|
|
11140
|
+
const argv = [process.execPath, entry, "download"];
|
|
11141
|
+
if (kind !== "workspace") argv.push(kind);
|
|
11142
|
+
argv.push(reg.name, "-y");
|
|
11143
|
+
return runHostCommand(argv, DOWNLOAD_RPC_TIMEOUT_MS);
|
|
11144
|
+
}
|
|
10760
11145
|
async function handleCheckpointRpc(reg, params) {
|
|
10761
11146
|
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
10762
11147
|
if (!entry) {
|
|
@@ -10770,8 +11155,18 @@ async function handleCheckpointRpc(reg, params) {
|
|
|
10770
11155
|
if (params?.name) argv.push("--name", params.name);
|
|
10771
11156
|
if (params?.merged === true) argv.push("--merged");
|
|
10772
11157
|
if (params?.setDefault === true) argv.push("--set-default");
|
|
11158
|
+
if (params?.replace === true) argv.push("--replace");
|
|
10773
11159
|
return runHostCommand(argv, CHECKPOINT_RPC_TIMEOUT_MS);
|
|
10774
11160
|
}
|
|
11161
|
+
function isOpenableUrl(value) {
|
|
11162
|
+
let url;
|
|
11163
|
+
try {
|
|
11164
|
+
url = new URL(value);
|
|
11165
|
+
} catch {
|
|
11166
|
+
return false;
|
|
11167
|
+
}
|
|
11168
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
11169
|
+
}
|
|
10775
11170
|
function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
|
|
10776
11171
|
return new Promise((resolve2) => {
|
|
10777
11172
|
const [cmd, ...rest] = argv;
|