Row 23. write_file and edit_file for roots marked write: true under the same fence as reads; web_fetch (https only, public addresses, pinned connection, capped body) and web_search through SearXNG; extension renamed to tools.mjs. Engine holds a prompt while pi is busy and sends it as its own run, so a second message mid-turn no longer folds into the first (live defect). fake-pi models the real follow-up folding. Suite 52/52, node tests 129. rev-code-02 APPROVED round 3, comment 26362, tree dbd2ce9a. Records: QUEUE rows 23-24, CURRENT, BUILD-LOG phase, SESSIONS, row 24 brief (git verbs, D5-D7 ruled). Co-Authored-By: Claude Fable 5.1 <[email protected]>
4.8 KiB
Discord Sage: writes into the strategy repository, and web research (#1509, QUEUE row 23)
Jason's word, 2026-09-16: "We need A + web. The agent needs to be able to
research." Option A was: writing confined to the shared-signals root,
no shell. This brief adds web reach to that.
1. Outcome
A Discord message to Sage can end with a file written or changed inside
the shared-signals repository, and Sage can read pages on the web while
answering. Nothing else on the host becomes reachable. Every write and
every web call is in the turn record.
2. What is built
Two more tools in the Mosaic extension (packages/discord/extension/ readonly-tools.mjs, to be renamed tools.mjs with the rules in
src/tools.mjs):
write_file(root, path, text): creates or replaces a file. Allowed only in roots the binding marks"write": true. Same path rules as reads (names not paths, no.., no dot segments, symlink walk refused), plus: the parent directory must already exist under the root, the target is a regular file or absent,.git/and any dot-prefixed path are refused, the text is at mostmaxFileBytes, a temp file and rename so a half-written file never exists, and the credential shapes refuse the write the same way they refuse a read.edit_file(root, path, old, new): one exact replacement of a string that occurs exactly once. Same fences aswrite_file.
Two web tools:
web_fetch(url): GET only,httpsonly, redirects followed at most three times and re-checked, no private or link-local addresses after name resolution, response capped atmaxFetchBytes, HTML reduced to text before the model sees it, 15 s timeout. No cookies, no auth headers, a fixed User-Agent naming the bot.web_search(query): a query to a SearXNG instance named in the binding (D1),GET /search?q=…&format=json, no key. Returns title, url and snippet, at most 10 results. The instance url must behttp://127.0.0.1orhttps; the query is sent as one parameter.
Binding changes (tools key, still fixed, needs a restart):
"tools": {
"roots": [
{ "name": "stack-docs", "path": "…/mosaic-stack/docs" },
{ "name": "sage", "path": "…/mosaic-stack/agents/sage" },
{ "name": "shared-signals", "path": "…/shared-signals", "write": true }
],
"maxFileBytes": 262144,
"maxCallsPerTurn": 12,
"web": { "searxng": "http://127.0.0.1:8888", "maxFetchBytes": 1048576 }
}
Without web, no web tools are offered. Without any write: true root,
no write tools are offered, and the prompt paragraph stays as today.
The system prompt paragraph names which roots are writable and says a write is real only once Jason commits it. Sage cannot run git, so the answer names the file it changed.
3. What is not built
No shell. No git from Sage: Jason commits from the terminal after
git diff. No writes to the Mosaic repository roots. No POST or forms on
the web. No per-user tool gating (Carmen gets the same tools where she is
allowed to write; see D2).
4. Evidence
packages/discord/tests/tools.test.mjs: refused write outside a writable root, into.git, through a symlinked parent, over a FIFO, past the cap, with a credential shape; a happy path that leaves the exact bytes; an edit with zero or two matches refused; a temp file never left behind after a refused rename.- Web tests against a local
http.createServer: redirect to a private address refused, size cap, timeout, non-https refused, HTML to text. - Suite check: the extension exposes exactly the tools the binding
enables, and a binding without
webexposes none of the web tools. - Live: Jason asks in #ideas for a naming shortlist written to
vault/Businesses/…; the file appears, the turn record shows the write and the web calls,git statusin shared-signals shows one new file.
5. Jason's rulings (2026-09-16)
- D1. Search goes through SearXNG, a self-hosted metasearch with a JSON
API and no key, so Sage is tied to no search vendor. The binding names
the instance url (
"web": {"searxng": "http://127.0.0.1:8888"}); the tool calls/search?q=…&format=jsonand returns title, url and snippet, at most 10. No instance runs on this host yet, so this piece includes a SearXNG container under the user's podman or docker, bound to localhost,format=jsonenabled in its settings. Jason's words: "I hate to tie Sage to Z.ai. If I can't [switch] providers, we need flexibility." - D2. Only Jason and Carmen work with the repository and Sage. Both may write. The binding's user list already enforces who reaches Sage.
- D3. Any https host; private and link-local addresses refused.
- D4. Reviewer: rev-code-02 on #1509, as row 21.
6. Order
Writes first (a day, with the tests), then web fetch and search, then
the SearXNG container and the live check. Each part is a separate local commit on refactor, no
push without Jason's word.