docs: move the agent tool docs to 4-implementation/agent-tools
This commit is contained in:
parent
df8a82bc33
commit
aa227a95c1
1 changed files with 94 additions and 83 deletions
175
docs/4-implementation/agent-tools/matrix.md
Normal file
175
docs/4-implementation/agent-tools/matrix.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Matrix MCP tools and extra MCP servers
|
||||
|
||||
For the operator: this page covers how the harness adds the matrix tools and what
|
||||
they do around the edges. How an agent calls them is in the tools' own MCP
|
||||
descriptions.
|
||||
|
||||
## Built-in matrix MCP (`mcp__matrix__*`)
|
||||
|
||||
When the agent has at least one `services.hyperhive.agent.matrixAccounts`
|
||||
entry, the harness autoinjects `hive-matrix-daemon`'s
|
||||
streamable-http endpoint as a second MCP server (no stdio bridge —
|
||||
see Architecture below). No `matrix.enable` switch exists: the account
|
||||
set *is* the switch, and the module declares the hive-internal `main`
|
||||
account for you as soon as `services.hyperhive.agent.matrix.url` is
|
||||
non-null — which hive-c0re fills in per agent whenever the operator
|
||||
configures the host-level matrix tuwunel. Tools land as `mcp__matrix__<name>`.
|
||||
|
||||
Each tool's parameters and return values are in its own MCP description, which
|
||||
is what an agent reads at call time. They fall into four groups:
|
||||
|
||||
- **Messaging:** `send_message`, `send_dm`, `send_reply`, `send_reaction`,
|
||||
`send_file`, `send_redact` and `open_dm`. A room is `!id:server` or
|
||||
`#alias:server`, and the daemon resolves either. `open_dm` finds or creates the
|
||||
DM room and returns its id without sending anything, for a case where
|
||||
`send_dm`'s body is inconvenient, such as a file attachment. `send_file` caps
|
||||
an upload at 50 MiB. Redacting someone else's event needs moderator power.
|
||||
- **Reading:** `read_room`, `download_file`, `list_rooms` and
|
||||
`list_room_members`. `read_room` shows media as `[file: …]`, `[image: …]`,
|
||||
`[audio: …]` or `[video: …]` markers, and the event id of one goes to
|
||||
`download_file`. An edited message shows once, with its latest text and
|
||||
`edited: true`. The edit event doesn't appear as a second message and doesn't
|
||||
count as unread. When the original is outside the window read, the edit stays
|
||||
as its own event, and its `replaces_event_id` names the message it replaces.
|
||||
- **Room membership:** `invite_user`, `list_invites`, `resolve_invite` and
|
||||
`join_room`. `resolve_invite` accepts or rejects a pending invite and is the
|
||||
path for invites; `join_room` is for a public room the agent wasn't invited to.
|
||||
`list_invites` gives the inviter and the topic, so an agent can judge an
|
||||
invite before it answers.
|
||||
- **Receipts:** `mark_read` advances the read receipt.
|
||||
|
||||
**DM recipient check.** `send_dm` and `open_dm` report where the other user
|
||||
stands in the room through a `recipient` field. `joined` means they see the
|
||||
message. `invited` means the invite is pending, and the result carries a
|
||||
`warning`. When the user is in neither state, because the invite failed
|
||||
or they left, the call fails and sends nothing.
|
||||
|
||||
**Unread guard.** `send_message`, `send_dm`, `send_file` and `send_reply` fail
|
||||
while the room has unread messages. The agent calls `read_room` and then
|
||||
`mark_read` on the latest event before sending to a room it hasn't read.
|
||||
|
||||
## Multiple accounts
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
`services.hyperhive.agent.matrixAccounts` (declared in `agent.nix`) is
|
||||
the agent's full set of matrix identities — for example an
|
||||
external-facing account alongside the internal one — **and, being
|
||||
non-empty, is what turns matrix on for the agent at all.** Each entry is
|
||||
keyed by account name and specifies `tokenFile` (bearer token,
|
||||
provisioned out-of-band; basename must start with `matrix-token`),
|
||||
`sessionDir` (per-account matrix-sdk sqlite state — crypto keys +
|
||||
cache), and an optional `homeserver` (defaults to
|
||||
`services.hyperhive.agent.matrix.url`). The hive-internal account is
|
||||
always named `main` and is always the primary; the matrix module
|
||||
declares it for you as an ordinary entry of this map, from
|
||||
`services.hyperhive.agent.matrix.url` + agent state, so what you add
|
||||
here are the *further* accounts. Its token comes from the swarm secret
|
||||
store (`swarm-controller` mints it); its `tokenFile` stays pinned to
|
||||
`<state>/matrix-token` as the fallback, and the dashboard's link-account
|
||||
route refuses to create an account by that name.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
`main` is present exactly when that URL is non-null, which is the whole
|
||||
mechanism behind "accounts are the enable signal": an agent with no
|
||||
homeserver and no account of its own has an empty set, so it gets no
|
||||
daemon, no path watcher and no injected MCP entry. That's how you give
|
||||
an agent no matrix tools. Declaring an external account with
|
||||
its own `homeserver` is enough on its own; a hive homeserver isn't
|
||||
required.
|
||||
|
||||
An account linked from the swarm UI needs no entry in this map.
|
||||
`swarm-controller` stores it at `swarm/agents/<agent>/matrix/<account>`.
|
||||
The daemon lists that directory under the agent's own certificate at
|
||||
start and every two minutes, and restarts itself when the set of linked
|
||||
accounts changes. A declared entry of the same name wins.
|
||||
|
||||
Every matrix tool above takes an optional `account` parameter (a name
|
||||
from this map, or a linked account's) to act as that identity instead
|
||||
of the primary one.
|
||||
|
||||
Declaring an extra account also changes what the agent *receives*: the
|
||||
summaries of unread-room and invite todos gain an `[acct:<name>]` prefix — see
|
||||
[Architecture](#architecture) below.
|
||||
|
||||
## Architecture
|
||||
|
||||
`hive-matrix-daemon` is a single long-running process (one per agent
|
||||
container, systemd service in `nix/agent-modules/matrix.nix`) — no
|
||||
stdio bridge, no separate bin. It owns the matrix-sdk `Client` + sync
|
||||
loop per configured account **and** serves the matrix tool surface
|
||||
directly over streamable-http on `services.hyperhive.agent.mcp.matrixHttpPort`
|
||||
(declared in `services.hyperhive.agent.extraMcpServers.matrix` as
|
||||
`{ type = "http"; url = ...; }`). Same shape as `hive-bash-daemon` and
|
||||
the built-in `hyperhive` surface (`hive-mcp-http`) — claude reconnects
|
||||
to the stable URL every turn instead of respawning a stdio child.
|
||||
Silently exits when the primary account has no token yet; the
|
||||
`systemd.paths.hive-matrix-daemon` watcher restarts it when a token file
|
||||
appears, and, on an agent with a store, a timer restarts it while it's
|
||||
down so it re-reads a token the swarm minted into the store.
|
||||
|
||||
**Wake path**: after every sync the daemon upserts a harness-local todo
|
||||
on the in-agent socket for each unread room (keyed by its room id) and
|
||||
each pending invite (below). A new or changed todo wakes the agent with
|
||||
the generic prompt `you have todos — call get_loose_ends to see them`
|
||||
(from `todo`). The `[matrix]` lines reach the agent through
|
||||
`get_loose_ends`, one per room, as
|
||||
`- todo #<id> [matrix <room_id>, <age>s old]: <summary> (cancel_loose_end …)`.
|
||||
The summary depends on the room's unread state:
|
||||
|
||||
- **One unread message**: `[matrix] <sender> in <room>: <body> —
|
||||
use read_room to view, mark_read to clear`
|
||||
- **Multiple unread messages**: `[matrix] N unread in <room> —
|
||||
use read_room to view, mark_read to clear`
|
||||
|
||||
**Multi-account prefix**: when the daemon serves more than one account
|
||||
(`services.hyperhive.agent.matrixAccounts`), it prefixes the summary of every
|
||||
unread-room todo above and every invite todo below with `[acct:<name>] `, so
|
||||
`get_loose_ends` shows
|
||||
`- todo #<id> [matrix <room_id>, …]: [acct:ccc] [matrix] <sender> in <room>: …`. A single-account agent gets the
|
||||
formats exactly as written — the daemon adds nothing — which is why matching
|
||||
on a leading `[matrix]` works right up until you declare a second account.
|
||||
|
||||
hive-matrix-daemon includes the same per-room breakdown in the `UnreadMatrix`
|
||||
entry `get_loose_ends` returns, so unread rooms surface in the
|
||||
loose-ends list between turns.
|
||||
|
||||
**Invite wakes**: the daemon sweeps `invited_rooms()` after every sync
|
||||
callback (deliberately not a one-shot `m.room.member` event handler —
|
||||
a one-shot signal that raced a socket-down window vanished with no
|
||||
retry, leaving the agent deaf until manually prompted) and upserts a
|
||||
todo (keyed `invite:<room>`) for each pending invite on the harness's
|
||||
in-agent socket, which drives a turn. The daemon does **not**
|
||||
autojoin — the agent calls `list_invites` to see pending invites and
|
||||
`resolve_invite` to accept or reject them.
|
||||
|
||||
**Pending invites as loose ends**: the daemon upserts pending invites as
|
||||
keyed todos, which appear in `get_loose_ends` output as
|
||||
`[matrix] invited to <room> (<room_id>) — use list_invites to see
|
||||
pending invites, resolve_invite to accept or reject`. The daemon clears
|
||||
the keyed todo when a `resolve_invite` (or `join_room`) call resolves
|
||||
the invite.
|
||||
|
||||
See [`docs/2-handbook/integrations/matrix.md`](../../2-handbook/integrations/matrix.md) for the homeserver setup,
|
||||
provisioning flow, and federation config.
|
||||
|
||||
## Extra MCP servers (per-agent)
|
||||
|
||||
Each agent's NixOS config can declare additional MCP servers via
|
||||
`services.hyperhive.agent.extraMcpServers.<key> = { type, command, args, env, url,
|
||||
allowedTools }` — `type = "stdio"` (the default, uses `command`/`args`/
|
||||
`env`) or `type = "http"` (uses `url`, a long-lived streamable-http
|
||||
endpoint — see `hive-bash-daemon` and `hive-matrix-daemon` above for
|
||||
the `"http"` shape). The module writes the map to
|
||||
`/etc/hyperhive/extra-mcp.json`; the harness reads it at boot and
|
||||
merges every entry into `--mcp-config` (under `mcpServers.<key>`)
|
||||
and `--allowedTools` (as `mcp__<key>__<pattern>`).
|
||||
|
||||
The agent's `flake.nix` forwards every flake input to `agent.nix` as
|
||||
the `flakeInputs` module arg, so you pull in external MCP-server flakes
|
||||
by adding them to `inputs.*` and reference them as
|
||||
`flakeInputs.<name>.packages.${pkgs.system}.default` — the resolved
|
||||
sha lands in the agent's own `flake.lock` and rolls up to `internal/meta`'s.
|
||||
|
||||
`allowedTools` defaults to `["*"]`, which expands to
|
||||
`mcp__<key>__*` (every tool from that server autoapproved). Restrict
|
||||
to specific tool names when you want finer control.
|
||||
Loading…
Reference in a new issue