Przeglądaj źródła

docs(chatto): document per-room toolsets and their replacement semantics

States the user-visible behaviour where users meet it (variable table with
its default, the capability matrix, a Features section) and the reasoning
in CONCEPTS: why the hook's answer replaces platform_toolsets instead of
subtracting from it, why the key is the room ID the event carried, and why
entry syntax fails startup while unknown toolset names are left to Hermes.
Paul Klumpp 1 tydzień temu
rodzic
commit
10ea2b09d7
3 zmienionych plików z 55 dodań i 0 usunięć
  1. 10 0
      AGENTS.md
  2. 39 0
      CONCEPTS.md
  3. 6 0
      README.md

+ 10 - 0
AGENTS.md

@@ -347,6 +347,16 @@ changes must keep them, or consciously renegotiate the docs.**
   back to `send()` rather than receiving silent truncation. The standalone
   cron sender splits the same way, so out-of-process delivery cannot die on
   the per-message limit either.
+- **Per-room toolsets replace, never subtract.** `toolsets_for_source` answers
+  with exactly the toolsets `CHATTO_ROOM_TOOLSETS` names for the turn's room
+  (`room_id:toolset` entries, accumulated per room; a thread turn carries its
+  room's ID), because Hermes' contract for that hook is a *replacement* of
+  `platform_toolsets`, not a subtraction from it. A room without an entry
+  returns None so the platform list stays in charge. A malformed entry is
+  rejected at startup: ignoring it would leave a room that the user meant to
+  restrict on the default toolsets, which fails open. Toolset *names* are not
+  validated here — Hermes drops unknown ones, so a typo yields fewer tools,
+  which is the safe direction.
 
 ### Auth defaults to closed
 

+ 39 - 0
CONCEPTS.md

@@ -8,6 +8,7 @@ for a reader who wants to understand the design before changing it.
 - [Thread sessions and the frontier](#thread-sessions-and-the-frontier)
 - [Realtime resume](#realtime-resume)
 - [Configuration resolution](#configuration-resolution)
+- [Per-room toolsets](#per-room-toolsets)
 - [One send path, two callers](#one-send-path-two-callers)
 
 ## Thread sessions and the frontier
@@ -230,6 +231,44 @@ differently is exactly the drift risk this design usually avoids; the containmen
 that they converge on one type, and the only place that fills it differently is the
 one place where no better information exists.
 
+## Per-room toolsets
+
+### Why the hook exists at all
+
+Hermes has no per-chat tool policy of its own. The adapter hook
+`toolsets_for_source(source)` is the only door: the gateway asks the adapter,
+per turn, which toolsets to enable. The webhook adapter already uses it for
+per-route tool lists, which is the precedent this follows.
+
+### Replacement, not subtraction
+
+The hook's contract is that a returned list *replaces* `platform_toolsets` for
+that turn — the gateway copies the override into the platform's entry and
+resolves it through the same `_get_platform_tools` path. Subtraction would need
+a second decision ("minus these, from what?"), and a room entry that removed
+`terminal` would otherwise silently keep everything else. Replacement makes the
+entry self-contained: what is written is what the room gets. The cost is that a
+user must list every toolset the room keeps, including `chatto-platform` if the
+agent should keep its Chatto tools — which is why README says so in the tool
+documentation and in the variable table rather than leaving it to be discovered.
+
+An unlisted room returns `None`, the hook's "no opinion" answer, so a config
+with no entries changes nothing.
+
+### Keyed on the room, validated at startup
+
+The entry key is `chat_id`, which for this adapter is the room ID the realtime
+event carried — not model input, so it cannot be talked around. A thread turn
+keeps the room as `chat_id` and carries the thread separately, so one entry
+covers a room's threads without a second lookup.
+
+The entry *syntax* is validated at startup because a malformed entry would
+otherwise be skipped, leaving a room the operator meant to restrict on the
+default toolsets — the one failure direction that is invisible in logs and
+dangerous. The toolset *names* are deliberately not validated here: Hermes drops
+unknown ones when it resolves the list, so a typo yields fewer tools, which is
+the failure direction that hides nothing.
+
 ## One send path, two callers
 
 A user cannot tell whether the agent replied on its own or through a tool, so the

+ 6 - 0
README.md

@@ -48,6 +48,7 @@ Capabilities natively implemented by the Chatto plugin adapter:
 | room join/leave over DM | yes (`/join`, `/leave`) |
 | member directory | yes (cached) |
 | room member context | yes (roster on every channel turn, presence kept live from a per-room cache) |
+| per-room toolsets | yes (`CHATTO_ROOM_TOOLSETS` replaces the platform toolsets for the rooms it lists) |
 | presence broadcasting | yes (online, refreshed) |
 | interactive prompts | yes (reaction-driven clarify + exec approval, see [Interactive Prompts](#interactive-prompts)) |
 | custom status | yes |
@@ -176,6 +177,7 @@ platforms:
 | `CHATTO_ALLOW_ALL_USERS` | No | `false` | Allow any Chatto user to talk to the agent (`true`/`false`) |
 | `CHATTO_REQUIRE_MENTION_ROOMS` | No | _(silent everywhere)_ | Comma-separated room IDs where Hermes answers only when addressed (`@name`, `@all`, `@here`). A room on neither mention list stays silent. |
 | `CHATTO_OPTIONAL_MENTION_ROOMS` | No | — | Comma-separated room IDs where Hermes answers every message, addressed or not. Must not overlap `CHATTO_REQUIRE_MENTION_ROOMS` (rejected at startup). |
+| `CHATTO_ROOM_TOOLSETS` | No | _(no room restricted)_ | Comma-separated `room_id:toolset` entries, e.g. `RbmwiHk4vs9a3Ql:web,RbmwiHk4vs9a3Ql:file`. Several entries for one room accumulate. A listed room **replaces** the platform's toolsets for its turns, so list every toolset it should keep; unlisted rooms keep the normal platform toolsets. A malformed entry is rejected at startup. |
 | `CHATTO_AUTO_THREAD` | No | `true` | Auto-create threads for replies in rooms (`true`/`false`) |
 | `CHATTO_REACTIONS` | No | `true` | 👀/✅/❌/🚫 processing reactions (`true`/`false`) |
 | `CHATTO_EDIT_DISPATCH` | No | `true` | Treat edits of chat messages as corrections: a message being processed restarts with the edited text, an ignored message is re-checked against its new text, already-answered messages stay answered (`true`/`false`) |
@@ -292,6 +294,10 @@ If the realtime stream drops, the adapter reconnects automatically with exponent
 
 After discovering rooms, the adapter marks joined rooms as read and dismisses pending notifications (`MarkRoomAsRead` / `DismissAllNotifications`), keeping the bot's unread indicators clean.
 
+### Per-Room Toolsets
+
+`CHATTO_ROOM_TOOLSETS` narrows what the agent may do in a named room — useful for a public or untrusted channel where the bot should answer but not reach for the shell. Each entry is `room_id:toolset`, several entries for one room accumulate, and a turn in a listed room gets exactly the toolsets its entries name. This is a **replacement**, not a subtraction: a room listed as `RbmwiHk4vs9a3Ql:web` loses every platform toolset that is normally enabled, including the `chatto-platform` tools, so list everything the room should keep. Rooms without an entry keep the normal platform toolsets. A thread turn belongs to its room and is restricted the same way. A malformed entry stops startup rather than being guessed at — silently leaving a restricted room unrestricted would fail open.
+
 ### DM Initiation
 
 On Chatto only a person can start a direct message — a bot cannot, not even with `message.post`. So a DM room exists only once the other user has written to the bot at least once; until then the bot has no room to post into, and no setting changes that. The adapter therefore has no way to open a DM at all: it joins *rooms* on request (`/join <room-id or #name>` in a DM, see [Managing Rooms over DM](#managing-rooms-over-dm)), and the `chatto_send_dm` plugin tool reports the precondition rather than pretending to deliver.