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
+5 -2
View File
@@ -94,7 +94,9 @@ export function message(overrides = {}) {
}
// Fake REST: scripted outcomes for createMessage, records every call.
export function fakeRest({ outcomes = [] } = {}) {
// With snowflakes, message ids are 18-digit like Discord's, for code that
// validates them (the approvals ledger keys).
export function fakeRest({ outcomes = [], snowflakes = false } = {}) {
const calls = [];
let n = 0;
return {
@@ -113,7 +115,8 @@ export function fakeRest({ outcomes = [] } = {}) {
async createMessage(channelId, body) {
calls.push({ channelId, ...body });
const o = outcomes.length > 0 ? outcomes.shift() : { ok: true };
if (o.ok) return { messageId: o.messageId || `m${++n}`, status: 200 };
n += 1;
if (o.ok) return { messageId: o.messageId || (snowflakes ? `5000000000000000${String(n).padStart(2, "0")}` : `m${n}`), status: 200 };
throw new RestOutcome(o.kind, o.message || `fake ${o.kind}`);
},
async typing(channelId) {