feat(discord): binding reload without a restart, and a per-user channel allowlist (#1509)

`reload` validates the binding file and sends SIGHUP to the live owner;
the running connector re-reads it and swaps guildName, channels, users
and limits in place. name, seat, guildId, botUserId, tokenFile, engine
and context are fixed for the life of the process; a change there, an
invalid file or a channel outside the guild refuses the reload and keeps
the old binding. Every attempt is one line in reloads.jsonl. The service
unit maps `systemctl --user reload` to the same signal.

A user entry may carry `channels`, an allowlist of listed channel ids;
absent means every listed channel. Outside the list the message is
dropped as channel-not-for-user; threads count as their parent.

Suite 41/41, 101 node tests. QUEUE rows 19 and 20 opened.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-13 18:59:31 -05:00
co-authored by Claude Fable 5.1
parent d9745a4510
commit caaef941e6
16 changed files with 326 additions and 21 deletions
@@ -436,6 +436,31 @@ Recorded here because they refine a ruling or fill a gap the rulings left.
still needs stop and start). Next: the control board row.
- Live check 2026-09-13 19:35 to 19:40 UTC: Sage seat migrated to the unit; SIGKILL recovered in 16 s with the dead lock cleared; `discord.sh stop` held (start exits 3, no restart); released and READY. Iteration 2 closed.
- Operator check 2026-09-13 21:29 UTC: Jason ran the four steps (traffic under the unit, SIGKILL recovery, brake holds with exit 3 and no restart, release to READY) and reported all verified. Receipt `mvp2-operator-verify-*.json` in the private evidence dir. Unit active afterwards with a fresh main PID and zero auto-restarts, as expected after a brake and release.
- Binding reload (iteration 4, Jason: "proceed in order", 2026-09-13).
Today the binding is read once at start, so a ceiling or channel change
needs brake, STOP removal, reset and start. New: `scripts/discord.sh
reload <binding>` validates the file, then sends SIGHUP to the live
owner in `run.lock`; the unit gets `ExecReload` so `systemctl --user
reload` does the same. The running process re-reads the file and applies
`channels`, `users`, `limits` and `guildName` in place; a new channel is
read over REST first and must sit in the bound guild. `name`, `seat`,
`guildId`, `botUserId`, `tokenFile`, `engine` and `context` are fixed for
the life of the process (the engine and its prompt are launched once);
a change there, an invalid file or a failed channel lookup refuses the
reload, keeps the old binding and journals the refusal in
`reloads.jsonl`. Turns in flight finish under the limits they started
with. Nothing is sent to Discord on a reload.
- Per-user channels and Carmen (iteration 5, same ruling). Jason gave
Carmen's id and the rule "all rooms except #sage-admin". A user entry
may carry `channels: [ids]`, an allowlist of listed channel ids; absent
means every listed channel. A listed user posting in a channel outside
their list is dropped with `channel-not-for-user`, silent like every
drop; threads use the parent. Choice recorded: allowlist over a deny
list, because the connector's policy is explicit lists (Q3) and a new
channel must not widen anyone's reach by default. Cost: when a channel
is added later, Carmen's list needs the id too. Enrollment is a binding
edit plus `reload`, which doubles as the live check for iteration 4.
Carmen's id lives only in the binding file, never in the repository.
- Control board row (iteration 3, briefed, not started). Blocked on
ownership, not on design: `packages/control-board` is darkwing's (#1503,
#1505, brief `2026-09-12_control-board-mvp.md`), and the row cannot be
+3
View File
@@ -43,6 +43,8 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
| 15 | Discord connector: eyes reaction on every admitted message as a read receipt (MVP iteration 1) | coordinator | #1509 | done: committed 93d6b624, live check passed 19:21 UTC (turn record receipt ok, Jason: test is successful), receipt `mvp1-read-receipt-20260913T192158Z.json` in the private evidence dir; `rest.react` best effort, reaction placed at admission before the engine runs, outcome in the turn record, no reaction on drops or refusals; `scripts/test-discord.sh` 28/28 (90 node tests) | Jason sees the reaction on a live message | `2026-09-13_discord-connector-pilot.md` section 11 |
| 17 | Discord connector: systemd user service with a supervised pre-start (`recover`, exit 3 never retried) (MVP iteration 2) | coordinator | #1509 | done, operator-verified by Jason 2026-09-13 (all four steps): `scripts/test-discord.sh` 40/40 (95 node tests); Sage seat migrated 19:35 UTC, SIGKILL recovered in 16 s with the dead lock cleared, brake held (exit 3, no restart), released and READY; receipt `mvp2-service-unit-*.json` in the private evidence dir. First cut (ExecStartPre) looped and was replaced by `run --supervised` before any traffic | the Sage connector runs under `mosaic-discord@shared-signals`, survives a kill with a clean restart, and stays down behind `discord.sh stop` | `2026-09-13_discord-connector-pilot.md` section 11 |
| 18 | Control board row for the Discord connector (MVP iteration 3): discovery from binding files, liveness from run.lock, reply refused | darkwing (Jason's ruling 2026-09-13); coordinator answers connector-side questions | #1509 | briefed; assigned to darkwing, not started | a Sage (discord) row on the board shows live, offline and braked correctly, and reply from the board is refused | `2026-09-13_discord-connector-pilot.md` section 11 |
| 19 | Discord connector: binding reload without a restart (`reload` verb, SIGHUP, `systemctl --user reload`); channels, users, limits and guildName apply in place, identity, engine and context stay fixed, an invalid file is refused and the old binding kept (MVP iteration 4) | coordinator | #1509 | in progress | edit the binding, run `scripts/discord.sh reload shared-signals`, the change applies with no restart, a broken edit is refused and journaled | `2026-09-13_discord-connector-pilot.md` section 11 |
| 20 | Discord connector: per-user channel allowlist in the binding and Carmen enrolled (all listed rooms except #sage-admin) (MVP iteration 5) | coordinator | #1509 | briefed, after row 19 | Carmen gets a reply in #general and silence in #sage-admin; Jason unchanged | `2026-09-13_discord-connector-pilot.md` section 11 |
Start message for row 6, sent from the board to darkwing:
"Read docs/plans/QUEUE.md, then the plan page section "Piece 5: darkwing on
@@ -88,3 +90,4 @@ Gate F or when blocked."
- 2026-09-13 — coordinator: rows 1718 pushed (dc5902aa..90cb31f5 to origin/refactor) under the jarvis git identity on Jason's authorization. No other row changed.
- 2026-09-13 — coordinator: row 18 assigned to darkwing by Jason ("darkwing should build the board row"). Coordinator no longer holds a Discord row in progress. No other row changed.
- 2026-09-13 — coordinator: row 17 operator check passed (Jason: all tests successfully verified, 21:29 UTC). No other row changed.
- 2026-09-13 — coordinator: rows 19 (binding reload) and 20 (per-user channels, Carmen) added on Jason's "proceed in order"; row 19 in progress. No other row changed.