docs(agents): surface capability belongs to the session, not the process env

The rule the preview-tool bug broke, written down so the next GUI-adjacent tool
does not rediscover it: the client and the backend are separate machines, so
"was this process spawned by Electron?" cannot answer "is a GUI watching?".
Names the working pattern (toolset gates the surface, check_fn answers only
reachability or user opt-in), the process-wide check_fn TTL cache that makes it
the wrong home for a per-session answer, and the test that would have caught
it — assert the GUI session gets the tool with the env var absent.
This commit is contained in:
Brooklyn Nicholson 2026-08-06 20:27:09 -05:00 committed by brooklyn!
parent 7ad9ace2cc
commit ac745a0b07
2 changed files with 47 additions and 0 deletions

View File

@ -210,6 +210,45 @@ backends, providers, notifiers), don't merge them one at a time — design an
ABC + orchestrator, wrap the existing built-in as the first provider, and turn
the competing PRs into plugins against that interface.
### Surface capability is a property of the SESSION, never of the process env
A tool that only works because of *who is on the other end of the connection*
the desktop app's panes, the in-app browser, message reactions, Projects — must
resolve its availability from the **session's own source**, not from an env var
on the backend process.
The client and the backend are separate machines on separate clocks. The
desktop app can be driving a backend Electron spawned locally, one over SSH,
one behind a plain URL + token, or Hermes Cloud. Only the first two are spawned
by us and carry `HERMES_DESKTOP=1`. Every env-keyed GUI gate is therefore a
silent no-op on the other half of the topologies, and the failure is invisible:
the tool is stripped from the schema before the model ever sees it, on the same
backend whose platform hint is telling the model it's *"chatting inside the
Hermes desktop app."*
The pattern that works:
- **The toolset is the surface gate.** Keep the tools off `_HERMES_CORE_TOOLS`
(nobody else should pay their schema) and put them in a named toolset —
`desktop_ui`, `project`. The GUI gateway's `_load_enabled_toolsets(platform)`
folds that toolset in when the session's platform says GUI. One resolver,
every topology.
- **`check_fn` answers reachability or user opt-in, not surface.** "Is the
renderer bridge wired?", "did the user enable reactions?" — fine. "Was I
spawned by Electron?" — not fine. `check_fn` results are also TTL-cached
process-wide (`tools/registry.py`), so a per-session answer does not belong
there at all: one process serves many sessions.
- **Ask which identity you actually mean.** `HERMES_DESKTOP=1` legitimately
marks *"this backend process was spawned by the app"* — it gates the cron
ticker and web-dist handling correctly. It does NOT mean "a GUI is watching",
and the embedded terminal pane (`hermes --tui` against that same backend) is
the standing counterexample.
Same test both ways: if the capability would still make sense with the client
on another machine, it is session-scoped. Cover it with a test that asserts the
GUI session gets the tool **with the env var absent** — that's the assertion
the original gate could never have passed.
## Development Environment
```bash

View File

@ -153,6 +153,14 @@ consumer. Design a shared contract only once more than one real consumer proves
its shape. "Plugin" means several unrelated things across Hermes — do not assume
one surface's extension model runs in another.
When the new capability is an **agent-callable** one — a tool that acts on this
renderer (open a pane, read the in-app browser, react to a message) — it is a
property of the SESSION's client, not of the backend host. Wire its
availability off the session source the app already sends on `session.create`
(`source: 'desktop'`), never off an env var on the backend process: that
process might be a remote or cloud gateway this app merely connected to. See
the root AGENTS.md, "Surface capability is a property of the SESSION."
## Respect the person using it
Design and engineering meet at intent. The user's attention and context are