From 34777c56bc77644c594d021fa5f92af2e485a15e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 26 Sep 2026 15:58:59 -0500 Subject: [PATCH] docs(records): row 25 SetSpark fix, Discord restart receipt, Gate F rulings Co-Authored-By: Claude Opus 5.5 --- BUILD-LOG.md | 28 +++++++++ agents/sage/work/row25-setspark-fix.diff | 78 ++++++++++++++++++++++++ docs/SESSIONS.md | 1 + docs/plans/2026-09-26_lead-decisions.md | 25 ++++++++ 4 files changed, 132 insertions(+) create mode 100644 agents/sage/work/row25-setspark-fix.diff diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 781ee988..3f0b3fc3 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -3046,3 +3046,31 @@ a startup journal line with HEAD, the scoped dirty state and a runtime file digest. Also unchanged from HEAD: a run pi started that never ends holds prompts until each one times out. No connector restart and no push were done here. + +## 2026-09-26: row 25 SetSpark fix and the Discord restart (#1509, Sage, reviewed by Rocko) + +Before: row 25 (43d7574d) was committed but could not go live. With the +`setspark` key added to the live binding, `discord.sh check` passed but listed +no SetSpark verbs. The cause: `resolveToolRoots` dropped `tools.setspark`, so +neither pi's `--tools` list nor the connector's SetSpark client ever saw it. No +test followed the binding to the extension. Passing the validated object +through would also have failed, because the extension refuses its +`maxResponseBytes` as an unknown key. + +After: 6c06a6f3. The resolved config carries only the four keys the extension +accepts, and the extension restores the response cap. The connector's client +uses the validated binding object, which keeps the cap. The README now marks +`principal` as required. The new test failed on HEAD and passes with the fix. +The eight suites passed on an index export (24/90/43/17/14/15/63/18). Rocko +approved the diff (4dec1898…) in +`agents/rocko/work/row25-setspark-fix-review-2026-09-26.md` (a28df89e…). + +Restart: the one restart Jason approved at 20:20Z, done at 20:58:03Z from +6c06a6f3 with `packages/discord` and `agents/sage` clean. Old PID 890894 +(running since 09-18) stopped cleanly. New PID 499064, pi child 499126, +gateway READY at 20:58:04Z, session continued, 0 restarts. The pi child's +tools config carries `setspark` (https://api.setspark.io as sage), and the +extension enables the eight record and approval verbs from it. Receipt +(0600): `restart-row25-20260926T205803Z.txt` in the Sage evidence directory. +Jason's live check (one work item, one proposal approved by button) is still +to do. diff --git a/agents/sage/work/row25-setspark-fix.diff b/agents/sage/work/row25-setspark-fix.diff new file mode 100644 index 00000000..bc56dcc5 --- /dev/null +++ b/agents/sage/work/row25-setspark-fix.diff @@ -0,0 +1,78 @@ +diff --git a/packages/discord/README.md b/packages/discord/README.md +index d716e695..4703fa86 100644 +--- a/packages/discord/README.md ++++ b/packages/discord/README.md +@@ -139,7 +139,7 @@ is `src/binding.mjs`. + | `engine` | `provider`, `model`, `thinking` for pi | + | `limits` | `turnsPerDay` (200), `turnTimeoutSeconds` (180), `replyChunkChars` (1900), `inboundMaxChars` (4000) | + | `context.files[]` | files appended to pi's system prompt in order, repository-relative and inside the repository (no absolute paths, `..` or symlinks); the Discord block is added after them | +-| `tools` | optional. `roots[]` of `{name, path, write?, git?}`: absolute directories the seat may read through `list_dir`, `read_file` and `search`; a root with `"write": true` may also be written through `write_file` and `edit_file`; a writable root that is a git work tree may carry `git` `{branch, identity, tokenFile, author, protocol?}` and gains `git_status`, `git_commit`, `git_pull` and `git_push` (`protocol: "vault"` adds `reserve_id`); `maxFileBytes` (262144), `maxCallsPerTurn` (8); `web` (optional) `{searxng, maxFetchBytes}` enables `web_fetch` and `web_search` through the named SearXNG instance (https, or http on loopback; `maxFetchBytes` 1048576); `setspark` (optional) `{baseUrl, keyFile, principal?}` names the SetSpark record service (https origin, or http on loopback; key file absolute, 0600, read per call, never printed) and turns on connector-verified approvals. Absent means no tools and a pi launch with `--no-tools`. A root may not be `/`, the home directory, a symlink, a path with a dot-prefixed segment, or anything inside or above the data root | ++| `tools` | optional. `roots[]` of `{name, path, write?, git?}`: absolute directories the seat may read through `list_dir`, `read_file` and `search`; a root with `"write": true` may also be written through `write_file` and `edit_file`; a writable root that is a git work tree may carry `git` `{branch, identity, tokenFile, author, protocol?}` and gains `git_status`, `git_commit`, `git_pull` and `git_push` (`protocol: "vault"` adds `reserve_id`); `maxFileBytes` (262144), `maxCallsPerTurn` (8); `web` (optional) `{searxng, maxFetchBytes}` enables `web_fetch` and `web_search` through the named SearXNG instance (https, or http on loopback; `maxFetchBytes` 1048576); `setspark` (optional) `{baseUrl, keyFile, principal, timeoutMs?}` names the SetSpark record service (https origin, or http on loopback; key file absolute, 0600, read per call, never printed) and turns on connector-verified approvals. Absent means no tools and a pi launch with `--no-tools`. A root may not be `/`, the home directory, a symlink, a path with a dot-prefixed segment, or anything inside or above the data root | + + Unknown keys, missing fields, wrong types, empty allowlists, a user channel + that is not listed and a bot listed as a user all refuse with exit 2. A +diff --git a/packages/discord/src/binding.mjs b/packages/discord/src/binding.mjs +index 587452fd..c9349b81 100644 +--- a/packages/discord/src/binding.mjs ++++ b/packages/discord/src/binding.mjs +@@ -360,5 +360,8 @@ export function resolveToolRoots(binding, { dataRoot }) { + if (real === data || real.startsWith(data + sep) || data.startsWith(real + sep)) throw new DiscordError(`tool root ${r.name} overlaps the data root: ${r.path}`); + return { name: r.name, path: real, write: r.write, ...(r.git ? { git: { branch: r.git.branch, identity: r.git.identity, tokenFile: r.git.tokenFile, author: `${r.git.author.name} <${r.git.author.email}>`, ...(r.git.protocol ? { protocol: r.git.protocol } : {}) } } : {}) }; + }); +- return { roots, maxFileBytes: binding.tools.maxFileBytes, maxCallsPerTurn: binding.tools.maxCallsPerTurn, ...(binding.tools.web ? { web: { searxng: binding.tools.web.searxng, maxFetchBytes: binding.tools.web.maxFetchBytes } } : {}) }; ++ // setspark carries only the keys loadSetsparkConfig accepts; the extension ++ // re-validates it and adds the response cap itself. ++ const ss = binding.tools.setspark; ++ return { roots, maxFileBytes: binding.tools.maxFileBytes, maxCallsPerTurn: binding.tools.maxCallsPerTurn, ...(binding.tools.web ? { web: { searxng: binding.tools.web.searxng, maxFetchBytes: binding.tools.web.maxFetchBytes } } : {}), ...(ss ? { setspark: { baseUrl: ss.baseUrl, keyFile: ss.keyFile, principal: ss.principal, timeoutMs: ss.timeoutMs } } : {}) }; + } +diff --git a/packages/discord/src/cli.mjs b/packages/discord/src/cli.mjs +index 93d18805..045f4918 100755 +--- a/packages/discord/src/cli.mjs ++++ b/packages/discord/src/cli.mjs +@@ -214,7 +214,7 @@ async function run(opts) { + // The connector's own SetSpark client (bind and approvals) uses the same + // key as the model's verbs; without a setspark key it has none, and an + // approval request from the model is refused. +- const api = toolRoots && toolRoots.setspark ? createSetsparkApi(toolRoots.setspark) : null; ++ const api = binding.tools && binding.tools.setspark ? createSetsparkApi(binding.tools.setspark) : null; + const connector = createConnector({ binding, journalDir, rest, gateway, engine, api, log: warn }); + let shuttingDown = false; + let exitCode = 0; +diff --git a/packages/discord/tests/setspark.test.mjs b/packages/discord/tests/setspark.test.mjs +index fc613011..f88dfb23 100644 +--- a/packages/discord/tests/setspark.test.mjs ++++ b/packages/discord/tests/setspark.test.mjs +@@ -12,7 +12,7 @@ import { + loadSetsparkConfig, readKey, idempotencyKey, connectorKey, callApi, renderRefusal, createSetsparkApi, renderRecord, + } from "../src/setspark.mjs"; + import { loadToolsConfig, createToolSet, enabledToolNames } from "../src/tools.mjs"; +-import { validateBinding } from "../src/binding.mjs"; ++import { validateBinding, resolveToolRoots } from "../src/binding.mjs"; + import { makeRoot, rawBinding } from "./helpers.mjs"; + + const KEY_A = "ssk_" + "a".repeat(40); +@@ -120,6 +120,24 @@ test("setspark config: reaches the tools config and the binding as a fixed key", + assert.throws(() => validateBinding(rawBinding({ tools: { roots: [{ name: "docs", path: docs }], setspark: { baseUrl: "https://api.setspark.io", keyFile: kf, principal: "sage", nope: 1 } } })), /unknown key/); + }); + ++test("setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension", () => { ++ const root = makeRoot(); ++ const docs = join(root, "docs"); ++ mkdirSync(docs); ++ const dataRoot = join(root, "data"); ++ mkdirSync(dataRoot); ++ const kf = keyFile(root); ++ const b = validateBinding(rawBinding({ tools: { roots: [{ name: "docs", path: docs }], setspark: { baseUrl: "https://api.setspark.io", keyFile: kf, principal: "sage", timeoutMs: 5000 } } })); ++ const resolved = resolveToolRoots(b, { dataRoot }); ++ assert.deepEqual(resolved.setspark, { baseUrl: "https://api.setspark.io", keyFile: kf, principal: "sage", timeoutMs: 5000 }); ++ for (const name of SETSPARK_TOOL_NAMES) assert.ok(enabledToolNames(resolved).includes(name), `${name} reaches pi's --tools list`); ++ const ext = loadToolsConfig(JSON.parse(JSON.stringify(resolved))); ++ assert.deepEqual(ext.setspark, b.tools.setspark, "the extension rebuilds the same config, response cap included"); ++ const plain = resolveToolRoots(validateBinding(rawBinding({ tools: { roots: [{ name: "docs", path: docs }] } })), { dataRoot }); ++ assert.equal("setspark" in plain, false); ++ assert.ok(!enabledToolNames(plain).some((n) => SETSPARK_TOOL_NAMES.includes(n))); ++}); ++ + test("setspark keys: read per call, one printable token per file, rotation without a restart", async () => { + const root = makeRoot(); + const c = config(root); diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 7214186f..61f77ae9 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -390,3 +390,4 @@ are never rewritten or removed; corrections are new entries. - 2026-09-26T20:35:39Z · Dewey (T3, claude-opus-5-5) · #1507 board Host/Origin guard before CHAT-02: serve.mjs foreignRequest + 2 failing-first tests, 7 scratch variants, suites 149/149 · candidate ready for Rocko's review via Sage; uncommitted 2026-09-26T20:35:44Z | Dewey (T3 Claude Code, thread 84fb346c) | #1507 board Host/Origin guard before CHAT-02 | Correction: the entry above at 20:35:39Z used the wrong line format; this one restates it. packages/control-board serve.mjs foreignRequest (d0a9bbed) plus two failing-first tests in serve.test.mjs (e01d7bd9); HEAD fails at foreign Host, six mutations each fail, control-board/webui/seat suites 149/149. Packet agents/dewey/work/board-guard/NOTES.md for Rocko via Sage. No commit or push. 2026-09-26T20:38:14Z | Sage (T3 Claude Code, thread 1ef1e4f8) | Integration: board Host/Origin guard | Rocko approved de9ff942 (review 5a12f08e); Sage committed the guard with records after suites on an index export, pushed, and restarted the control board. Discord restart still held on 6b R2. +2026-09-26T20:58:56Z | Sage (T3 Claude Code, thread 1ef1e4f8) | #1509 row 25 SetSpark live, Gate F routing | Found resolveToolRoots dropping setspark; fixed with failing-first test, Rocko approved, committed 6c06a6f3 and pushed; restarted mosaic-discord@shared-signals (890894 -> 499064), pi has the eight SetSpark verbs. Ruled Darkwing's three Gate F questions; Filbert reviews the brief. Jason live check pending. diff --git a/docs/plans/2026-09-26_lead-decisions.md b/docs/plans/2026-09-26_lead-decisions.md index e5a068f1..8724db14 100644 --- a/docs/plans/2026-09-26_lead-decisions.md +++ b/docs/plans/2026-09-26_lead-decisions.md @@ -141,3 +141,28 @@ which stay with him. Each item names who decided it and what happened. 44 sessions before and after. Live checks: a foreign Host on `/api/board` returns 403, a foreign Origin on `POST /api/reply` returns 403, and the WebUI proxy's `/api/board` returns 200. +11. **Row 25 was never live.** Before 20:56Z `discord.sh check` passed with the + new `setspark` key, but it listed no SetSpark verbs. `resolveToolRoots` + dropped `tools.setspark`, so pi never got the record verbs and the + connector never built its SetSpark client. Passing the validated object + through as-is would also fail, because the extension refuses its + `maxResponseBytes` as an unknown key. Sage wrote the fix with a test that + fails first (binding to extension round trip). The connector's client now + uses the validated object, which keeps the response cap. The README now + marks `principal` as required. The eight suites passed on an index export, + and the check lists the eight verbs. Rocko reviews the staged diff + (`agents/sage/work/row25-setspark-fix.diff`, 4dec1898…). The restart + waits on that verdict and the commit. +12. **Gate F brief (Darkwing, 08959a05…).** Sage ruled on the three questions + Darkwing had marked for Jason: + - Gate F is on by default. A missing DB exits 1 and names `--no-t3`. + - The `t3:unmapped` row stays. + - The 14 old Discord Bot headers stay as recorded and appear only in the + JSON diagnostic. + + The 6a uppercase-class fix rides in Gate F. Filbert reviews the brief, + and no code starts before the verdict. +13. **Discord restarted with row 25 live.** Rocko approved the fix, which is + committed and pushed as 6c06a6f3. The restart ran at 20:58:03Z: PID + 890894 was replaced by 499064, the gateway was READY at 20:58:04Z, and pi + has the SetSpark verbs. Jason's live check comes next.