toolroll 0.9.60 → 0.9.62
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/CHANGELOG.md +32 -0
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment.d.ts +4 -0
- package/dist/assignment.js +8 -6
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +113 -13
- package/dist/chat-channel.js +9 -0
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/cli.js +28 -11
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +14 -3
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +170 -0
- package/dist/landed-cleanup.js +814 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +4 -4
- package/dist/lead-doors.js +45 -1
- package/dist/lead-tools.js +40 -7
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -26
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-admission.d.ts +10 -6
- package/dist/project-admission.js +33 -11
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +32 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +65 -8
- package/dist/provider.js +80 -21
- package/dist/remote-main.d.ts +57 -0
- package/dist/remote-main.js +179 -0
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +19 -1
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +12 -0
- package/dist/server/render-tasks.d.ts +15 -2
- package/dist/server/render-tasks.js +30 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +5 -3
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-status.d.ts +8 -2
- package/dist/steer-status.js +19 -5
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +48 -0
- package/dist/storage-sweep-process.js +204 -0
- package/dist/storage-sweep-schedule.d.ts +30 -0
- package/dist/storage-sweep-schedule.js +164 -0
- package/dist/storage-sweep.d.ts +31 -6
- package/dist/storage-sweep.js +165 -39
- package/dist/storage-ui.d.ts +4 -0
- package/dist/storage-ui.js +22 -7
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +200 -3
- package/dist/store.js +443 -17
- package/dist/surface.js +8 -4
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-base.d.ts +35 -0
- package/dist/task-base.js +51 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +57 -3
- package/dist/worktree.js +201 -23
- package/package.json +1 -1
- package/scripts/provider-canary.mjs +235 -147
package/dist/exec.js
CHANGED
|
@@ -420,7 +420,10 @@ export const DEFAULT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
|
420
420
|
* watchdog with no wall-clock deadline. Both roads share the same kill and
|
|
421
421
|
* settlement semantics.
|
|
422
422
|
*/
|
|
423
|
-
function streamWatchdog(options, onTimeout
|
|
423
|
+
function streamWatchdog(options, onTimeout,
|
|
424
|
+
/** The idle limit was reached: true when the caller ended the wait its own way (a paused live session closes
|
|
425
|
+
* stdin), so the same limit runs again for the process to exit before it is killed. */
|
|
426
|
+
onIdle) {
|
|
424
427
|
const hardMs = options.timeoutMs ?? (options.idleTimeoutMs === undefined ? DEFAULT_TIMEOUT_MS : undefined);
|
|
425
428
|
const idleMs = options.idleTimeoutMs;
|
|
426
429
|
let fired = false;
|
|
@@ -432,12 +435,25 @@ function streamWatchdog(options, onTimeout) {
|
|
|
432
435
|
fired = true;
|
|
433
436
|
onTimeout();
|
|
434
437
|
};
|
|
438
|
+
const idle = () => {
|
|
439
|
+
let handled = false;
|
|
440
|
+
try {
|
|
441
|
+
handled = onIdle?.() === true;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
handled = false;
|
|
445
|
+
}
|
|
446
|
+
if (handled)
|
|
447
|
+
armIdle();
|
|
448
|
+
else
|
|
449
|
+
fire();
|
|
450
|
+
};
|
|
435
451
|
const armIdle = () => {
|
|
436
452
|
if (idleMs === undefined)
|
|
437
453
|
return;
|
|
438
454
|
if (idleTimer !== undefined)
|
|
439
455
|
clearTimeout(idleTimer);
|
|
440
|
-
idleTimer = setTimeout(
|
|
456
|
+
idleTimer = setTimeout(idle, idleMs);
|
|
441
457
|
idleTimer.unref?.();
|
|
442
458
|
};
|
|
443
459
|
if (hardMs !== undefined) {
|
|
@@ -725,14 +741,36 @@ const JSONL_OVERFLOW_PREFIX_CAP = 4 * 1024;
|
|
|
725
741
|
* storage can be reused for the next record. Counting the original bytes
|
|
726
742
|
* (rather than JavaScript characters) makes the overflow receipt exact.
|
|
727
743
|
*/
|
|
728
|
-
function boundedJsonlFramer(cap, onLine, inspectDiscriminants = false) {
|
|
744
|
+
function boundedJsonlFramer(cap, onLine, inspectDiscriminants = false, tailCap = 0) {
|
|
729
745
|
const retained = Buffer.allocUnsafe(cap);
|
|
730
746
|
let retainedBytes = 0;
|
|
731
747
|
let originalBytes = 0;
|
|
732
748
|
let nonWhitespace = false;
|
|
733
749
|
let discriminants = inspectDiscriminants ? jsonlDiscriminants() : null;
|
|
750
|
+
// An oversized line's last tailCap bytes, kept only once the line passes the cap (Claude's result carries its cost
|
|
751
|
+
// after the words that made it too large). Seeded from the retained prefix's end, so it is always the line's true end.
|
|
752
|
+
const tailSize = Math.max(0, Math.min(tailCap, cap));
|
|
753
|
+
const tail = tailSize === 0 ? null : Buffer.allocUnsafe(tailSize);
|
|
754
|
+
let tailBytes = 0;
|
|
755
|
+
let tailSeeded = false;
|
|
756
|
+
const keepTail = (part) => {
|
|
757
|
+
if (tail === null)
|
|
758
|
+
return;
|
|
759
|
+
if (!tailSeeded) {
|
|
760
|
+
tailSeeded = true;
|
|
761
|
+
tailBytes = retained.copy(tail, 0, retainedBytes - tailSize, retainedBytes);
|
|
762
|
+
}
|
|
763
|
+
if (part.length >= tailSize) {
|
|
764
|
+
tailBytes = part.copy(tail, 0, part.length - tailSize);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const keepOld = Math.min(tailBytes, tailSize - part.length);
|
|
768
|
+
tail.copyWithin(0, tailBytes - keepOld, tailBytes);
|
|
769
|
+
tailBytes = keepOld + part.copy(tail, keepOld);
|
|
770
|
+
};
|
|
734
771
|
const append = (part) => {
|
|
735
772
|
discriminants?.push(part);
|
|
773
|
+
const before = originalBytes;
|
|
736
774
|
originalBytes += part.length;
|
|
737
775
|
if (!nonWhitespace) {
|
|
738
776
|
for (const byte of part) {
|
|
@@ -745,6 +783,9 @@ function boundedJsonlFramer(cap, onLine, inspectDiscriminants = false) {
|
|
|
745
783
|
if (retainedBytes < cap) {
|
|
746
784
|
retainedBytes += part.copy(retained, retainedBytes, 0, cap - retainedBytes);
|
|
747
785
|
}
|
|
786
|
+
// The bytes up to the cap are in the prefix; only what runs past it moves the tail.
|
|
787
|
+
if (tail !== null && originalBytes > cap)
|
|
788
|
+
keepTail(before >= cap ? part : part.subarray(cap - before));
|
|
748
789
|
};
|
|
749
790
|
const emit = () => {
|
|
750
791
|
onLine({
|
|
@@ -752,7 +793,10 @@ function boundedJsonlFramer(cap, onLine, inspectDiscriminants = false) {
|
|
|
752
793
|
bytesOriginal: originalBytes,
|
|
753
794
|
overflowed: originalBytes > cap,
|
|
754
795
|
...(discriminants === null ? {} : { discriminants: discriminants.finish() }),
|
|
796
|
+
...(tail !== null && originalBytes > cap ? { tail: tail.subarray(0, tailBytes) } : {}),
|
|
755
797
|
});
|
|
798
|
+
tailBytes = 0;
|
|
799
|
+
tailSeeded = false;
|
|
756
800
|
retainedBytes = 0;
|
|
757
801
|
originalBytes = 0;
|
|
758
802
|
nonWhitespace = false;
|
|
@@ -866,6 +910,85 @@ function leadingClaudeSystemSubtype(prefix) {
|
|
|
866
910
|
const match = /^\s*\{\s*"type"\s*:\s*"system"\s*,\s*"subtype"\s*:\s*"([A-Za-z0-9._-]+)"/.exec(prefix.toString("utf8", 0, Math.min(prefix.length, 512)));
|
|
867
911
|
return match?.[1] ?? null;
|
|
868
912
|
}
|
|
913
|
+
/** How much of an oversized Claude result's end is kept to read its cost and usage (they follow the words). */
|
|
914
|
+
const CLAUDE_RESULT_TAIL_CAP = 64 * 1024;
|
|
915
|
+
/**
|
|
916
|
+
* Where a key's value starts in a fragment of JSON text, once and only once. `"key":` never occurs inside a JSON
|
|
917
|
+
* string (every quote there is escaped, so `y":` cannot appear), so each match is a real key, though possibly of a
|
|
918
|
+
* nested object. A key seen more than once is ambiguous and reads as absent.
|
|
919
|
+
*/
|
|
920
|
+
function soleKeyValueAt(texts, key) {
|
|
921
|
+
const pattern = new RegExp(`"${key}"\\s*:\\s*`, "g");
|
|
922
|
+
let found = null;
|
|
923
|
+
for (const text of texts) {
|
|
924
|
+
for (const match of text.matchAll(pattern)) {
|
|
925
|
+
if (found !== null)
|
|
926
|
+
return null;
|
|
927
|
+
found = { text, at: match.index + match[0].length };
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return found;
|
|
931
|
+
}
|
|
932
|
+
/** The JSON object starting at `at`, read to its matching brace (strings and escapes respected), or null. */
|
|
933
|
+
function jsonObjectAt(text, at) {
|
|
934
|
+
if (text[at] !== "{")
|
|
935
|
+
return null;
|
|
936
|
+
let depth = 0, inString = false;
|
|
937
|
+
for (let i = at; i < text.length && i - at < CLAUDE_RESULT_TAIL_CAP; i += 1) {
|
|
938
|
+
const c = text[i];
|
|
939
|
+
if (inString) {
|
|
940
|
+
if (c === "\\")
|
|
941
|
+
i += 1;
|
|
942
|
+
else if (c === '"')
|
|
943
|
+
inString = false;
|
|
944
|
+
}
|
|
945
|
+
else if (c === '"')
|
|
946
|
+
inString = true;
|
|
947
|
+
else if (c === "{")
|
|
948
|
+
depth += 1;
|
|
949
|
+
else if (c === "}" && --depth === 0) {
|
|
950
|
+
try {
|
|
951
|
+
const value = JSON.parse(text.slice(at, i + 1));
|
|
952
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* The accounting an oversized Claude result still carries, read by a bounded scan of its kept prefix and tail: the
|
|
963
|
+
* session's running `total_cost_usd`, its `usage` and `num_turns`, and whether it is a main result at all (an
|
|
964
|
+
* `origin` naming anything but a person makes it a background task's). Anything unreadable or seen twice is absent.
|
|
965
|
+
*/
|
|
966
|
+
export function scanOversizedClaudeResult(prefix, tail) {
|
|
967
|
+
const texts = [prefix.toString("utf8"), ...(tail === undefined ? [] : [tail.toString("utf8")])];
|
|
968
|
+
const origin = soleKeyValueAt(texts, "origin");
|
|
969
|
+
const originObject = origin === null ? null : jsonObjectAt(origin.text, origin.at);
|
|
970
|
+
const primary = originObject === null || String(originObject["kind"] ?? "") === "human";
|
|
971
|
+
const account = {};
|
|
972
|
+
const number = (key) => {
|
|
973
|
+
const at = soleKeyValueAt(texts, key);
|
|
974
|
+
const match = at === null ? null : /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(at.text.slice(at.at, at.at + 64));
|
|
975
|
+
const value = match === null ? NaN : Number(match[0]);
|
|
976
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
977
|
+
};
|
|
978
|
+
const cost = number("total_cost_usd");
|
|
979
|
+
if (cost !== null)
|
|
980
|
+
account["total_cost_usd"] = cost;
|
|
981
|
+
const turns = number("num_turns");
|
|
982
|
+
if (turns !== null)
|
|
983
|
+
account["num_turns"] = turns;
|
|
984
|
+
const usageAt = soleKeyValueAt(texts, "usage");
|
|
985
|
+
const usage = usageAt === null ? null : jsonObjectAt(usageAt.text, usageAt.at);
|
|
986
|
+
if (usage !== null)
|
|
987
|
+
account["usage"] = usage;
|
|
988
|
+
return { primary, account };
|
|
989
|
+
}
|
|
990
|
+
/** Marks a Claude result whose session cost could not be read: the gateway charges the run's dollar cap instead. */
|
|
991
|
+
export const CLAUDE_COST_UNKNOWN = "standing_orders_cost_unknown";
|
|
869
992
|
function malformedCodexEventIsLoadBearing(prefix) {
|
|
870
993
|
const type = leadingJsonObjectType(prefix);
|
|
871
994
|
if (type === "thread.started" || type === "turn.completed" || type === "turn.failed")
|
|
@@ -1331,15 +1454,45 @@ function primaryResultOrigin(event) {
|
|
|
1331
1454
|
return true;
|
|
1332
1455
|
return false; // task-notification, channel, peer, coordinator, unknown: never the envelope
|
|
1333
1456
|
}
|
|
1457
|
+
/** The usage fields Claude reports per message (not per session): a live session's account adds them up. */
|
|
1458
|
+
const LIVE_SUMMED_USAGE = ["input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"];
|
|
1459
|
+
/**
|
|
1460
|
+
* A live session's account so far (one main result per turn, checked on Claude Code 2.1.290): the latest result's
|
|
1461
|
+
* session, ending, words and cost — `total_cost_usd` is already the session's running total — with the usage token
|
|
1462
|
+
* fields and `num_turns`, which count only their own message, added up across every main result. A field that is not
|
|
1463
|
+
* a non-negative number adds nothing.
|
|
1464
|
+
*/
|
|
1465
|
+
export function cumulativeLiveResult(previous, latest) {
|
|
1466
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
1467
|
+
const usageOf = (event) => event["usage"] !== null && typeof event["usage"] === "object" && !Array.isArray(event["usage"]) ? event["usage"] : null;
|
|
1468
|
+
const before = usageOf(previous), now = usageOf(latest);
|
|
1469
|
+
const usage = { ...(now ?? {}) };
|
|
1470
|
+
for (const key of LIVE_SUMMED_USAGE) {
|
|
1471
|
+
const a = before === null ? null : count(before[key]), b = now === null ? null : count(now[key]);
|
|
1472
|
+
if (a !== null || b !== null)
|
|
1473
|
+
usage[key] = (a ?? 0) + (b ?? 0);
|
|
1474
|
+
}
|
|
1475
|
+
const turnsBefore = count(previous["num_turns"]), turnsNow = count(latest["num_turns"]);
|
|
1476
|
+
return {
|
|
1477
|
+
...latest,
|
|
1478
|
+
...(before === null && now === null ? {} : { usage }),
|
|
1479
|
+
...(turnsBefore === null && turnsNow === null ? {} : { num_turns: (turnsBefore ?? 0) + (turnsNow ?? 0) }),
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
/** This transport can keep stdin open for a live session (RunOptions.liveInput). */
|
|
1483
|
+
export const STREAM_INPUT = Symbol.for("toolroll.streamInput");
|
|
1484
|
+
/** Whether a runner declares it keeps stdin open for a live session; an injected runner opts in by carrying the mark. */
|
|
1485
|
+
export const streamsInput = (runner) => typeof runner === "function" && runner[STREAM_INPUT] === true;
|
|
1334
1486
|
export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
1335
1487
|
const { cwd } = options;
|
|
1336
1488
|
const childEnv = resolveChildEnv(options);
|
|
1489
|
+
const live = options.liveInput ?? null;
|
|
1337
1490
|
return new Promise(resolve => {
|
|
1338
1491
|
let child;
|
|
1339
1492
|
let container = null;
|
|
1340
1493
|
let attachFailure = null;
|
|
1341
1494
|
try {
|
|
1342
|
-
const spawned = spawnStream(file, args, options, ["ignore", "pipe", "pipe"], childEnv);
|
|
1495
|
+
const spawned = spawnStream(file, args, options, [live === null ? "ignore" : "pipe", "pipe", "pipe"], childEnv);
|
|
1343
1496
|
child = spawned.child;
|
|
1344
1497
|
container = spawned.container;
|
|
1345
1498
|
void spawned.attached.then(outcome => { if (!outcome.ok)
|
|
@@ -1359,6 +1512,12 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1359
1512
|
let conflictingInitLine = null;
|
|
1360
1513
|
let initSessionId = null;
|
|
1361
1514
|
let resultLine = null;
|
|
1515
|
+
let resultOverflowed = false;
|
|
1516
|
+
// The run's account: the first main result, or for a live session every main result added up (each oversized
|
|
1517
|
+
// one's scanned cost and usage too). Its cost is the highest running total seen; costUnknown when the latest main
|
|
1518
|
+
// result's cost could not be read (an oversized one), which only a later readable result can clear.
|
|
1519
|
+
let account = null;
|
|
1520
|
+
let costUnknown = false;
|
|
1362
1521
|
let overflowLine = null;
|
|
1363
1522
|
let receiptFired = false;
|
|
1364
1523
|
let stderr = "";
|
|
@@ -1380,6 +1539,26 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1380
1539
|
// note stays honestly unreceipted and re-attaches later.
|
|
1381
1540
|
}
|
|
1382
1541
|
};
|
|
1542
|
+
const tally = (event, line) => {
|
|
1543
|
+
const before = account;
|
|
1544
|
+
account = account === null || live === null ? event : { ...(line === null ? account : {}), ...cumulativeLiveResult(account, event) };
|
|
1545
|
+
const cost = event["total_cost_usd"];
|
|
1546
|
+
const readable = typeof cost === "number" && Number.isFinite(cost) && cost >= 0;
|
|
1547
|
+
// The running total only grows: an earlier, higher total stands (a session's cost is never given back).
|
|
1548
|
+
const prior = before?.["total_cost_usd"];
|
|
1549
|
+
if (readable && typeof prior === "number" && prior > cost)
|
|
1550
|
+
account = { ...account, total_cost_usd: prior };
|
|
1551
|
+
if (line === null)
|
|
1552
|
+
costUnknown = !readable;
|
|
1553
|
+
else if (readable)
|
|
1554
|
+
costUnknown = false;
|
|
1555
|
+
if (resultOverflowed)
|
|
1556
|
+
return;
|
|
1557
|
+
if (line !== null && account === event)
|
|
1558
|
+
resultLine = line;
|
|
1559
|
+
else if (line !== null || resultLine !== null)
|
|
1560
|
+
resultLine = JSON.stringify(account);
|
|
1561
|
+
};
|
|
1383
1562
|
const keep = (framed) => {
|
|
1384
1563
|
if (framed.overflowed) {
|
|
1385
1564
|
// An oversized result is the first primary candidate and therefore
|
|
@@ -1404,10 +1583,22 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1404
1583
|
overflowLine = claudeOverflowLine(framed);
|
|
1405
1584
|
return;
|
|
1406
1585
|
}
|
|
1407
|
-
|
|
1586
|
+
// Nothing inside an oversized result is trusted: the agent shapes its bytes (tool inputs in permission_denials
|
|
1587
|
+
// are real JSON objects), so a planted total_cost_usd or origin could read as genuine. It counts as the main
|
|
1588
|
+
// result with an unknown cost, charged at the run's cap, and a live session's input closes.
|
|
1589
|
+
const scanned = { primary: true, account: {} };
|
|
1590
|
+
if (live !== null || resultLine === null)
|
|
1591
|
+
tally(scanned.account, null);
|
|
1592
|
+
if (resultLine === null) {
|
|
1408
1593
|
resultLine = claudeOverflowLine(framed);
|
|
1594
|
+
resultOverflowed = true;
|
|
1595
|
+
}
|
|
1409
1596
|
else if (overflowLine === null)
|
|
1410
1597
|
overflowLine = claudeOverflowLine(framed);
|
|
1598
|
+
// A live session whose main result was too large to read cannot be steered safely: close its input, so the
|
|
1599
|
+
// process ends instead of running further turns nobody can account for.
|
|
1600
|
+
if (live !== null && scanned.primary)
|
|
1601
|
+
live.close("settled");
|
|
1411
1602
|
return;
|
|
1412
1603
|
}
|
|
1413
1604
|
const line = framed.prefix.toString("utf8");
|
|
@@ -1458,8 +1649,11 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1458
1649
|
// v105: the plan's usage windows, as Claude says them each turn (the Tasks page's limits).
|
|
1459
1650
|
noteLimits(claudeLimitsOf(event));
|
|
1460
1651
|
}
|
|
1461
|
-
else if (type === "result" && resultLine === null && primaryResultOrigin(event)) {
|
|
1462
|
-
|
|
1652
|
+
else if (type === "result" && (resultLine === null || live !== null) && primaryResultOrigin(event)) {
|
|
1653
|
+
// A live session ends a turn with a result each time: the run's account is the last one, with every main
|
|
1654
|
+
// result's usage and turns added up (account). After an oversized main result the retained result stays the
|
|
1655
|
+
// fail-closed overflow, but later results still count toward its cost.
|
|
1656
|
+
tally(event, line);
|
|
1463
1657
|
// Fallback receipt (finding 11): a SUCCESSFUL main-query completion
|
|
1464
1658
|
// entails the prompt ran even if the stream elided assistant
|
|
1465
1659
|
// events. Error results never fire — startup death is not delivery.
|
|
@@ -1474,15 +1668,32 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1474
1668
|
// Observational only: a broken listener never touches the run.
|
|
1475
1669
|
}
|
|
1476
1670
|
}
|
|
1671
|
+
// After the run's own bookkeeping: the session may close stdin on this event.
|
|
1672
|
+
if (live !== null)
|
|
1673
|
+
live.observe(event);
|
|
1477
1674
|
};
|
|
1478
|
-
|
|
1675
|
+
// The retained main result as the run's account: an oversized one stays the fail-closed overflow but carries the
|
|
1676
|
+
// cost and usage read from it and from later results; a cost that could not be read says so (CLAUDE_COST_UNKNOWN).
|
|
1677
|
+
const accountedResultLine = () => {
|
|
1678
|
+
if (resultLine === null || (!resultOverflowed && !costUnknown))
|
|
1679
|
+
return resultLine;
|
|
1680
|
+
const kept = JSON.parse(resultLine);
|
|
1681
|
+
const counted = {};
|
|
1682
|
+
if (resultOverflowed && account !== null) {
|
|
1683
|
+
for (const key of ["total_cost_usd", "usage", "num_turns"])
|
|
1684
|
+
if (account[key] !== undefined)
|
|
1685
|
+
counted[key] = account[key];
|
|
1686
|
+
}
|
|
1687
|
+
return JSON.stringify({ ...kept, ...counted, ...(costUnknown ? { [CLAUDE_COST_UNKNOWN]: true } : {}) });
|
|
1688
|
+
};
|
|
1689
|
+
const framing = boundedJsonlFramer(JSONL_EVENT_HARD_CAP, keep, false, CLAUDE_RESULT_TAIL_CAP);
|
|
1479
1690
|
const watchdog = streamWatchdog(options, () => {
|
|
1480
1691
|
timedOut = true;
|
|
1481
1692
|
if (options.processGroup === true)
|
|
1482
1693
|
killGroup(child);
|
|
1483
1694
|
else
|
|
1484
1695
|
child.kill("SIGKILL");
|
|
1485
|
-
});
|
|
1696
|
+
}, live === null ? undefined : () => live.idleLimit());
|
|
1486
1697
|
child.stdout?.on("data", (chunk) => {
|
|
1487
1698
|
watchdog.touch();
|
|
1488
1699
|
framing.push(chunk);
|
|
@@ -1501,7 +1712,8 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1501
1712
|
watchdog.stop();
|
|
1502
1713
|
liveProviders.delete(child);
|
|
1503
1714
|
framing.finish();
|
|
1504
|
-
|
|
1715
|
+
live?.detach();
|
|
1716
|
+
const lines = [initLine, initIdentityLine, conflictingInitLine, accountedResultLine(), overflowLine]
|
|
1505
1717
|
.filter((one) => one !== null)
|
|
1506
1718
|
.filter((line, index, all) => all.indexOf(line) === index);
|
|
1507
1719
|
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
|
|
@@ -1521,5 +1733,12 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1521
1733
|
finish(null);
|
|
1522
1734
|
});
|
|
1523
1735
|
child.on("close", code => finish(code));
|
|
1736
|
+
if (live !== null && child.stdin !== null) {
|
|
1737
|
+
const input = child.stdin;
|
|
1738
|
+
// A pipe the child closed (it exited) ends live input; the run settles on the process, as always.
|
|
1739
|
+
input.on("error", () => live.detach("input-failed"));
|
|
1740
|
+
live.attach({ write: line => { input.write(line, "utf8"); }, end: () => { input.end(); } });
|
|
1741
|
+
}
|
|
1524
1742
|
});
|
|
1525
1743
|
}
|
|
1744
|
+
runClaudeStreamJsonl[STREAM_INPUT] = true;
|
package/dist/invoke.js
CHANGED
|
@@ -16,19 +16,21 @@
|
|
|
16
16
|
* (`adapterFor`), and only builder, planner, reviewer, and scout import
|
|
17
17
|
* `invokeAgent`.
|
|
18
18
|
*/
|
|
19
|
-
import { adapterFor, auditOf } from "./provider.js";
|
|
19
|
+
import { adapterFor, auditOf, isolationUnsupported } from "./provider.js";
|
|
20
20
|
import { readProviderKey, readAuthModeStrict, PROVIDER_KEY_ENV, OWN_KEY_ENV } from "./keys.js";
|
|
21
21
|
import { classifyTerminal } from "./exhaustion.js";
|
|
22
22
|
import { liftAuthPause } from "./provider-auth.js";
|
|
23
23
|
import { attestProvider } from "./attest.js";
|
|
24
|
-
import { run as runCommand, runOwnerTag } from "./exec.js";
|
|
24
|
+
import { run as runCommand, runOwnerTag, streamsInput } from "./exec.js";
|
|
25
25
|
import { currentContainment } from "./containment.js";
|
|
26
26
|
import { witnessedRunner } from "./process-custody.js";
|
|
27
27
|
import { underStopWatch } from "./task-control.js";
|
|
28
28
|
import { childDatabaseEnv, isolatedChildDatabase, removeChildDatabase as removeAgentDatabase } from "./child-database.js";
|
|
29
29
|
import { noToolsArgs, prepareRunTools } from "./project-tools.js";
|
|
30
|
-
import { realpathSync } from "node:fs";
|
|
31
|
-
import {
|
|
30
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
31
|
+
import { homedir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { agentFence, linuxFenceAvailable, macosFenceAvailable, taskBoundary } from "./agent-fence.js";
|
|
32
34
|
import { billingOf, claudeBillingFrom, seenBilling } from "./spend.js";
|
|
33
35
|
/** The claude binary name — kept for the legacy quota rows and tests that
|
|
34
36
|
* describe history; new code carries a resolved AgentSpec instead. */
|
|
@@ -36,6 +38,46 @@ export const PROVIDER_BINARY = "claude";
|
|
|
36
38
|
/** "Within seconds": how soon a silent failed run must end for its stderr to
|
|
37
39
|
* be read for the sign-in signal. */
|
|
38
40
|
const AUTH_EARLY_EXIT_MS = 30_000;
|
|
41
|
+
// Fixed names only: neither setting values nor arbitrary JSON keys belong in
|
|
42
|
+
// diagnostics. The CLI's existing login stays where it is; no config is copied.
|
|
43
|
+
const CLAUDE_AUTH_ENV = [
|
|
44
|
+
"ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN",
|
|
45
|
+
"CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_SKIP_BEDROCK_AUTH", "ANTHROPIC_BEDROCK_BASE_URL",
|
|
46
|
+
"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_PROFILE",
|
|
47
|
+
"AWS_REGION", "AWS_DEFAULT_REGION", "AWS_BEARER_TOKEN_BEDROCK", "ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION",
|
|
48
|
+
"CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_SKIP_VERTEX_AUTH", "ANTHROPIC_VERTEX_BASE_URL",
|
|
49
|
+
"ANTHROPIC_VERTEX_PROJECT_ID", "CLOUD_ML_REGION", "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT",
|
|
50
|
+
];
|
|
51
|
+
/** A value-only preflight: a settings-dependent login cannot survive empty setting sources. */
|
|
52
|
+
function claudeSettingsAuthProblem(configDir) {
|
|
53
|
+
let text;
|
|
54
|
+
try {
|
|
55
|
+
text = readFileSync(join(configDir, "settings.json"), "utf8");
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code === "ENOENT")
|
|
59
|
+
return null;
|
|
60
|
+
return "Can't read your Claude settings.json to check sign-in. Restore access to that file and try again. Isolated builds do not load it.";
|
|
61
|
+
}
|
|
62
|
+
let settings;
|
|
63
|
+
try {
|
|
64
|
+
settings = JSON.parse(text);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return "Your Claude settings.json is not valid JSON. Fix that file and try again. Isolated builds do not load it, so Toolroll must check whether sign-in depends on it first.";
|
|
68
|
+
}
|
|
69
|
+
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) {
|
|
70
|
+
return "Your Claude settings.json must contain an object. Fix that file and try again. Isolated builds do not load it.";
|
|
71
|
+
}
|
|
72
|
+
const value = settings;
|
|
73
|
+
const keys = ["apiKeyHelper", "forceLoginMethod"].filter(key => Object.hasOwn(value, key));
|
|
74
|
+
const env = value["env"];
|
|
75
|
+
if (env !== null && typeof env === "object" && !Array.isArray(env)) {
|
|
76
|
+
keys.push(...CLAUDE_AUTH_ENV.filter(key => Object.hasOwn(env, key)).map(key => `env.${key}`));
|
|
77
|
+
}
|
|
78
|
+
return keys.length === 0 ? null
|
|
79
|
+
: `Your Claude settings.json defines sign-in settings: ${keys.join(", ")}. Isolated builds do not load that file. Use a Claude login that works without these settings, or select another build provider in Settings. Toolroll has not changed your login or copied these values.`;
|
|
80
|
+
}
|
|
39
81
|
/**
|
|
40
82
|
* A provider can run the repository's own CLI. In a self-hosting build that
|
|
41
83
|
* must never mean "open the database that launched me": a migration from the
|
|
@@ -104,9 +146,15 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
104
146
|
throw new Error(`run ${runId} already records session ${recordedSessionId} but this invocation neither resumes nor starts under it`);
|
|
105
147
|
}
|
|
106
148
|
const adapter = adapterFor(spec.provider);
|
|
107
|
-
const { startSessionId: _untrustedStartSessionId, ...invocationWithoutStart } = invocation;
|
|
149
|
+
const { startSessionId: _untrustedStartSessionId, streamInput: _untrustedStreamInput, ...invocationWithoutStart } = invocation;
|
|
150
|
+
// Live input (live-input.ts): only Claude's builds, only through a transport that keeps stdin open. The brief is
|
|
151
|
+
// the session's first stdin message and never rides the argv; every other road keeps its prompt where it was.
|
|
152
|
+
const liveInput = spec.provider === "claude" && invocation.phase === "build" && options.liveInput !== undefined && streamsInput(options.runner ?? adapter.defaultRunner)
|
|
153
|
+
? options.liveInput
|
|
154
|
+
: null;
|
|
108
155
|
const resolvedInvocation = {
|
|
109
156
|
...invocationWithoutStart,
|
|
157
|
+
...(liveInput === null ? {} : { streamInput: true }),
|
|
110
158
|
model: spec.model,
|
|
111
159
|
resumeSession: requestedResumeSessionId,
|
|
112
160
|
...(startSessionId === undefined ? {} : { startSessionId }),
|
|
@@ -142,7 +190,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
142
190
|
// re-supply survives resolveChildEnv's chat-key strip); in
|
|
143
191
|
// "subscription" mode the key is STRIPPED from the child so the CLI's
|
|
144
192
|
// own login is used — its key is retained, just not handed over.
|
|
145
|
-
const { runner, clock: _clock, versionProbe: _probe, keyHome, timeoutMs: _hardTimeout, idleTimeoutMs: _idleTimeout, stdin: _callerStdin, accumulateUsage: _accumulateUsage, ...runOptions } = options;
|
|
193
|
+
const { runner, clock: _clock, versionProbe: _probe, keyHome, timeoutMs: _hardTimeout, idleTimeoutMs: _idleTimeout, stdin: _callerStdin, accumulateUsage: _accumulateUsage, liveInput: _liveInput, ...runOptions } = options;
|
|
146
194
|
// The live setting is read STRICTLY (atomic authority closure): a
|
|
147
195
|
// present mode file that says neither word is a value-shaped refusal
|
|
148
196
|
// before any stamp or process — never the subscription default a
|
|
@@ -158,6 +206,11 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
158
206
|
};
|
|
159
207
|
}
|
|
160
208
|
const authMode = strict.mode;
|
|
209
|
+
if (spec.provider === "claude" && invocation.phase !== "review") {
|
|
210
|
+
const problem = claudeSettingsAuthProblem(options.env?.["CLAUDE_CONFIG_DIR"] ?? process.env["CLAUDE_CONFIG_DIR"] ?? join(keyHome ?? homedir(), ".claude"));
|
|
211
|
+
if (problem !== null)
|
|
212
|
+
return { kind: "refused", reason: "auth-mode", providerVersion: attested?.version ?? null, diagnostic: problem };
|
|
213
|
+
}
|
|
161
214
|
const ownKeyEnv = OWN_KEY_ENV[spec.provider];
|
|
162
215
|
const managedKey = authMode === "api-key"
|
|
163
216
|
? readProviderKey(spec.provider, keyHome) ?? (process.env[PROVIDER_KEY_ENV[spec.provider]] || null)
|
|
@@ -239,6 +292,8 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
239
292
|
store.stampProviderStart(runId, clock(), attested.version);
|
|
240
293
|
else
|
|
241
294
|
store.stampProviderStart(runId, clock());
|
|
295
|
+
// The brief is the live session's first message, queued before the spawn writes it.
|
|
296
|
+
liveInput?.begin(invocation.brief);
|
|
242
297
|
const spawn = witnessedRunner(store, runId, clock, runner ?? adapter.defaultRunner);
|
|
243
298
|
let transportAnnouncedSessionId = null;
|
|
244
299
|
let transportSessionConflict = false;
|
|
@@ -255,10 +310,16 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
255
310
|
// The agent fence: Toolroll's own secrets, database and other runs'
|
|
256
311
|
// evidence stay out of reach, whatever the permission mode. A review is
|
|
257
312
|
// already confined to its sealed files.
|
|
258
|
-
|
|
313
|
+
// A build's fence also holds its task boundary: home configuration and
|
|
314
|
+
// the repositories beside its own, fresh and resumed alike.
|
|
315
|
+
const fenceOf = invocation.phase === "review" ? { fence: [], fileTools: [] } : runFence(store, runId, spec.provider, options.keyHome);
|
|
316
|
+
const baseFence = fenceOf.fence;
|
|
259
317
|
const toolsDir = (spec.provider === "codex" || spec.provider === "openrouter") && baseFence.length > 0 ? tools?.privateDir : undefined;
|
|
260
318
|
const fence = toolsDir === undefined ? baseFence : [...baseFence, realpathSync(toolsDir)];
|
|
261
|
-
|
|
319
|
+
// Where the build runs: Claude reads the repository's CLAUDE.md from here.
|
|
320
|
+
const workspace = invocation.phase === "review" ? null : runOptions.cwd ?? run.worktree ?? null;
|
|
321
|
+
const buildInvocation = { ...resolvedInvocation, ...(tools === null ? {} : { toolArgv: tools.argv, fence, fileToolFence: fenceOf.fileTools }), ...(workspace === null ? {} : { workspace }) };
|
|
322
|
+
const launchArgv = tools === null ? argv : adapter.argv(buildInvocation);
|
|
262
323
|
const fenced = fenceLaunch(spec.provider, fence);
|
|
263
324
|
if (invocation.phase !== "review")
|
|
264
325
|
store.recordRunFence(runId, { method: fenced.method, paths: fence.length }, clock());
|
|
@@ -302,11 +363,13 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
302
363
|
throw new Error("the attempt was stopped before spawn custody completed");
|
|
303
364
|
},
|
|
304
365
|
...(stdin === undefined ? {} : { stdin }),
|
|
366
|
+
...(liveInput === null ? {} : { liveInput }),
|
|
305
367
|
...(hardTimeoutMs === undefined ? {} : { timeoutMs: hardTimeoutMs }),
|
|
306
368
|
...(idleTimeoutMs === undefined ? {} : { idleTimeoutMs }),
|
|
307
369
|
...(defaultTimeoutMs === undefined ? {} : { timeoutMs: defaultTimeoutMs }),
|
|
308
370
|
env: {
|
|
309
371
|
...(runOptions.env ?? {}),
|
|
372
|
+
...(adapter.env?.(buildInvocation) ?? {}),
|
|
310
373
|
...(tools?.env ?? {}),
|
|
311
374
|
...(managedKey === null ? {} : { [PROVIDER_KEY_ENV[spec.provider]]: managedKey }),
|
|
312
375
|
// This key is deliberately last: no caller may point an agent back at
|
|
@@ -360,6 +423,12 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
360
423
|
}
|
|
361
424
|
store.fixRunBilling(runId, authMode === "api-key" ? "api-key" : evidence ?? billingOf(spec.provider, store.handle), new Date());
|
|
362
425
|
const envelope = adapter.parse(result.stdout);
|
|
426
|
+
// An installed CLI too old for a build's own setup refuses its flags
|
|
427
|
+
// before it starts: nothing ran, and the run says what to update.
|
|
428
|
+
const unsupported = result.code !== 0 && envelope.initObserved !== true && invocation.phase !== "review" ? isolationUnsupported(spec.provider, result.stderr) : null;
|
|
429
|
+
if (unsupported !== null) {
|
|
430
|
+
return { kind: "refused", reason: "provider-unattested", providerVersion: attested === null ? null : attested.version, diagnostic: unsupported };
|
|
431
|
+
}
|
|
363
432
|
// The callback is the crash-safe early stamp; the parsed envelope is the
|
|
364
433
|
// authoritative completed-stream account. Stamp it as a fallback for an
|
|
365
434
|
// alternate transport. On fresh runs COALESCE makes callback/parser
|
|
@@ -384,11 +453,19 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
384
453
|
? "the provider returned a session id different from the exact identity recorded for resume"
|
|
385
454
|
: "the provider session id was different from the identity durably recorded when the stream initialized"
|
|
386
455
|
: null;
|
|
456
|
+
// A run whose cost could not be read may still have spent up to its cap: it is charged the cap, never nothing, so
|
|
457
|
+
// the next run's remaining budget stays honest. With no cap it is unmeasured, as a Codex run is, and says so.
|
|
458
|
+
const unknownCost = envelope.costUnknown === true;
|
|
459
|
+
const cap = typeof invocation.maxBudgetUsd === "number" && Number.isFinite(invocation.maxBudgetUsd) && invocation.maxBudgetUsd > 0 ? invocation.maxBudgetUsd : null;
|
|
460
|
+
const costUsd = unknownCost ? cap : envelope.costUsd;
|
|
461
|
+
const usageJson = unknownCost
|
|
462
|
+
? JSON.stringify({ cost_note: cap === null ? "cost unknown, no dollar cap applied: unmeasured" : "cost unknown, charged at the cap", ...usageOf(envelope.usageRaw) })
|
|
463
|
+
: envelope.usageRaw;
|
|
387
464
|
store.recordUsage(runId, {
|
|
388
465
|
...(envelope.tokensIn === null ? {} : { tokensIn: envelope.tokensIn + (options.accumulateUsage ? run.tokensIn ?? 0 : 0) }),
|
|
389
466
|
...(envelope.tokensOut === null ? {} : { tokensOut: envelope.tokensOut + (options.accumulateUsage ? run.tokensOut ?? 0 : 0) }),
|
|
390
|
-
...(
|
|
391
|
-
...(
|
|
467
|
+
...(costUsd === null ? {} : { costUsd: costUsd + (options.accumulateUsage ? run.costUsd ?? 0 : 0) }),
|
|
468
|
+
...(usageJson === null ? {} : { usageJson }),
|
|
392
469
|
});
|
|
393
470
|
// How the attempt ended, classified HERE where the structural terminal
|
|
394
471
|
// still exists; a sign-in that no longer works pauses its provider.
|
|
@@ -501,7 +578,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
501
578
|
usage: {
|
|
502
579
|
tokensIn: envelope.tokensIn,
|
|
503
580
|
tokensOut: envelope.tokensOut,
|
|
504
|
-
costUsd
|
|
581
|
+
costUsd,
|
|
505
582
|
},
|
|
506
583
|
// Gated on the run having NOTHING to show (Codex M5-M8 audit, C-8),
|
|
507
584
|
// where "nothing to show" is STRUCTURAL when the transport can say so
|
|
@@ -520,17 +597,32 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
520
597
|
},
|
|
521
598
|
};
|
|
522
599
|
}
|
|
523
|
-
/** The
|
|
524
|
-
function
|
|
600
|
+
/** The reported usage, when its kept text is whole JSON, to sit beside a cost note. */
|
|
601
|
+
function usageOf(raw) {
|
|
602
|
+
if (raw === null)
|
|
603
|
+
return {};
|
|
604
|
+
try {
|
|
605
|
+
return { usage: JSON.parse(raw) };
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return {};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/** The paths this run's agent may not reach: the state folder around the live database (but not its own worktree),
|
|
612
|
+
* ~/.toolroll (or its older name), and its task boundary; plus what only Claude's file tools refuse. */
|
|
613
|
+
function runFence(store, runId, provider, keyHome) {
|
|
525
614
|
try {
|
|
526
615
|
// A plane always has a database file; an in-memory store (a test) has nothing to fence.
|
|
527
616
|
const databaseFile = store.databaseFile();
|
|
528
617
|
if (databaseFile === null)
|
|
529
|
-
return [];
|
|
530
|
-
|
|
618
|
+
return { fence: [], fileTools: [] };
|
|
619
|
+
const worktree = store.getRun(runId)?.worktree ?? null;
|
|
620
|
+
const home = keyHome === undefined ? {} : { home: keyHome };
|
|
621
|
+
const boundary = worktree === null ? null : taskBoundary({ worktree, provider, ...home });
|
|
622
|
+
return { fence: agentFence({ databaseFile, worktree, ...home, boundary }), fileTools: boundary?.fileTools ?? [] };
|
|
531
623
|
}
|
|
532
624
|
catch {
|
|
533
|
-
return [];
|
|
625
|
+
return { fence: [], fileTools: [] };
|
|
534
626
|
}
|
|
535
627
|
}
|
|
536
628
|
/** How the fence reaches a provider: Codex through its own sandbox profile
|
|
@@ -551,10 +643,10 @@ function fenceLaunch(provider, fence) {
|
|
|
551
643
|
/** One launch's tools, or — when they cannot be prepared — the same isolation with none, never the operator's global servers. */
|
|
552
644
|
function runTools(store, runId, spec, keyHome, clock, readOnly, withheld) {
|
|
553
645
|
try {
|
|
554
|
-
return prepareRunTools(store, runId, spec.provider, { ...(keyHome === undefined ? {} : { home: keyHome }), now: clock(),
|
|
646
|
+
return prepareRunTools(store, runId, spec.provider, { ...(keyHome === undefined ? {} : { home: keyHome }), now: clock(), ...(readOnly === undefined ? {} : { readOnly }), ...(withheld === undefined ? {} : { withheld }) });
|
|
555
647
|
}
|
|
556
648
|
catch {
|
|
557
|
-
return noToolsArgs(spec.provider
|
|
649
|
+
return noToolsArgs(spec.provider);
|
|
558
650
|
}
|
|
559
651
|
}
|
|
560
652
|
/** How Codex says it's signed in (`codex login status`): a ChatGPT sign-in is its plan; a key or Bedrock is a key;
|