feat(discord): read-only tools for the Discord Sage through a Mosaic pi extension confined to declared roots (#1509)
A binding may declare `tools` with named roots. pi starts with --no-builtin-tools and the package's own extension, allowlisting list_dir, read_file and search. src/tools.mjs holds the rules: names not paths, per-segment lstat walk, one checked descriptor read that refuses symlinks, swaps, FIFOs, hard links and oversize files, credential shapes refusing the whole read, and a per-message call budget. The engine settles on agent_end and records tool calls in the turn record. Jason's rulings R1-R7 in the brief, section 7. rev-code-02 approved round 2 (comment 26276) on tree 43f0329b after four round 1 fixes. Suite 48/48, node tests 116. Not pushed. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -253,3 +253,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
- 2026-09-13 UTC — coordinator (Claude) — Discord read receipt live check (#1509, QUEUE row 15): reaction confirmed on a live message at 19:21 UTC, turn record receipt ok true, private evidence receipt written, row 15 done. Committed; push on Jason's say-so.
|
||||
- 2026-09-13 UTC — coordinator (Claude) — Discord service unit (#1509, QUEUE row 17): `run --supervised` and `recover`, exit 3 for brakes, `scripts/discord-service.sh`, systemd user unit; suite 40/40 (95 node tests); Sage seat migrated from tmux to `mosaic-discord@shared-signals`, kill and brake checks passed live. Committed; push on Jason's say-so.
|
||||
- 2026-09-13 UTC — coordinator (Claude) — Discord binding reload and per-user channels (#1509, QUEUE rows 19–20): `reload`/SIGHUP with fixed keys refused, `reloads.jsonl`, user `channels` allowlist; suite 41/41, 101 node tests; Carmen enrolled live by reload at 00:03 UTC, her first message pending; row 18 assigned to darkwing by Jason.
|
||||
- 2026-09-14 UTC — coordinator (Claude) — Discord read-only tools (#1509, QUEUE row 21): `tools` binding key, host pi extension `list_dir`/`read_file`/`search` confined to declared roots, engine settles on `agent_end`, tool calls in the turn record; rev-code-02 round 2 APPROVE (26276) after four round 1 findings were fixed; suite 48/48, 116 node tests; committed locally, not pushed; live check in #sage-admin next.
|
||||
|
||||
+6
-2
@@ -181,9 +181,13 @@ user entry may carry `channels`, an allowlist of listed channel ids.
|
||||
`scripts/discord-service.sh install` renders and writes the systemd user
|
||||
unit `[email protected]` (one instance per binding, restart on
|
||||
failure, exit 3 never retried, SIGTERM on `systemctl --user stop`, SIGHUP on
|
||||
`systemctl --user reload`). Records
|
||||
`systemctl --user reload`). An optional `tools` key in the binding declares
|
||||
read-only roots; pi then runs with its own tools off and the package's
|
||||
extension providing `list_dir`, `read_file` and `search` confined to those
|
||||
roots, with each call in the turn record. `tools` is a fixed key: changing
|
||||
it needs a stop and start. Records
|
||||
under `<dataRoot>/discord/<binding>/`: `inbox.jsonl`, `outbox.jsonl`,
|
||||
`drops.jsonl`, `admissions.jsonl`, `notices.jsonl`, write-once `turns/<id>.json`. Suite:
|
||||
`drops.jsonl`, `admissions.jsonl`, `notices.jsonl`, `reloads.jsonl`, write-once `turns/<id>.json`. Suite:
|
||||
`scripts/test-discord.sh`.
|
||||
Exit codes: 0 ok, 1 operation failed, 2 invalid data or configuration, 3
|
||||
refused by a brake (a supervisor must not retry), 4 usage. Details:
|
||||
|
||||
@@ -183,3 +183,75 @@ Receipts to the private evidence directory as before.
|
||||
Reviewer per Q12, in practice rev-code-02 on issue #1509. Commit after the
|
||||
suite is green and the review passes; live check after the commit; push
|
||||
only on Jason's word.
|
||||
|
||||
## 7. Rulings (Jason, 2026-09-14)
|
||||
|
||||
Asked as a seven-question round in plain terms; every answer matched the
|
||||
recommendation.
|
||||
|
||||
- R1 Roots: this repository's `docs/` and `agents/sage/` only. DYOR
|
||||
repository and jarvis-brain later, each as its own step.
|
||||
- R2 Q16 stays: no DYOR strategy in Discord until the shared repository
|
||||
exists.
|
||||
- R3 Confinement by a Mosaic-owned extension on the host, pi built-in tools
|
||||
off. The container route is for writes, later.
|
||||
- R4 Limits: 8 tool calls per message, 400 lines per read, files over
|
||||
256 KiB refused. All in the binding.
|
||||
- R5 Sage says plainly when a read was refused.
|
||||
- R6 The folder list is a fixed key: stop and start to change it.
|
||||
- R7 Review by rev-code-02 on #1509, offline suite, local commit, live check
|
||||
in #sage-admin with Jason. Push on Jason's word only.
|
||||
- Carmen gets the tools too (Jason, 2026-09-13, before the round).
|
||||
|
||||
## 8. Build notes (2026-09-14)
|
||||
|
||||
Built as in section 3 with these departures, each recorded here rather
|
||||
than silently:
|
||||
|
||||
- The engine settles a prompt on `agent_end` (with `willRetry` false), not
|
||||
on a turn end without tool calls. pi's `agent_end` carries every message
|
||||
of the run, so the reply is the run's last assistant message and the
|
||||
count of `turn_end` events is recorded as `engine.turns`. A run that ends
|
||||
on a tool-only turn settles with empty text, which the connector already
|
||||
turns into the fixed failure line (`engine-empty`).
|
||||
- The extension is plain JavaScript (`.mjs`), confirmed to load through
|
||||
`--extension` in rpc mode; `--no-extensions` stays on so only the explicit
|
||||
path loads. A throw at load makes pi exit 1, so a missing
|
||||
`MOSAIC_DISCORD_TOOLS` refuses the connector start rather than running
|
||||
without tools. The suite proves all three flag combinations against the
|
||||
real pi, offline, with a probe extension that prints the active tool list.
|
||||
- Refused reads keep the requested root and path in the turn record as
|
||||
evidence of what was asked; the model gets the fixed reason only.
|
||||
- Unreadable files (permissions) are a fixed refusal, not a tool error, and
|
||||
search skips them like binary, oversize and credential-bearing files.
|
||||
- A tool root may not be `/`, the home directory, or a path with a
|
||||
dot-prefixed segment (binding loader), and may not sit inside or above the
|
||||
data root (resolved at `check`/`run` against the configured data root).
|
||||
- Q15's paragraph changes only when `tools` is present; without it the
|
||||
pilot's wording is byte-identical.
|
||||
|
||||
Round 1 review (rev-code-02, #1509 comment 26272) asked for four changes,
|
||||
all made with a test that fails on the old code:
|
||||
|
||||
- Tool and turn events now go to the prompt at the front of the queue, and
|
||||
are dropped while that prompt is already failed. Before, a run that
|
||||
outlived its timeout wrote its reads into the next prompt's record.
|
||||
- A read opens the checked file once, with no following of a final symlink
|
||||
and no blocking on a FIFO. It compares device and inode with the walk's
|
||||
`lstat` and reads only from that descriptor, up to the cap plus one byte.
|
||||
A rename between the check and the open is refused instead of followed.
|
||||
Files with more than one hard link are refused too, since a link made
|
||||
under a root can name a file outside it; the two live roots hold none.
|
||||
- The credential shape allows one scheme word before the value, so the
|
||||
usual Authorization header form refuses the read. On the two live roots
|
||||
this refuses no file the old shape allowed.
|
||||
- `scripts/discord-service.sh` names the account for lingering without
|
||||
needing `USER` set.
|
||||
|
||||
One gap stays open and is recorded here. `list_dir` checks the folder's
|
||||
inode after reading its names. A local process that swaps a folder and
|
||||
swaps it back inside that window could still show names from elsewhere,
|
||||
never file content. Only someone who can already write under a root on
|
||||
this host can try it; Discord users cannot write.
|
||||
|
||||
Suite: `scripts/test-discord.sh` 47 checks, node tests 114 (was 41 and 101).
|
||||
|
||||
@@ -367,3 +367,4 @@ git history + Gitea issues.
|
||||
- 2026-09-13 — Discord connector pilot for the Sage seat (#1509; QUEUE rows 14–15; brief `2026-09-13_discord-connector-pilot.md`): nine review rounds with rev-code-02, live pilot steps 1–8 with private receipts, Gate H passed (Jason: the replies read as Sage). Commits 786e379c, 788515dc (pushed). MVP iteration 1, eyes reaction as a read receipt, committed 93d6b624 (local); live check pending. Connector stays up in tmux `discord-sage`; binding and token live outside the repo.
|
||||
- 2026-09-13 — Discord connector iteration 2 (#1509, QUEUE row 17): systemd user service `mosaic-discord@<binding>` with a supervised run that clears a dead lock and never retries a brake (exit 3). Sage seat now runs under systemd, not tmux. Next: control board row.
|
||||
- 2026-09-13 — Discord connector iterations 4 and 5 (#1509, QUEUE rows 19–20): `reload` verb and `systemctl --user reload` apply channels, users, limits and guildName to the running connector, fixed keys refused, attempts journaled in `reloads.jsonl`; per-user channel allowlist; Carmen enrolled live by a reload at 00:03 UTC (all listed rooms except #sage-admin). Suite 41/41, 101 node tests. Commit caaef941 plus records. Row 18 (board row) is darkwing's by Jason's ruling.
|
||||
- 2026-09-14 — Discord connector iteration 6 (#1509, QUEUE row 21): read-only tools through a Mosaic pi extension confined to declared roots (Jason's R1–R7: repo `docs/` and `agents/sage/`, every listed user including Carmen, 8 calls a message, 400 lines a read, 256 KiB a file, refusals said plainly, `tools` a fixed key). rev-code-02 approved round 2 (26276). Suite 41 → 48, node tests 101 → 116. Next: live check in #sage-admin, then attachments.
|
||||
|
||||
+2
-1
@@ -45,7 +45,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
| 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 | done: caaef941; live 00:03 UTC: reload applied Carmen's entry with no restart, unknown key refused by the CLI (exit 2), fixed key refused in the process with the binding kept, `systemctl --user reload` applied; suite 41/41 (101 node tests); receipt `mvp4-5-reload-carmen-*.json` | 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 | done: caaef941 (`users[].channels` allowlist, `channel-not-for-user` drop); Carmen enrolled live by reload 00:03 UTC; her first message is the remaining check | Carmen gets a reply in #general and silence in #sage-admin; Jason unchanged | `2026-09-13_discord-connector-pilot.md` section 11 |
|
||||
| 21 | Discord connector: read-only tools for the Discord Sage through a Mosaic pi extension confined to declared roots (MVP iteration 6) | coordinator; reviewer per Q12 | #1509 | briefed; waiting on Jason's D1–D3 (roots, every listed user, Q16) | Sage answers a question from a file under a declared root with the reads in the turn record; a read outside the roots is refused and recorded | `2026-09-14_discord-readonly-tools.md` |
|
||||
| 21 | Discord connector: read-only tools for the Discord Sage through a Mosaic pi extension confined to declared roots (MVP iteration 6) | coordinator; reviewer per Q12 | #1509 | approved: rev-code-02 round 2 verdict 26276 (tree 43f0329b); committed locally; live check in #sage-admin with Jason next; Jason ruled R1–R7 2026-09-14 (roots docs/ and agents/sage/, Carmen included) | Sage answers a question from a file under a declared root with the reads in the turn record; a read outside the roots is refused and recorded | `2026-09-14_discord-readonly-tools.md` |
|
||||
|
||||
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
|
||||
@@ -94,3 +94,4 @@ Gate F or when blocked."
|
||||
- 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.
|
||||
- 2026-09-13 — coordinator: rows 19–20 done (caaef941, live reload 00:03 UTC, Carmen enrolled); Carmen's first message pending as the operator check. No other row changed.
|
||||
- 2026-09-14 — coordinator: row 21 (read-only tools) briefed on Jason's "let's move to tools"; Carmen's test deferred by Jason; waiting on D1–D3 in the brief. No other row changed.
|
||||
- 2026-09-14 — coordinator: row 21 rulings in (brief section 7), built and approved by rev-code-02 (26276); suite 48/48, 116 node tests; committed locally, live check next. No other row changed.
|
||||
|
||||
Reference in New Issue
Block a user