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
+12
View File
@@ -55,6 +55,18 @@ for (const [name, msg, expected] of table) {
});
}
test("authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected", () => {
const guest = { id: IDS.stranger, name: "guest", channels: [IDS.general] };
const scoped = binding({ users: [{ id: IDS.owner, name: "owner" }, guest] });
const asGuest = (o) => authorize(scoped, message({ author: { id: IDS.stranger }, ...o }), info);
assert.equal(asGuest({ channel_id: IDS.admin }).reason, DROP.USER_CHANNEL);
assert.equal(asGuest({ channel_id: IDS.threadOfAdmin }).reason, DROP.USER_CHANNEL);
assert.equal(asGuest({ channel_id: IDS.general, mentions: botMention }).ok, true);
assert.equal(asGuest({ channel_id: IDS.general }).reason, DROP.MENTION);
assert.equal(asGuest({ channel_id: "100000000000000030", mentions: botMention }).channel.id, IDS.general);
assert.equal(authorize(scoped, message({ channel_id: IDS.admin }), info).ok, true);
});
test("authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers)", () => {
let looked = 0;
const spy = (id) => {