@madarco/agentbox 0.4.1 → 0.5.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-WR5FFGE5.js → chunk-6VTAPD4H.js} +123 -112
- package/dist/chunk-6VTAPD4H.js.map +1 -0
- package/dist/{chunk-J35IH7W5.js → chunk-7J5AJLWG.js} +61 -23
- package/dist/chunk-7J5AJLWG.js.map +1 -0
- package/dist/{chunk-FQD6ZWYW.js → chunk-FJNIFTWK.js} +66 -65
- package/dist/chunk-FJNIFTWK.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-PXUBE5KS.js} +365 -258
- package/dist/chunk-PXUBE5KS.js.map +1 -0
- package/dist/{chunk-SOMIKEN2.js → chunk-RFC5F5HR.js} +272 -214
- package/dist/chunk-RFC5F5HR.js.map +1 -0
- package/dist/create-AHZ3GVEZ-TGEDL7UX.js +15 -0
- package/dist/index.js +2757 -1854
- package/dist/index.js.map +1 -1
- package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-LFOL6YFM-TCHDX3J5.js} +5 -5
- package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js +19 -0
- package/package.json +5 -4
- package/runtime/docker/Dockerfile.box +47 -19
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +39 -50
- package/runtime/docker/packages/ctl/dist/bin.cjs +219 -148
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +42 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +26 -15
- package/runtime/relay/bin.cjs +288 -12
- package/share/agentbox-setup/SKILL.md +39 -50
- 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-AHZ3GVEZ-TGEDL7UX.js.map} +0 -0
- /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-LFOL6YFM-TCHDX3J5.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-Z4BVJODD-HEC4TMUZ.js.map} +0 -0
package/runtime/relay/bin.cjs
CHANGED
|
@@ -10381,6 +10381,110 @@ 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/prompts.ts
|
|
10385
|
+
var import_node_crypto = require("crypto");
|
|
10386
|
+
var PendingPrompts = class {
|
|
10387
|
+
entries = /* @__PURE__ */ new Map();
|
|
10388
|
+
add(boxId, ev) {
|
|
10389
|
+
return new Promise((resolve2) => {
|
|
10390
|
+
this.entries.set(ev.id, {
|
|
10391
|
+
ev,
|
|
10392
|
+
boxId,
|
|
10393
|
+
resolve: resolve2,
|
|
10394
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10395
|
+
});
|
|
10396
|
+
});
|
|
10397
|
+
}
|
|
10398
|
+
/**
|
|
10399
|
+
* Idempotent: returns true if a pending entry was found + resolved, false
|
|
10400
|
+
* otherwise. The /admin/prompts/answer handler uses the bool to decide
|
|
10401
|
+
* 204 vs 404 — the wrapper treats both as "we're done."
|
|
10402
|
+
*/
|
|
10403
|
+
resolve(id, answer, cancelled) {
|
|
10404
|
+
const entry = this.entries.get(id);
|
|
10405
|
+
if (!entry) return false;
|
|
10406
|
+
this.entries.delete(id);
|
|
10407
|
+
entry.resolve({ answer, cancelled });
|
|
10408
|
+
return true;
|
|
10409
|
+
}
|
|
10410
|
+
/**
|
|
10411
|
+
* Snapshot of all pending prompts for a given box; used to flush the
|
|
10412
|
+
* backlog to a newly-attached SSE subscriber.
|
|
10413
|
+
*/
|
|
10414
|
+
forBox(boxId) {
|
|
10415
|
+
const out = [];
|
|
10416
|
+
for (const entry of this.entries.values()) {
|
|
10417
|
+
if (entry.boxId === boxId) out.push(entry.ev);
|
|
10418
|
+
}
|
|
10419
|
+
return out;
|
|
10420
|
+
}
|
|
10421
|
+
/** boxId that owns a pending prompt id, or null when unknown. */
|
|
10422
|
+
boxFor(id) {
|
|
10423
|
+
const entry = this.entries.get(id);
|
|
10424
|
+
return entry ? entry.boxId : null;
|
|
10425
|
+
}
|
|
10426
|
+
size() {
|
|
10427
|
+
return this.entries.size;
|
|
10428
|
+
}
|
|
10429
|
+
};
|
|
10430
|
+
var PromptSubscribers = class {
|
|
10431
|
+
byBox = /* @__PURE__ */ new Map();
|
|
10432
|
+
add(boxId, res) {
|
|
10433
|
+
let set = this.byBox.get(boxId);
|
|
10434
|
+
if (!set) {
|
|
10435
|
+
set = /* @__PURE__ */ new Set();
|
|
10436
|
+
this.byBox.set(boxId, set);
|
|
10437
|
+
}
|
|
10438
|
+
set.add(res);
|
|
10439
|
+
}
|
|
10440
|
+
remove(boxId, res) {
|
|
10441
|
+
const set = this.byBox.get(boxId);
|
|
10442
|
+
if (!set) return;
|
|
10443
|
+
set.delete(res);
|
|
10444
|
+
if (set.size === 0) this.byBox.delete(boxId);
|
|
10445
|
+
}
|
|
10446
|
+
forBox(boxId) {
|
|
10447
|
+
const set = this.byBox.get(boxId);
|
|
10448
|
+
return set ? Array.from(set) : [];
|
|
10449
|
+
}
|
|
10450
|
+
/**
|
|
10451
|
+
* Fire-and-forget broadcast. SSE writes that fail (closed socket) are
|
|
10452
|
+
* swallowed — the `res.on('close')` handler in the server route already
|
|
10453
|
+
* deregisters the dead subscriber.
|
|
10454
|
+
*/
|
|
10455
|
+
broadcast(boxId, event, data) {
|
|
10456
|
+
const set = this.byBox.get(boxId);
|
|
10457
|
+
if (!set) return;
|
|
10458
|
+
const payload = `event: ${event}
|
|
10459
|
+
data: ${JSON.stringify(data)}
|
|
10460
|
+
|
|
10461
|
+
`;
|
|
10462
|
+
for (const res of set) {
|
|
10463
|
+
try {
|
|
10464
|
+
res.write(payload);
|
|
10465
|
+
} catch {
|
|
10466
|
+
}
|
|
10467
|
+
}
|
|
10468
|
+
}
|
|
10469
|
+
};
|
|
10470
|
+
async function askPrompt(prompts, subscribers, boxId, params) {
|
|
10471
|
+
if (process.env.AGENTBOX_PROMPT === "off") {
|
|
10472
|
+
return { answer: "y" };
|
|
10473
|
+
}
|
|
10474
|
+
const ev = { id: (0, import_node_crypto.randomUUID)(), ...params };
|
|
10475
|
+
const promise = prompts.add(boxId, ev);
|
|
10476
|
+
subscribers.broadcast(boxId, "prompt-ask", ev);
|
|
10477
|
+
return promise;
|
|
10478
|
+
}
|
|
10479
|
+
function isPromptAnswerBody(v) {
|
|
10480
|
+
if (!v || typeof v !== "object") return false;
|
|
10481
|
+
const o = v;
|
|
10482
|
+
if (typeof o.id !== "string" || o.id.length === 0) return false;
|
|
10483
|
+
if (o.answer !== "y" && o.answer !== "n") return false;
|
|
10484
|
+
if (o.cancelled !== void 0 && typeof o.cancelled !== "boolean") return false;
|
|
10485
|
+
return true;
|
|
10486
|
+
}
|
|
10487
|
+
|
|
10384
10488
|
// src/registry.ts
|
|
10385
10489
|
var BoxRegistry = class {
|
|
10386
10490
|
map = /* @__PURE__ */ new Map();
|
|
@@ -10447,8 +10551,16 @@ var EventBuffer = class {
|
|
|
10447
10551
|
var import_promises = require("fs/promises");
|
|
10448
10552
|
var import_node_os = require("os");
|
|
10449
10553
|
var import_node_path = require("path");
|
|
10450
|
-
function
|
|
10451
|
-
return (
|
|
10554
|
+
function sanitizeMnemonic(raw) {
|
|
10555
|
+
return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
|
|
10556
|
+
}
|
|
10557
|
+
function boxRunDirFor(boxId, name, projectIndex) {
|
|
10558
|
+
const mnemonic = sanitizeMnemonic(name);
|
|
10559
|
+
const segment = typeof projectIndex === "number" && Number.isFinite(projectIndex) && projectIndex > 0 ? `${boxId}-${String(projectIndex)}-${mnemonic}` : `${boxId}-${mnemonic}`;
|
|
10560
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentbox", "boxes", segment);
|
|
10561
|
+
}
|
|
10562
|
+
function boxStatusPathFor(boxId, name, projectIndex) {
|
|
10563
|
+
return (0, import_node_path.join)(boxRunDirFor(boxId, name, projectIndex), "status.json");
|
|
10452
10564
|
}
|
|
10453
10565
|
function isValidBoxStatus(payload) {
|
|
10454
10566
|
if (typeof payload !== "object" || payload === null) return false;
|
|
@@ -10460,13 +10572,19 @@ var BoxStatusStore = class {
|
|
|
10460
10572
|
get(boxId) {
|
|
10461
10573
|
return this.map.get(boxId);
|
|
10462
10574
|
}
|
|
10463
|
-
/**
|
|
10464
|
-
|
|
10575
|
+
/**
|
|
10576
|
+
* Update the in-memory entry and best-effort persist it to disk. `name` is
|
|
10577
|
+
* the box's user-facing name (from the registry); `projectIndex` is the
|
|
10578
|
+
* 1-based per-project `N`. Together they form the on-disk dir
|
|
10579
|
+
* `~/.agentbox/boxes/<id>-<n>-<mnemonic>/status.json` (or
|
|
10580
|
+
* `<id>-<mnemonic>/` if N is absent — legacy boxes).
|
|
10581
|
+
*/
|
|
10582
|
+
async set(boxId, name, projectIndex, status) {
|
|
10465
10583
|
this.map.set(boxId, status);
|
|
10466
|
-
const target = boxStatusPathFor(boxId);
|
|
10584
|
+
const target = boxStatusPathFor(boxId, name, projectIndex);
|
|
10467
10585
|
const tmp = `${target}.${String(process.pid)}.tmp`;
|
|
10468
10586
|
try {
|
|
10469
|
-
await (0, import_promises.mkdir)((
|
|
10587
|
+
await (0, import_promises.mkdir)(boxRunDirFor(boxId, name, projectIndex), { recursive: true });
|
|
10470
10588
|
await (0, import_promises.writeFile)(tmp, JSON.stringify(status), "utf8");
|
|
10471
10589
|
await (0, import_promises.rename)(tmp, target);
|
|
10472
10590
|
} catch {
|
|
@@ -10485,6 +10603,9 @@ var BOX_STATUS_EVENT = "box-status";
|
|
|
10485
10603
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
10486
10604
|
var GIT_RPC_TIMEOUT_MS = 12e4;
|
|
10487
10605
|
var CHECKPOINT_RPC_TIMEOUT_MS = 6e5;
|
|
10606
|
+
var DOWNLOAD_RPC_TIMEOUT_MS = 6e5;
|
|
10607
|
+
var CP_RPC_TIMEOUT_MS = 3e5;
|
|
10608
|
+
var SSE_HEARTBEAT_MS = 15e3;
|
|
10488
10609
|
function send(res, status, body, contentType = "application/json") {
|
|
10489
10610
|
const text = body == null ? "" : typeof body === "string" ? body : JSON.stringify(body);
|
|
10490
10611
|
res.statusCode = status;
|
|
@@ -10540,6 +10661,8 @@ function createRelayServer(opts) {
|
|
|
10540
10661
|
const registry = new BoxRegistry();
|
|
10541
10662
|
const events = new EventBuffer();
|
|
10542
10663
|
const statusStore = new BoxStatusStore();
|
|
10664
|
+
const prompts = new PendingPrompts();
|
|
10665
|
+
const subscribers = new PromptSubscribers();
|
|
10543
10666
|
const host = opts.host ?? "0.0.0.0";
|
|
10544
10667
|
const server = (0, import_node_http.createServer)((req, res) => {
|
|
10545
10668
|
handle(req, res).catch((err) => {
|
|
@@ -10575,7 +10698,7 @@ function createRelayServer(opts) {
|
|
|
10575
10698
|
send(res, 400, { error: "invalid box-status payload" });
|
|
10576
10699
|
return;
|
|
10577
10700
|
}
|
|
10578
|
-
await statusStore.set(reg.boxId, body.payload);
|
|
10701
|
+
await statusStore.set(reg.boxId, reg.name, reg.projectIndex, body.payload);
|
|
10579
10702
|
log(`box-status box=${reg.boxId}`);
|
|
10580
10703
|
send(res, 202, { ok: true });
|
|
10581
10704
|
return;
|
|
@@ -10599,12 +10722,78 @@ function createRelayServer(opts) {
|
|
|
10599
10722
|
return;
|
|
10600
10723
|
}
|
|
10601
10724
|
log(`rpc box=${reg.boxId} method=${body.method}`);
|
|
10602
|
-
if (body.method === "git.
|
|
10725
|
+
if (body.method === "git.push" || body.method === "git.fetch") {
|
|
10726
|
+
if (body.method === "git.push") {
|
|
10727
|
+
const params = body.params;
|
|
10728
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
10729
|
+
kind: "confirm",
|
|
10730
|
+
message: `Allow git push from box ${reg.name}?`,
|
|
10731
|
+
detail: `${params?.remote ?? "origin"} ${(params?.args ?? []).join(" ")}`.trim(),
|
|
10732
|
+
defaultAnswer: "n",
|
|
10733
|
+
context: {
|
|
10734
|
+
command: "git push",
|
|
10735
|
+
cwd: params?.path,
|
|
10736
|
+
argv: params?.args
|
|
10737
|
+
}
|
|
10738
|
+
});
|
|
10739
|
+
if (verdict.answer !== "y") {
|
|
10740
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
10741
|
+
return;
|
|
10742
|
+
}
|
|
10743
|
+
}
|
|
10603
10744
|
const result = await handleGitRpc(reg, body.method, body.params);
|
|
10604
10745
|
const status = result.exitCode === 0 ? 200 : 500;
|
|
10605
10746
|
send(res, status, result);
|
|
10606
10747
|
return;
|
|
10607
10748
|
}
|
|
10749
|
+
if (body.method === "cp.toHost" || body.method === "cp.fromHost") {
|
|
10750
|
+
const params = body.params;
|
|
10751
|
+
if (!params || typeof params.boxPath !== "string" || typeof params.hostPath !== "string") {
|
|
10752
|
+
send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
|
|
10753
|
+
return;
|
|
10754
|
+
}
|
|
10755
|
+
const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
|
|
10756
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
10757
|
+
kind: "confirm",
|
|
10758
|
+
message: `Allow cp (${direction}) on ${reg.name}?`,
|
|
10759
|
+
detail: body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`,
|
|
10760
|
+
defaultAnswer: "n",
|
|
10761
|
+
context: {
|
|
10762
|
+
command: body.method,
|
|
10763
|
+
argv: [params.boxPath, params.hostPath]
|
|
10764
|
+
}
|
|
10765
|
+
});
|
|
10766
|
+
if (verdict.answer !== "y") {
|
|
10767
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
10768
|
+
return;
|
|
10769
|
+
}
|
|
10770
|
+
const result = await handleCpRpc(reg, body.method, params);
|
|
10771
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
10772
|
+
send(res, status, result);
|
|
10773
|
+
return;
|
|
10774
|
+
}
|
|
10775
|
+
if (body.method === "download.workspace" || body.method === "download.env" || body.method === "download.config" || body.method === "download.claude") {
|
|
10776
|
+
const params = body.params;
|
|
10777
|
+
const kind = body.method.split(".")[1] ?? "workspace";
|
|
10778
|
+
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
10779
|
+
kind: "confirm",
|
|
10780
|
+
message: `Allow download (${kind}) from ${reg.name}?`,
|
|
10781
|
+
detail: params?.hostPath ?? "(default host location)",
|
|
10782
|
+
defaultAnswer: "n",
|
|
10783
|
+
context: {
|
|
10784
|
+
command: body.method,
|
|
10785
|
+
argv: params?.hostPath ? [params.hostPath] : []
|
|
10786
|
+
}
|
|
10787
|
+
});
|
|
10788
|
+
if (verdict.answer !== "y") {
|
|
10789
|
+
send(res, 500, { exitCode: 10, stdout: "", stderr: "denied by user\n" });
|
|
10790
|
+
return;
|
|
10791
|
+
}
|
|
10792
|
+
const result = await handleDownloadRpc(reg, kind);
|
|
10793
|
+
const status = result.exitCode === 0 ? 200 : 500;
|
|
10794
|
+
send(res, status, result);
|
|
10795
|
+
return;
|
|
10796
|
+
}
|
|
10608
10797
|
if (body.method === "checkpoint.create") {
|
|
10609
10798
|
const result = await handleCheckpointRpc(
|
|
10610
10799
|
reg,
|
|
@@ -10629,6 +10818,7 @@ function createRelayServer(opts) {
|
|
|
10629
10818
|
return;
|
|
10630
10819
|
}
|
|
10631
10820
|
const worktrees = sanitizeWorktrees(body.worktrees);
|
|
10821
|
+
const projectIndex = typeof body.projectIndex === "number" && Number.isFinite(body.projectIndex) && body.projectIndex > 0 ? Math.trunc(body.projectIndex) : void 0;
|
|
10632
10822
|
const reg = {
|
|
10633
10823
|
boxId: body.boxId,
|
|
10634
10824
|
token: body.token,
|
|
@@ -10636,6 +10826,7 @@ function createRelayServer(opts) {
|
|
|
10636
10826
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10637
10827
|
containerName: typeof body.containerName === "string" && body.containerName.length > 0 ? body.containerName : void 0,
|
|
10638
10828
|
createdAt: typeof body.createdAt === "string" && body.createdAt.length > 0 ? body.createdAt : void 0,
|
|
10829
|
+
projectIndex,
|
|
10639
10830
|
worktrees
|
|
10640
10831
|
};
|
|
10641
10832
|
registry.register(reg);
|
|
@@ -10681,11 +10872,66 @@ function createRelayServer(opts) {
|
|
|
10681
10872
|
registeredAt: r.registeredAt,
|
|
10682
10873
|
containerName: r.containerName,
|
|
10683
10874
|
createdAt: r.createdAt,
|
|
10875
|
+
projectIndex: r.projectIndex,
|
|
10684
10876
|
worktrees: r.worktrees ?? []
|
|
10685
10877
|
}));
|
|
10686
10878
|
send(res, 200, { boxes: redacted });
|
|
10687
10879
|
return;
|
|
10688
10880
|
}
|
|
10881
|
+
if (route === "GET /admin/prompts/stream") {
|
|
10882
|
+
const boxId = url.searchParams.get("boxId") ?? "";
|
|
10883
|
+
if (boxId.length === 0) {
|
|
10884
|
+
send(res, 400, { error: "missing boxId query param" });
|
|
10885
|
+
return;
|
|
10886
|
+
}
|
|
10887
|
+
res.statusCode = 200;
|
|
10888
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
10889
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
10890
|
+
res.setHeader("Connection", "keep-alive");
|
|
10891
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
10892
|
+
if (typeof res.flushHeaders === "function") res.flushHeaders();
|
|
10893
|
+
res.write(": connected\n\n");
|
|
10894
|
+
subscribers.add(boxId, res);
|
|
10895
|
+
for (const ev of prompts.forBox(boxId)) {
|
|
10896
|
+
res.write(`event: prompt-ask
|
|
10897
|
+
data: ${JSON.stringify(ev)}
|
|
10898
|
+
|
|
10899
|
+
`);
|
|
10900
|
+
}
|
|
10901
|
+
const heartbeat = setInterval(() => {
|
|
10902
|
+
try {
|
|
10903
|
+
res.write(`event: ping
|
|
10904
|
+
data: {"ts":"${(/* @__PURE__ */ new Date()).toISOString()}"}
|
|
10905
|
+
|
|
10906
|
+
`);
|
|
10907
|
+
} catch {
|
|
10908
|
+
}
|
|
10909
|
+
}, SSE_HEARTBEAT_MS);
|
|
10910
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
10911
|
+
res.on("close", () => {
|
|
10912
|
+
clearInterval(heartbeat);
|
|
10913
|
+
subscribers.remove(boxId, res);
|
|
10914
|
+
});
|
|
10915
|
+
return;
|
|
10916
|
+
}
|
|
10917
|
+
if (route === "POST /admin/prompts/answer") {
|
|
10918
|
+
const body = await readJsonBody(req);
|
|
10919
|
+
if (!isPromptAnswerBody(body)) {
|
|
10920
|
+
send(res, 400, { error: 'expected {id, answer:"y"|"n", cancelled?}' });
|
|
10921
|
+
return;
|
|
10922
|
+
}
|
|
10923
|
+
const targetBox = prompts.boxFor(body.id);
|
|
10924
|
+
const hit = prompts.resolve(body.id, body.answer, body.cancelled);
|
|
10925
|
+
if (!hit) {
|
|
10926
|
+
send(res, 404, { error: "no pending prompt with that id" });
|
|
10927
|
+
return;
|
|
10928
|
+
}
|
|
10929
|
+
if (targetBox) {
|
|
10930
|
+
subscribers.broadcast(targetBox, "prompt-resolved", { id: body.id });
|
|
10931
|
+
}
|
|
10932
|
+
send(res, 204, null);
|
|
10933
|
+
return;
|
|
10934
|
+
}
|
|
10689
10935
|
send(res, 404, { error: "not found", route });
|
|
10690
10936
|
}
|
|
10691
10937
|
function authBox(req, res, reg) {
|
|
@@ -10706,6 +10952,8 @@ function createRelayServer(opts) {
|
|
|
10706
10952
|
registry,
|
|
10707
10953
|
events,
|
|
10708
10954
|
statusStore,
|
|
10955
|
+
prompts,
|
|
10956
|
+
subscribers,
|
|
10709
10957
|
url: `http://${host}:${String(opts.port)}`,
|
|
10710
10958
|
close: () => new Promise((resolve2, reject) => {
|
|
10711
10959
|
server.close((err) => {
|
|
@@ -10719,10 +10967,10 @@ function sanitizeWorktrees(input) {
|
|
|
10719
10967
|
if (!Array.isArray(input)) return void 0;
|
|
10720
10968
|
const out = [];
|
|
10721
10969
|
for (const w of input) {
|
|
10722
|
-
if (w && typeof w.containerPath === "string" && typeof w.
|
|
10970
|
+
if (w && typeof w.containerPath === "string" && typeof w.hostMainRepo === "string" && typeof w.branch === "string") {
|
|
10723
10971
|
out.push({
|
|
10724
10972
|
containerPath: w.containerPath,
|
|
10725
|
-
|
|
10973
|
+
hostMainRepo: w.hostMainRepo,
|
|
10726
10974
|
branch: w.branch
|
|
10727
10975
|
});
|
|
10728
10976
|
}
|
|
@@ -10747,9 +10995,9 @@ async function handleGitRpc(reg, method, params) {
|
|
|
10747
10995
|
stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`
|
|
10748
10996
|
};
|
|
10749
10997
|
}
|
|
10750
|
-
const op = method === "git.
|
|
10998
|
+
const op = method === "git.push" ? "push" : "fetch";
|
|
10751
10999
|
const remote = params?.remote ?? "origin";
|
|
10752
|
-
const argv = ["git", "-C", worktree.
|
|
11000
|
+
const argv = ["git", "-C", worktree.hostMainRepo, op, remote, worktree.branch];
|
|
10753
11001
|
if (Array.isArray(params?.args)) {
|
|
10754
11002
|
for (const a of params.args) {
|
|
10755
11003
|
if (typeof a === "string") argv.push(a);
|
|
@@ -10757,6 +11005,33 @@ async function handleGitRpc(reg, method, params) {
|
|
|
10757
11005
|
}
|
|
10758
11006
|
return runHostCommand(argv);
|
|
10759
11007
|
}
|
|
11008
|
+
async function handleCpRpc(reg, method, params) {
|
|
11009
|
+
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
11010
|
+
if (!entry) {
|
|
11011
|
+
return {
|
|
11012
|
+
exitCode: 64,
|
|
11013
|
+
stdout: "",
|
|
11014
|
+
stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run cp host-side"
|
|
11015
|
+
};
|
|
11016
|
+
}
|
|
11017
|
+
const boxRef = `${reg.name}:${params.boxPath}`;
|
|
11018
|
+
const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath] : [process.execPath, entry, "cp", params.hostPath, boxRef];
|
|
11019
|
+
return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
|
|
11020
|
+
}
|
|
11021
|
+
async function handleDownloadRpc(reg, kind) {
|
|
11022
|
+
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
11023
|
+
if (!entry) {
|
|
11024
|
+
return {
|
|
11025
|
+
exitCode: 64,
|
|
11026
|
+
stdout: "",
|
|
11027
|
+
stderr: "relay: AGENTBOX_CLI_ENTRY not set; cannot run download host-side"
|
|
11028
|
+
};
|
|
11029
|
+
}
|
|
11030
|
+
const argv = [process.execPath, entry, "download"];
|
|
11031
|
+
if (kind !== "workspace") argv.push(kind);
|
|
11032
|
+
argv.push(reg.name, "-y");
|
|
11033
|
+
return runHostCommand(argv, DOWNLOAD_RPC_TIMEOUT_MS);
|
|
11034
|
+
}
|
|
10760
11035
|
async function handleCheckpointRpc(reg, params) {
|
|
10761
11036
|
const entry = process.env.AGENTBOX_CLI_ENTRY;
|
|
10762
11037
|
if (!entry) {
|
|
@@ -10770,6 +11045,7 @@ async function handleCheckpointRpc(reg, params) {
|
|
|
10770
11045
|
if (params?.name) argv.push("--name", params.name);
|
|
10771
11046
|
if (params?.merged === true) argv.push("--merged");
|
|
10772
11047
|
if (params?.setDefault === true) argv.push("--set-default");
|
|
11048
|
+
if (params?.replace === true) argv.push("--replace");
|
|
10773
11049
|
return runHostCommand(argv, CHECKPOINT_RPC_TIMEOUT_MS);
|
|
10774
11050
|
}
|
|
10775
11051
|
function runHostCommand(argv, timeoutMs = GIT_RPC_TIMEOUT_MS) {
|
|
@@ -5,7 +5,21 @@ description: Generate an agentbox.yaml for the current AgentBox workspace. Invok
|
|
|
5
5
|
|
|
6
6
|
# /agentbox-setup
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Box layout (what you're configuring against)
|
|
9
|
+
|
|
10
|
+
Your user i `vscode` and you can use passwordless sudo to run commands as root.
|
|
11
|
+
|
|
12
|
+
`/workspace` is the box's plain writable filesystem — a per-box git worktree on a fresh `agentbox/<box-name>` branch (or a tar-piped copy of the host workspace for non-git projects). Anything you install or build into `/workspace` (incl. `node_modules`, `.next`, `target`, `.venv`) lives in the **container's writable layer** and is captured wholesale by `agentbox checkpoint` (`docker commit`) — so a setup task that runs the install once becomes a warm-start asset for every future box in the project. Everything is wiped on `agentbox destroy`.
|
|
13
|
+
|
|
14
|
+
Three bind mounts wire the box back to the host:
|
|
15
|
+
|
|
16
|
+
- **Host main repo's `.git/`** — bind-mounted RW at its identical absolute host path. In-box commits land on the host's branch refs (visible to `git log` on the host immediately); the box itself carries no SSH/git creds, so `git push` goes through the host relay (`agentbox-ctl git push`). The host's **working tree is never written to** — only refs/objects under `.git/`.
|
|
17
|
+
- **`~/.claude`** — a Docker named volume (`agentbox-claude-config`, shared across boxes by default) seeded from the host's `~/.claude` on each create so auth, skills, and plugins persist without leaking the host's home dir.
|
|
18
|
+
- **`agentbox.yaml`** — read by `agentbox-ctl` from `/workspace`. Tasks and services declared here are what the supervisor will run.
|
|
19
|
+
|
|
20
|
+
## Goal
|
|
21
|
+
|
|
22
|
+
Produce a `/workspace/agentbox.yaml` that captures this project's services, tasks, and box defaults so the in-box supervisor (`agentbox-ctl`) can boot the workspace deterministically.
|
|
9
23
|
|
|
10
24
|
`agentbox.yaml` is **declarative**. The supervisor reads it on box start, but you don't have to restart the box: after you write the file, `agentbox-ctl reload` (run from inside the box) makes the already-running supervisor re-read it and immediately run the declared tasks and autostart the services. See step 8.
|
|
11
25
|
|
|
@@ -23,10 +37,12 @@ Look at `/workspace`:
|
|
|
23
37
|
## 2. Pick services and tasks
|
|
24
38
|
|
|
25
39
|
- **Services** = long-running. Web servers, watchers, queue workers, databases. `restart: on-failure` by default.
|
|
26
|
-
- **Tasks** = one-shot. `pnpm install`, DB migrations, codegen, fixture loaders. Wire dependent services with `needs:` so they wait for the task to finish successfully.
|
|
40
|
+
- **Tasks** = one-shot. `pnpm install`, DB migrations, codegen, fixture loaders, install apt packages. Wire dependent services with `needs:` so they wait for the task to finish successfully.
|
|
27
41
|
- Names: must match `[A-Za-z0-9_-]+`. Task names and service names share a namespace — no collisions.
|
|
28
42
|
- No cycles in `needs:`.
|
|
29
|
-
- **Always generate a dependency-install task** and make it the root of the `needs:` graph (every service that needs deps gets `needs: [install, …]`).
|
|
43
|
+
- **Always generate a dependency-install task** and make it the root of the `needs:` graph (every service that needs deps gets `needs: [install, …]`). Future boxes start from a snapshot of the final filesystem so they won't need this, but updates or moving to a cloud provider might need to rebuild the container from scratch. The filesystem can be then later captured by `agentbox-ctl checkpoint --set-default`. The task must be **idempotent and self-healing**: `agentbox-ctl` re-runs pending tasks on every box stop/start (the daemon dies with the container and is relaunched), so a plain `rm -rf node_modules && install` would wipe + reinstall on every start. Guard the rebuild with a marker file *inside* `node_modules` (the `.agentbox-installed` convention AgentBox uses internally): rebuild only when the marker is absent (fresh box), and be a fast no-op once it exists. Detect the package manager from the lockfile — never hardcode `pnpm`. See the worked example below.
|
|
44
|
+
- **Add a comment to the beginning** of the file to explain what you did and what issues you encountered, so that future run might use this information in case the project evolves and you need to update the agentbox.yaml file.
|
|
45
|
+
-
|
|
30
46
|
|
|
31
47
|
## 3. Wire readiness probes (services only)
|
|
32
48
|
|
|
@@ -61,7 +77,7 @@ Per service:
|
|
|
61
77
|
|
|
62
78
|
Sets per-project defaults for `agentbox create`/`claude`/`code`/`shell` — same shape as `~/.agentbox/config.yaml`. CLI flags still override. Common keys:
|
|
63
79
|
|
|
64
|
-
- `box.hostSnapshot` (bool) —
|
|
80
|
+
- `box.hostSnapshot` (bool) — APFS-clone the *host* workspace into a per-box scratch dir before seeding `/workspace` (stabilizes the tar-pipe source).
|
|
65
81
|
- `box.defaultCheckpoint` (string) — checkpoint new boxes start from (normally you set this via `agentbox-ctl checkpoint --set-default` at the end of setup — see section 9, not by hand).
|
|
66
82
|
- `box.withPlaywright` (bool) — install `@playwright/cli` globally inside the box.
|
|
67
83
|
- `box.vnc` (bool) — run Xvnc + noVNC on container port 6080.
|
|
@@ -76,6 +92,11 @@ Full key list (run on the host): `agentbox config list --keys`.
|
|
|
76
92
|
|
|
77
93
|
```yaml
|
|
78
94
|
# yaml-language-server: $schema=https://agentbox.dev/schema/agentbox.schema.json
|
|
95
|
+
# This agentbox.yaml setup this Next.js project, and includes:
|
|
96
|
+
# - a postgres database because it's used in the project
|
|
97
|
+
# - an inngest server for queues
|
|
98
|
+
# - a fix to move .turbo/cache folder to the workspace to avoid a permission error during setup
|
|
99
|
+
# - ...
|
|
79
100
|
defaults:
|
|
80
101
|
box:
|
|
81
102
|
withPlaywright: true
|
|
@@ -83,30 +104,23 @@ defaults:
|
|
|
83
104
|
ide: cursor
|
|
84
105
|
|
|
85
106
|
tasks:
|
|
86
|
-
# Idempotent install.
|
|
87
|
-
#
|
|
88
|
-
# node_modules is macOS-native
|
|
89
|
-
#
|
|
90
|
-
# (agentbox-ctl re-runs pending tasks after
|
|
91
|
-
# lockfile detection to the project's package
|
|
107
|
+
# Idempotent install. /workspace is the container's writable filesystem, so
|
|
108
|
+
# node_modules persists across pause/stop/start and is captured by
|
|
109
|
+
# `agentbox checkpoint`. The host's node_modules is macOS-native and is
|
|
110
|
+
# never copied in, so force a clean Linux build the first time — but skip
|
|
111
|
+
# on every subsequent box start (agentbox-ctl re-runs pending tasks after
|
|
112
|
+
# stop/start). Adjust the lockfile detection to the project's package
|
|
113
|
+
# manager.
|
|
92
114
|
install:
|
|
93
115
|
command: |
|
|
94
116
|
set -e
|
|
95
117
|
MARKER=node_modules/.agentbox-installed
|
|
96
118
|
[ -f "$MARKER" ] && { echo "deps installed (marker present) — skip"; exit 0; }
|
|
119
|
+
apt-get update && apt-get install -y postgresql-client
|
|
97
120
|
rm -rf node_modules
|
|
98
121
|
if [ -f pnpm-lock.yaml ]; then
|
|
99
122
|
corepack enable >/dev/null 2>&1 || true
|
|
100
123
|
pnpm install --frozen-lockfile || pnpm install
|
|
101
|
-
elif [ -f yarn.lock ]; then
|
|
102
|
-
corepack enable >/dev/null 2>&1 || true
|
|
103
|
-
yarn install --frozen-lockfile || yarn install
|
|
104
|
-
elif [ -f bun.lockb ] || [ -f bun.lock ]; then
|
|
105
|
-
bun install
|
|
106
|
-
elif [ -f package-lock.json ]; then
|
|
107
|
-
npm ci || npm install
|
|
108
|
-
else
|
|
109
|
-
npm install
|
|
110
124
|
fi
|
|
111
125
|
touch "$MARKER"
|
|
112
126
|
|
|
@@ -149,44 +163,19 @@ services:
|
|
|
149
163
|
1. Write the file to `/workspace/agentbox.yaml`.
|
|
150
164
|
2. **Apply it live**: from inside the box run `agentbox-ctl reload`. The already-running supervisor re-reads the config and immediately runs the declared tasks and autostarts the services — no box restart needed. It prints the `added` / `removed` / `changed` diff. If it errors because the daemon isn't running, the config is still valid: the next `agentbox start` (or `agentbox create` in this workspace) picks it up automatically.
|
|
151
165
|
3. Confirm with `agentbox-ctl status`: tasks should be `running` or `done`, autostart services `starting` or `ready`. If something failed, tail it with `agentbox-ctl logs <service>` and fix the config, then `agentbox-ctl reload` again.
|
|
152
|
-
|
|
153
|
-
4. **Then do section 9** — once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --set-default` so future boxes start ready. This is the real final step; don't stop at hand-off.
|
|
166
|
+
4. Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --set-default` so future boxes start ready.
|
|
154
167
|
|
|
155
168
|
5. Tell the user:
|
|
156
169
|
|
|
157
170
|
> I wrote `/workspace/agentbox.yaml` and ran `agentbox-ctl reload` so the supervisor is already running the declared tasks/services. To land the file on the host:
|
|
158
171
|
> - I've created a checkpoint of the warm box state so future boxes start ready in seconds, no reinstall.
|
|
159
172
|
> - commit it inside the box (`git add agentbox.yaml && git commit -m 'add agentbox config'`) — the box's `.git/` is bind-mounted, so the commit shows up on the host immediately; or
|
|
160
|
-
> - on the host, tell the user to run `agentbox
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
## 9. Checkpoint the warm state (do this at the very end)
|
|
164
|
-
|
|
165
|
-
Once `agentbox-ctl status` shows the install task `done` and services `ready` — i.e. the box is fully warmed up — capture a **checkpoint** so future boxes in this project start from this exact state instead of cold.
|
|
173
|
+
> - on the host, tell the user to run `agentbox download config` to update their original host workspace.
|
|
166
174
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
- `node_modules` (and `.next`, `target`, `.venv`, build caches) — the expensive Linux-native install,
|
|
170
|
-
- `.env` / `.env.*` / `secrets.toml` and any other gitignored config files present in `/workspace`,
|
|
171
|
-
- any other files written during setup.
|
|
172
|
-
|
|
173
|
-
A new box created from the checkpoint reuses all of it (the install task's marker is already present, so it no-ops), while git-tracked code still comes fresh from the host's current HEAD. Result: new boxes are ready in seconds with no reinstall and no missing env vars.
|
|
174
|
-
|
|
175
|
-
**Run this from inside the box, as the final step:**
|
|
176
|
-
|
|
177
|
-
```sh
|
|
178
|
-
agentbox-ctl checkpoint --set-default
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
This routes through the host relay (no host shell needed), captures `<box-name>-<n>`, and writes `box.defaultCheckpoint` into the project's config so **every future `agentbox create` / `claude` in this project starts warm automatically**. Pass `--name <name>` for a stable name, or omit `--set-default` to capture without changing the default. Re-run it whenever the warm state meaningfully changes (e.g. after a dependency bump you want every new box to inherit).
|
|
182
|
-
|
|
183
|
-
Then tell the user, e.g.:
|
|
184
|
-
|
|
185
|
-
> I checkpointed the warm box state (node_modules, env files, build caches) and set it as this project's default — `agentbox create` will now spin up new boxes from it in seconds, no reinstall.
|
|
186
|
-
|
|
187
|
-
## 10. Known issues
|
|
175
|
+
## 9. Known issues
|
|
188
176
|
|
|
189
177
|
- For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
|
|
190
|
-
|
|
178
|
+
|
|
191
179
|
- The `install` task is intentionally a no-op once `node_modules/.agentbox-installed` exists. Do **not** remove the marker guard to "force a fresh install" — that reinstalls on every box start. To force a one-off rebuild, delete `node_modules` (or just the marker) then run `agentbox-ctl reload`.
|
|
192
|
-
|
|
180
|
+
|
|
181
|
+
- Host-only CLI wrappers (portless, etc.) must be bypassed, eg some projects wrap the dev server with a host-side proxy (here: `portless projectname next dev --turbopack`). Override the service command: to call the underlying tool directly (`next dev --turbopack`)
|