From ac745a0b0767b80795f6532d882e35eb517bac32 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 6 Aug 2026 20:27:09 -0500 Subject: [PATCH] docs(agents): surface capability belongs to the session, not the process env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- AGENTS.md | 39 +++++++++++++++++++++++++++++++++++++++ apps/desktop/AGENTS.md | 8 ++++++++ 2 files changed, 47 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 70fc9bc8d647e..95f3985c4bd77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/apps/desktop/AGENTS.md b/apps/desktop/AGENTS.md index 6908c2a423438..c59d13e8b1cdf 100644 --- a/apps/desktop/AGENTS.md +++ b/apps/desktop/AGENTS.md @@ -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