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]>
109 lines
4.8 KiB
Markdown
109 lines
4.8 KiB
Markdown
# 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.
|