feat(discord): SetSpark record client for the Discord Sage, fixed verbs against setspark-api, connector-verified approvals (#1509)

Row 25, parts 2a and 2b, against the shared-signals contract a5425a2.

Model side: eight fixed verbs in the pi extension (record_list, record_get,
record_create, record_update, resolve_id, open_approval_request,
get_approval_request, create_document), each one HTTP call with arguments
checked before any request. Writes carry an idempotency key
<principal>:<message id>:<call index> and an audit context. The seat key is
read from a 0600 file on every call and never cached, printed or journaled.

Connector side: append-only approval ledger, Approve button and exact
"approve" reply resolved by the connector against the required approvers,
confirmation message posted as button evidence, bind and add_approval through
the service under connector keys, retry of unknown entries on start.

Evidence: node tests 162 pass, scripts/test-discord.sh 63/63. Review by
rev-code-02, round 1 approved (#1509 comment 26467, tree 7872d8c5).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-22 12:59:39 -05:00
co-authored by Claude Fable 5.1
parent 1949ed8d31
commit 43d7574d6a
24 changed files with 2178 additions and 32 deletions
+11 -1
View File
@@ -20,6 +20,7 @@
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
import { loadWebConfig } from "./web.mjs";
import { loadSetsparkConfig } from "./setspark.mjs";
import { isAbsolute, join, resolve, sep } from "node:path";
import { homedir } from "node:os";
import { DiscordError } from "./errors.mjs";
@@ -45,7 +46,7 @@ const USER_KEYS = ["id", "name", "channels"];
const ENGINE_KEYS = ["provider", "model", "thinking"];
const LIMIT_KEYS = Object.keys(LIMIT_DEFAULTS);
const CONTEXT_KEYS = ["files"];
const TOOLS_KEYS = ["roots", "maxFileBytes", "maxCallsPerTurn", "web"];
const TOOLS_KEYS = ["roots", "maxFileBytes", "maxCallsPerTurn", "web", "setspark"];
const ROOT_KEYS = ["name", "path", "write", "git"];
const ROOT_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
@@ -216,6 +217,7 @@ export function validateBinding(raw, where = "binding") {
maxFileBytes: requireInteger(mergedTools, "maxFileBytes", `${where}.tools`, { min: 1024, max: 4 * 1024 * 1024 }),
maxCallsPerTurn: requireInteger(mergedTools, "maxCallsPerTurn", `${where}.tools`, { min: 1, max: 64 }),
web: raw.tools.web === undefined ? null : webConfig(raw.tools.web, `${where}.tools.web`),
setspark: raw.tools.setspark === undefined ? null : setsparkConfig(raw.tools.setspark, `${where}.tools.setspark`),
});
}
@@ -326,6 +328,14 @@ export function resolveContextFiles(binding, repo) {
// Tool roots must exist as real directories on this host, not symlinks, and
// must not sit inside the data root (bindings, tokens, journals) or contain
// it. Returns the resolved config the engine hands the extension.
function setsparkConfig(raw, where) {
try {
return loadSetsparkConfig(raw, where);
} catch (err) {
throw new DiscordError(err.message);
}
}
function webConfig(raw, where) {
try {
return loadWebConfig(raw, where);