docs(records): row 25 SetSpark fix, Discord restart receipt, Gate F rulings

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-26 15:58:59 -05:00
co-authored by Claude Opus 5.5
parent 6c06a6f358
commit 34777c56bc
4 changed files with 132 additions and 0 deletions
+28
View File
@@ -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 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 prompts until each one times out. No connector restart and no push were done
here. 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.
+78
View File
@@ -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);
+1
View File
@@ -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: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: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: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.
+25
View File
@@ -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 44 sessions before and after. Live checks: a foreign Host on
`/api/board` returns 403, a foreign Origin on `POST /api/reply` returns `/api/board` returns 403, a foreign Origin on `POST /api/reply` returns
403, and the WebUI proxy's `/api/board` returns 200. 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.