feat(discord): writes on write-marked roots, web fetch and search, held prompts (#1509)

Row 23. write_file and edit_file for roots marked write: true under the
same fence as reads; web_fetch (https only, public addresses, pinned
connection, capped body) and web_search through SearXNG; extension
renamed to tools.mjs. Engine holds a prompt while pi is busy and sends
it as its own run, so a second message mid-turn no longer folds into
the first (live defect). fake-pi models the real follow-up folding.

Suite 52/52, node tests 129. rev-code-02 APPROVED round 3, comment
26362, tree dbd2ce9a. Records: QUEUE rows 23-24, CURRENT, BUILD-LOG
phase, SESSIONS, row 24 brief (git verbs, D5-D7 ruled).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-18 07:27:50 -05:00
co-authored by Claude Fable 5.1
parent 1ac812d3d5
commit 1685deb423
24 changed files with 1519 additions and 113 deletions
+5 -2
View File
@@ -204,7 +204,7 @@ test("binding: tools is optional, validated strictly, a fixed key for reload, an
const docs = join(root, "docs");
mkdirSync(docs);
const ok = validateBinding(rawBinding({ tools: { roots: [{ name: "docs", path: docs }] } }));
assert.deepEqual(ok.tools, { roots: [{ name: "docs", path: docs }], maxFileBytes: 262144, maxCallsPerTurn: 8 });
assert.deepEqual(ok.tools, { roots: [{ name: "docs", path: docs, write: false }], maxFileBytes: 262144, maxCallsPerTurn: 8, web: null });
assert.ok(FIXED_KEYS.includes("tools"));
const bad = [
[{ tools: [] }, /must be an object/],
@@ -217,6 +217,7 @@ test("binding: tools is optional, validated strictly, a fixed key for reload, an
[{ tools: { roots: [{ name: "docs", path: docs }], maxCallsPerTurn: 65 } }, /maxCallsPerTurn/],
[{ tools: { roots: [{ name: "docs", path: docs }], extra: true } }, /unknown key/],
[{ tools: { roots: [{ name: "docs", path: docs, mode: "rw" }] } }, /unknown key/],
[{ tools: { roots: [{ name: "docs", path: docs, write: "yes" }] } }, /write must be true or false/],
];
for (const [o, re] of bad) assert.throws(() => validateBinding(rawBinding(o)), re, JSON.stringify(o));
assert.throws(() => reloadDiff(ok, validateBinding(rawBinding())), (e) => e instanceof DiscordError && e.exitCode === 2 && /tools cannot change/.test(e.message));
@@ -225,7 +226,9 @@ test("binding: tools is optional, validated strictly, a fixed key for reload, an
mkdirSync(join(dataRoot, "discord"), { recursive: true });
assert.equal(resolveToolRoots(validateBinding(rawBinding()), { dataRoot }), null);
const resolved = resolveToolRoots(ok, { dataRoot });
assert.deepEqual(resolved, { roots: [{ name: "docs", path: docs }], maxFileBytes: 262144, maxCallsPerTurn: 8 });
assert.deepEqual(resolved, { roots: [{ name: "docs", path: docs, write: false }], maxFileBytes: 262144, maxCallsPerTurn: 8 });
const rw = validateBinding(rawBinding({ tools: { roots: [{ name: "docs", path: docs, write: true }] } }));
assert.deepEqual(resolveToolRoots(rw, { dataRoot }).roots, [{ name: "docs", path: docs, write: true }], "write: true reaches the extension's config");
const inData = validateBinding(rawBinding({ tools: { roots: [{ name: "d", path: join(dataRoot, "discord") }] } }));
assert.throws(() => resolveToolRoots(inData, { dataRoot }), /overlaps the data root/);
const above = validateBinding(rawBinding({ tools: { roots: [{ name: "r", path: root }] } }));