# 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 most `maxFileBytes`, 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 as `write_file`. Two web tools: - `web_fetch(url)`: GET only, `https` only, redirects followed at most three times and re-checked, no private or link-local addresses after name resolution, response capped at `maxFetchBytes`, 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 be `http://127.0.0.1` or `https`; the query is sent as one parameter. Binding changes (`tools` key, still fixed, needs a restart): ```json "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 `web` exposes 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 status` in 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=json` and 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=json` enabled 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.