Compare commits
7 changed files with 92 additions and 254 deletions
|
|
@ -546,15 +546,9 @@ the body + marker check at that point.
|
||||||
process per agent. Serves its MCP tools directly over
|
process per agent. Serves its MCP tools directly over
|
||||||
streamable-http (`services.hyperhive.agent.mcp.matrixHttpPort`, no stdio bridge —
|
streamable-http (`services.hyperhive.agent.mcp.matrixHttpPort`, no stdio bridge —
|
||||||
same shape as `hive-bash-daemon`), emits hyperhive wake signals
|
same shape as `hive-bash-daemon`), emits hyperhive wake signals
|
||||||
on incoming room events via `/run/hive/mcp.sock`. Conditional on the
|
on incoming room events via `/run/hive/mcp.sock`. Conditional on
|
||||||
agent having at least one `services.hyperhive.agent.matrixAccounts`
|
`services.hyperhive.agent.matrix.enable` (which both the daemon AND the
|
||||||
entry — there is no separate enable switch, and the same condition gates
|
autoinjected `extraMcpServers.matrix` entry read).
|
||||||
the daemon, its path watcher AND the autoinjected
|
|
||||||
`extraMcpServers.matrix` entry. The module declares the hive-internal
|
|
||||||
`main` entry whenever `services.hyperhive.agent.matrix.url` is non-null,
|
|
||||||
so on a real hive (where hive-c0re renders that URL per agent) every
|
|
||||||
agent has one; a `null` URL with no operator-declared account is the
|
|
||||||
"this agent has no matrix" state.
|
|
||||||
|
|
||||||
**First-boot ordering**: hive-c0re provisions the matrix token AFTER
|
**First-boot ordering**: hive-c0re provisions the matrix token AFTER
|
||||||
agent containers come up. Without the path-trigger sibling
|
agent containers come up. Without the path-trigger sibling
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,10 @@
|
||||||
|
|
||||||
## Built-in matrix MCP (`mcp__matrix__*`)
|
## Built-in matrix MCP (`mcp__matrix__*`)
|
||||||
|
|
||||||
When the agent has at least one `services.hyperhive.agent.matrixAccounts`
|
When `services.hyperhive.agent.matrix.enable = true` and the host-level matrix
|
||||||
entry, the harness autoinjects `hive-matrix-daemon`'s
|
tuwunel is configured, the harness autoinjects `hive-matrix-daemon`'s
|
||||||
streamable-http endpoint as a second MCP server (no stdio bridge —
|
streamable-http endpoint as a second MCP server (no stdio bridge —
|
||||||
see Architecture below). No `matrix.enable` switch exists: the account
|
see Architecture below). Tools land as `mcp__matrix__<name>`:
|
||||||
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 host-level
|
|
||||||
matrix tuwunel is configured. Tools land as `mcp__matrix__<name>`:
|
|
||||||
|
|
||||||
### Messaging
|
### Messaging
|
||||||
|
|
||||||
|
|
@ -75,8 +71,7 @@ room you haven't read yet.
|
||||||
|
|
||||||
`services.hyperhive.agent.matrixAccounts` (declared in `agent.nix`) is
|
`services.hyperhive.agent.matrixAccounts` (declared in `agent.nix`) is
|
||||||
the agent's full set of matrix identities — for example an
|
the agent's full set of matrix identities — for example an
|
||||||
external-facing account alongside the internal one — **and, being
|
external-facing account alongside the internal one. Each entry is
|
||||||
non-empty, is what turns matrix on for the agent at all.** Each entry is
|
|
||||||
keyed by account name and specifies `tokenFile` (bearer token,
|
keyed by account name and specifies `tokenFile` (bearer token,
|
||||||
provisioned out-of-band; basename must start with `matrix-token`),
|
provisioned out-of-band; basename must start with `matrix-token`),
|
||||||
`sessionDir` (per-account matrix-sdk sqlite state — crypto keys +
|
`sessionDir` (per-account matrix-sdk sqlite state — crypto keys +
|
||||||
|
|
@ -88,17 +83,8 @@ declares it for you as an ordinary entry of this map, from
|
||||||
here are the *further* accounts. Its `tokenFile` is pinned to
|
here are the *further* accounts. Its `tokenFile` is pinned to
|
||||||
`<state>/matrix-token` (hive-c0re provisions it there), and the
|
`<state>/matrix-token` (hive-c0re provisions it there), and the
|
||||||
dashboard's link-account route refuses to create an account by that
|
dashboard's link-account route refuses to create an account by that
|
||||||
name.
|
name. Declaring extras requires
|
||||||
|
`services.hyperhive.agent.matrix.enable = true`.
|
||||||
`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 — it replaces the removed
|
|
||||||
`services.hyperhive.agent.matrix.enable = false`, which now fails
|
|
||||||
evaluation with a message saying so. Declaring an external account with
|
|
||||||
its own `homeserver` is enough on its own; a hive homeserver isn't
|
|
||||||
required.
|
|
||||||
|
|
||||||
Every matrix tool above takes an optional `account` parameter (a name
|
Every matrix tool above takes an optional `account` parameter (a name
|
||||||
from this map) to act as that identity instead of the primary one.
|
from this map) to act as that identity instead of the primary one.
|
||||||
|
|
|
||||||
|
|
@ -18,25 +18,6 @@
|
||||||
flakeInputs ? { },
|
flakeInputs ? { },
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
# What an `agent.nix` still setting the dropped per-agent matrix switch is
|
|
||||||
# told. Shared by the two `mkRemovedOptionModule` entries below, one per
|
|
||||||
# spelling of the path.
|
|
||||||
matrixEnableRemoved = ''
|
|
||||||
The per-agent matrix switch is gone. Matrix is enabled for an agent exactly
|
|
||||||
when it has at least one services.hyperhive.agent.matrixAccounts entry, and
|
|
||||||
the hive-internal `main` entry is declared for you whenever
|
|
||||||
services.hyperhive.agent.matrix.url is non-null.
|
|
||||||
|
|
||||||
- `enable = true` was the default: drop the line, nothing else to do.
|
|
||||||
- `enable = false`: set services.hyperhive.agent.matrix.url = null instead
|
|
||||||
(and declare no accounts of your own). That is what leaves the account set
|
|
||||||
empty, which is now what suppresses the daemon, its path watcher and the
|
|
||||||
injected extraMcpServers.matrix entry.
|
|
||||||
|
|
||||||
See docs/tools/matrix.md.
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./agent-service.nix
|
./agent-service.nix
|
||||||
|
|
@ -66,20 +47,6 @@ in
|
||||||
The built-in Bash tool is fully disabled; agents use
|
The built-in Bash tool is fully disabled; agents use
|
||||||
mcp__bash__run instead. Remove the setting from your agent.nix.
|
mcp__bash__run instead. Remove the setting from your agent.nix.
|
||||||
'')
|
'')
|
||||||
# The dropped per-agent matrix switch. A removal, not a silent delete,
|
|
||||||
# because the definition that *changes meaning* is `false`: left undeclared
|
|
||||||
# it would be ignored and the agent would quietly gain the matrix tools its
|
|
||||||
# operator turned off. Failing the eval with the replacement spelling is the
|
|
||||||
# only outcome that cannot do that.
|
|
||||||
#
|
|
||||||
# Both spellings, for the same reason ../host-modules/deploy.nix lists both
|
|
||||||
# of `swarm|deploy.matrix.registrationTokenFile`: an existing agent.nix may
|
|
||||||
# carry either the pre-rename `hyperhive.*` path or the current one, and only
|
|
||||||
# the one it actually carries will fire. The rename shim that used to bridge
|
|
||||||
# them is gone from ./renamed-options.nix — a rename pointing at an
|
|
||||||
# undeclared option breaks evaluation for everyone, removed or not.
|
|
||||||
(lib.mkRemovedOptionModule [ "hyperhive" "matrix" "enable" ] matrixEnableRemoved)
|
|
||||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "matrix" "enable" ] matrixEnableRemoved)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.services.hyperhive.agent.icon = lib.mkOption {
|
options.services.hyperhive.agent.icon = lib.mkOption {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
# hive-matrix-daemon (serves its MCP tools directly over
|
# hive-matrix-daemon (serves its MCP tools directly over
|
||||||
# streamable-http), its token-arrival path trigger, and the
|
# streamable-http), its token-arrival path trigger, and the
|
||||||
# auto-injected extraMcpServers entry.
|
# auto-injected extraMcpServers entry.
|
||||||
#
|
|
||||||
# There is no `matrix.enable`. An agent has matrix exactly when it has an
|
|
||||||
# account to serve — see `matrixEnabled` below.
|
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
|
|
@ -19,21 +16,6 @@ let
|
||||||
# its matrix-sdk store. Shared by the `main` account entry below and
|
# its matrix-sdk store. Shared by the `main` account entry below and
|
||||||
# the path-watcher glob at the bottom of this file.
|
# the path-watcher glob at the bottom of this file.
|
||||||
stateDir = "/agents/${userName}/state";
|
stateDir = "/agents/${userName}/state";
|
||||||
accounts = config.services.hyperhive.agent.matrixAccounts;
|
|
||||||
# **The enable signal.** Matrix is on for this agent exactly when it has at
|
|
||||||
# least one account, because an account is the only thing the daemon has to
|
|
||||||
# do: no account, no Client, no sync, no tool surface worth injecting.
|
|
||||||
#
|
|
||||||
# This is not trivially true even though the module declares `main` itself:
|
|
||||||
# that definition is gated on `matrix.url != null` (see below), which is the
|
|
||||||
# per-agent "does this agent have a homeserver to reach" fact. So an agent the
|
|
||||||
# hive handed no homeserver URL, whose operator declared no external account
|
|
||||||
# either, has an empty set here and gets none of the units — the case the
|
|
||||||
# deleted `matrix.enable = false` used to express.
|
|
||||||
#
|
|
||||||
# No cycle: `matrixAccounts`'s own definition reads `matrix.url`, never this
|
|
||||||
# binding, so the `mkIf`s below may read the merged option value freely.
|
|
||||||
matrixEnabled = accounts != { };
|
|
||||||
# Rasterize the operator-set agent icon (`services.hyperhive.agent.icon`, an SVG) to a
|
# Rasterize the operator-set agent icon (`services.hyperhive.agent.icon`, an SVG) to a
|
||||||
# 512x512 PNG so the matrix daemon can upload it as each account's avatar
|
# 512x512 PNG so the matrix daemon can upload it as each account's avatar
|
||||||
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
|
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
|
||||||
|
|
@ -45,6 +27,40 @@ let
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
options.services.hyperhive.agent.matrix.enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Enable per-agent matrix integration via `hive-matrix-daemon`.
|
||||||
|
When true (the default), the harness:
|
||||||
|
|
||||||
|
- runs `hive-matrix-daemon` as a systemd unit that holds a
|
||||||
|
matrix-sdk Client + sync against the homeserver named by
|
||||||
|
`HIVE_MATRIX_URL` (see `services.hyperhive.agent.matrix.url` — there is no
|
||||||
|
default, since an agent's own netns makes a loopback guess
|
||||||
|
wrong). The daemon auto-skips when that URL or
|
||||||
|
`<state>/matrix-token` is missing, and a `systemd.paths`
|
||||||
|
watcher restarts it the moment hive-c0re provisions the token
|
||||||
|
(same path-trigger shape as `forge-avatar-sync`).
|
||||||
|
- exposes the matrix tool surface (send_message, send_dm,
|
||||||
|
send_reaction, send_reply, mark_read, list_rooms,
|
||||||
|
list_room_members, read_room) to claude via an auto-injected
|
||||||
|
`extraMcpServers.matrix` entry pointed at the daemon's own
|
||||||
|
streamable-http listener (`services.hyperhive.agent.mcp.matrixHttpPort`) — no
|
||||||
|
stdio bridge, no per-turn respawn, same shape as the built-in
|
||||||
|
hyperhive surface and `hive-bash-daemon`.
|
||||||
|
- wakes the agent on incoming room events via a short teaser
|
||||||
|
Wake signal (`[matrix] <sender> in <room>: <first 100c>…`)
|
||||||
|
to the hyperhive control socket; the full event stays
|
||||||
|
unread server-side until `read_room` consumes it.
|
||||||
|
|
||||||
|
Set to `false` for agents that should NOT have matrix tools at
|
||||||
|
all (e.g. agents on a host without `services.hyperhive.agent.matrix.enable` on
|
||||||
|
the meta side). When token file is absent the daemon and MCP
|
||||||
|
both no-op cleanly anyway, so `false` is rarely necessary.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
options.services.hyperhive.agent.matrix.url = lib.mkOption {
|
options.services.hyperhive.agent.matrix.url = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
|
@ -64,18 +80,10 @@ in
|
||||||
different host from the agents, and inside an agent's network
|
different host from the agents, and inside an agent's network
|
||||||
namespace `localhost` reaches the agent rather than the
|
namespace `localhost` reaches the agent rather than the
|
||||||
homeserver, so a default would be a value that builds fine and
|
homeserver, so a default would be a value that builds fine and
|
||||||
then talks to the wrong machine. An absent integration, never a
|
then talks to the wrong machine. When this is `null` the daemon
|
||||||
|
is left without a homeserver and no-ops, exactly as it does when
|
||||||
|
the token file is absent --- an absent integration, never a
|
||||||
misdirected one.
|
misdirected one.
|
||||||
|
|
||||||
`null` is also this agent's **matrix off switch**, and the
|
|
||||||
replacement for the `services.hyperhive.agent.matrix.enable`
|
|
||||||
boolean that used to exist: the hive-internal `main` account in
|
|
||||||
`services.hyperhive.agent.matrixAccounts` is declared from this
|
|
||||||
URL, so `null` leaves that set empty and the whole integration —
|
|
||||||
daemon unit, path watcher, injected MCP entry — is not generated
|
|
||||||
at all. Declaring an external account with its own `homeserver`
|
|
||||||
turns matrix back on without a hive homeserver, which is the
|
|
||||||
honest reading of that config.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -135,41 +143,12 @@ in
|
||||||
each account (unique by construction) and is the handle the matrix
|
each account (unique by construction) and is the handle the matrix
|
||||||
MCP tools target via their `account` argument.
|
MCP tools target via their `account` argument.
|
||||||
|
|
||||||
**This set is also the enable signal for per-agent matrix** —
|
The **hive-internal account is always present and is the primary**:
|
||||||
there is no separate boolean. A non-empty set means the harness:
|
it is named `main`, and this module declares it for you (when
|
||||||
|
`services.hyperhive.agent.matrix.enable` is set) from
|
||||||
- runs `hive-matrix-daemon` as a systemd unit that holds a
|
|
||||||
matrix-sdk Client + sync per account against that account's
|
|
||||||
homeserver (`homeserver`, else
|
|
||||||
`services.hyperhive.agent.matrix.url`). The daemon auto-skips an
|
|
||||||
account whose URL or token file is missing, and a `systemd.paths`
|
|
||||||
watcher restarts it the moment hive-c0re provisions the token
|
|
||||||
(same path-trigger shape as `forge-avatar-sync`).
|
|
||||||
- exposes the matrix tool surface (send_message, send_dm,
|
|
||||||
send_reaction, send_reply, mark_read, list_rooms,
|
|
||||||
list_room_members, read_room) to claude via an auto-injected
|
|
||||||
`extraMcpServers.matrix` entry pointed at the daemon's own
|
|
||||||
streamable-http listener (`services.hyperhive.agent.mcp.matrixHttpPort`) — no
|
|
||||||
stdio bridge, no per-turn respawn, same shape as the built-in
|
|
||||||
hyperhive surface and `hive-bash-daemon`.
|
|
||||||
- wakes the agent on incoming room events via a short teaser
|
|
||||||
Wake signal (`[matrix] <sender> in <room>: <first 100c>…`)
|
|
||||||
to the hyperhive control socket; the full event stays
|
|
||||||
unread server-side until `read_room` consumes it.
|
|
||||||
|
|
||||||
An **empty** set is an agent with no matrix at all: none of those
|
|
||||||
three exist. That is the state an agent reaches by having no
|
|
||||||
homeserver (`services.hyperhive.agent.matrix.url = null`) and no
|
|
||||||
account of its own, and it replaces the removed
|
|
||||||
`services.hyperhive.agent.matrix.enable = false`.
|
|
||||||
|
|
||||||
The **hive-internal account is the primary whenever it exists**: it
|
|
||||||
is named `main`, and this module declares it for you from
|
|
||||||
`services.hyperhive.agent.matrix.url` + `<state>/matrix-token` +
|
`services.hyperhive.agent.matrix.url` + `<state>/matrix-token` +
|
||||||
`<state>/matrix-sdk-state` — so it is present exactly when that URL
|
`<state>/matrix-sdk-state`. It is the account a tool call acts as
|
||||||
is non-null, which on a real hive is always (hive-c0re renders it
|
when it omits `account`. It is an ordinary entry of this option
|
||||||
per agent). It is the account a tool call acts as when it omits
|
|
||||||
`account`. It is an ordinary entry of this option
|
|
||||||
like any other, so it shows up in the account list --- what you
|
like any other, so it shows up in the account list --- what you
|
||||||
add here are the *further* accounts (e.g. an external
|
add here are the *further* accounts (e.g. an external
|
||||||
public-matrix account). Its `tokenFile` stays pinned to
|
public-matrix account). Its `tokenFile` stays pinned to
|
||||||
|
|
@ -181,7 +160,8 @@ in
|
||||||
Leave it alone (the default) for the common single-account case:
|
Leave it alone (the default) for the common single-account case:
|
||||||
the agent then has only `main`. The whole set is serialized to the
|
the agent then has only `main`. The whole set is serialized to the
|
||||||
daemon's `HIVE_MATRIX_ACCOUNTS` environment variable, `main`
|
daemon's `HIVE_MATRIX_ACCOUNTS` environment variable, `main`
|
||||||
first.
|
first. Declaring extras requires
|
||||||
|
`services.hyperhive.agent.matrix.enable` (there is no `main` to extend otherwise).
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -205,26 +185,29 @@ in
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
assertions = [
|
assertions = [
|
||||||
# The "extras require `matrix.enable`" assertion that used to head this
|
# Extra matrix accounts only make sense alongside the hive-internal
|
||||||
# list is gone with the option: a non-empty account set is now what
|
# `main` account they extend, which exists only when matrix is
|
||||||
# enables matrix, so the condition it checked has become the definition
|
# enabled.
|
||||||
# of the thing it was checking. An operator declaring only an external
|
{
|
||||||
# account, with no hive homeserver, is a config this module now renders
|
assertion =
|
||||||
# rather than rejects — matrix on, no `main`.
|
config.services.hyperhive.agent.matrixAccounts == { }
|
||||||
#
|
|| config.services.hyperhive.agent.matrix.enable;
|
||||||
# `main` is not a forbidden key --- this module declares it
|
message =
|
||||||
|
"services.hyperhive.agent.matrixAccounts requires services.hyperhive.agent.matrix.enable = true "
|
||||||
|
+ "(the extras extend the hive-internal `main` account, which only "
|
||||||
|
+ "exists when matrix is enabled).";
|
||||||
|
}
|
||||||
|
# `main` is no longer a forbidden key --- this module declares it
|
||||||
# itself (see the `matrixAccounts.main` definition below), so the
|
# itself (see the `matrixAccounts.main` definition below), so the
|
||||||
# name must be allowed. What stays rejected is retargeting *its
|
# name must be allowed. What stays rejected is retargeting *its
|
||||||
# token file*: hive-c0re writes the hive-internal account's token
|
# token file*: hive-c0re writes the hive-internal account's token
|
||||||
# to `<state>/matrix-token` and nowhere else, so an override there
|
# to `<state>/matrix-token` and nowhere else, so an override there
|
||||||
# is an account that evaluates fine and then never restores. The
|
# is an account that evaluates fine and then never restores. The
|
||||||
# other two fields are free to override (a `mkDefault` each).
|
# other two fields are free to override (a `mkDefault` each).
|
||||||
#
|
|
||||||
# Guarded on `? main` rather than on an enable flag: `main` is absent
|
|
||||||
# whenever `matrix.url` is null, and an unguarded `.main.tokenFile`
|
|
||||||
# would throw on exactly those agents instead of passing vacuously.
|
|
||||||
{
|
{
|
||||||
assertion = !(accounts ? main) || accounts.main.tokenFile == "${stateDir}/matrix-token";
|
assertion =
|
||||||
|
!config.services.hyperhive.agent.matrix.enable
|
||||||
|
|| config.services.hyperhive.agent.matrixAccounts.main.tokenFile == "${stateDir}/matrix-token";
|
||||||
message =
|
message =
|
||||||
"services.hyperhive.agent.matrixAccounts.main.tokenFile must stay "
|
"services.hyperhive.agent.matrixAccounts.main.tokenFile must stay "
|
||||||
+ "\"${stateDir}/matrix-token\" --- that is where hive-c0re provisions the "
|
+ "\"${stateDir}/matrix-token\" --- that is where hive-c0re provisions the "
|
||||||
|
|
@ -239,7 +222,7 @@ in
|
||||||
# `matrix-catgirl-token`) is caught at build time, not silently.
|
# `matrix-catgirl-token`) is caught at build time, not silently.
|
||||||
{
|
{
|
||||||
assertion = lib.all (a: lib.hasPrefix "matrix-token" (baseNameOf a.tokenFile)) (
|
assertion = lib.all (a: lib.hasPrefix "matrix-token" (baseNameOf a.tokenFile)) (
|
||||||
lib.attrValues accounts
|
lib.attrValues config.services.hyperhive.agent.matrixAccounts
|
||||||
);
|
);
|
||||||
message =
|
message =
|
||||||
"every services.hyperhive.agent.matrixAccounts.<name>.tokenFile basename must start with "
|
"every services.hyperhive.agent.matrixAccounts.<name>.tokenFile basename must start with "
|
||||||
|
|
@ -247,7 +230,9 @@ in
|
||||||
+ "(matrix-token* in the agent's state dir) picks it up live. Offending: "
|
+ "(matrix-token* in the agent's state dir) picks it up live. Offending: "
|
||||||
+ lib.concatStringsSep ", " (
|
+ lib.concatStringsSep ", " (
|
||||||
lib.mapAttrsToList (n: a: "${n}=${baseNameOf a.tokenFile}") (
|
lib.mapAttrsToList (n: a: "${n}=${baseNameOf a.tokenFile}") (
|
||||||
lib.filterAttrs (_n: a: !lib.hasPrefix "matrix-token" (baseNameOf a.tokenFile)) accounts
|
lib.filterAttrs (
|
||||||
|
_n: a: !lib.hasPrefix "matrix-token" (baseNameOf a.tokenFile)
|
||||||
|
) config.services.hyperhive.agent.matrixAccounts
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
+ ".";
|
+ ".";
|
||||||
|
|
@ -261,17 +246,7 @@ in
|
||||||
# operator can retarget e.g. the homeserver without a
|
# operator can retarget e.g. the homeserver without a
|
||||||
# conflicting-definition error (the token file is pinned by an
|
# conflicting-definition error (the token file is pinned by an
|
||||||
# assertion above, since hive-c0re owns that path).
|
# assertion above, since hive-c0re owns that path).
|
||||||
#
|
services.hyperhive.agent.matrixAccounts = lib.mkIf config.services.hyperhive.agent.matrix.enable {
|
||||||
# ⚠️ Gated on the homeserver URL, and that gate is what keeps `matrixEnabled`
|
|
||||||
# from being trivially true for every agent in the hive. A `main` with no
|
|
||||||
# homeserver is an account the daemon can never log in as, so declaring one
|
|
||||||
# unconditionally would enable matrix everywhere and inject a tool surface
|
|
||||||
# backed by a permanently no-opping daemon. On a real hive hive-c0re renders
|
|
||||||
# this URL per agent (meta.rs's `FORWARDED_VAR_OPTIONS`), so the common case
|
|
||||||
# is still "every agent has `main`".
|
|
||||||
services.hyperhive.agent.matrixAccounts =
|
|
||||||
lib.mkIf (config.services.hyperhive.agent.matrix.url != null)
|
|
||||||
{
|
|
||||||
main = {
|
main = {
|
||||||
tokenFile = lib.mkDefault "${stateDir}/matrix-token";
|
tokenFile = lib.mkDefault "${stateDir}/matrix-token";
|
||||||
sessionDir = lib.mkDefault "${stateDir}/matrix-sdk-state";
|
sessionDir = lib.mkDefault "${stateDir}/matrix-sdk-state";
|
||||||
|
|
@ -283,7 +258,7 @@ in
|
||||||
# ./mcp.nix. `lib.mkDefault` so the operator's own agent.nix can
|
# ./mcp.nix. `lib.mkDefault` so the operator's own agent.nix can
|
||||||
# override it. Points at the daemon's own persistent
|
# override it. Points at the daemon's own persistent
|
||||||
# streamable-http listener — no stdio bridge, no per-turn spawn.
|
# streamable-http listener — no stdio bridge, no per-turn spawn.
|
||||||
services.hyperhive.agent.extraMcpServers = lib.mkIf matrixEnabled {
|
services.hyperhive.agent.extraMcpServers = lib.mkIf config.services.hyperhive.agent.matrix.enable {
|
||||||
matrix = lib.mkDefault {
|
matrix = lib.mkDefault {
|
||||||
type = "http";
|
type = "http";
|
||||||
url = "http://127.0.0.1:${toString config.services.hyperhive.agent.mcp.matrixHttpPort}/mcp";
|
url = "http://127.0.0.1:${toString config.services.hyperhive.agent.mcp.matrixHttpPort}/mcp";
|
||||||
|
|
@ -296,7 +271,7 @@ in
|
||||||
# signals on incoming room events via `/run/hive/mcp.sock`. See
|
# signals on incoming room events via `/run/hive/mcp.sock`. See
|
||||||
# `docs/agent-lifecycle/persistence.md::Matrix per-agent daemon + token-arrival
|
# `docs/agent-lifecycle/persistence.md::Matrix per-agent daemon + token-arrival
|
||||||
# trigger` for the first-boot-ordering rationale.
|
# trigger` for the first-boot-ordering rationale.
|
||||||
systemd.services.hive-matrix-daemon = lib.mkIf matrixEnabled {
|
systemd.services.hive-matrix-daemon = lib.mkIf config.services.hyperhive.agent.matrix.enable {
|
||||||
description = "long-running matrix-sdk Client + MCP daemon";
|
description = "long-running matrix-sdk Client + MCP daemon";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
before = [ "hive-agent.service" ];
|
before = [ "hive-agent.service" ];
|
||||||
|
|
@ -329,10 +304,7 @@ in
|
||||||
# synthesizing it from the per-agent paths only when this JSON
|
# synthesizing it from the per-agent paths only when this JSON
|
||||||
# carries no `main` --- which is how an agent whose harness
|
# carries no `main` --- which is how an agent whose harness
|
||||||
# predates this entry keeps working.
|
# predates this entry keeps working.
|
||||||
#
|
// lib.optionalAttrs (config.services.hyperhive.agent.matrixAccounts != { }) {
|
||||||
# Unconditional, not `optionalAttrs (accounts != {})`: a non-empty set is
|
|
||||||
# what generated this unit at all, so the guard could only ever be true.
|
|
||||||
// {
|
|
||||||
HIVE_MATRIX_ACCOUNTS = builtins.toJSON (
|
HIVE_MATRIX_ACCOUNTS = builtins.toJSON (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
name: a:
|
name: a:
|
||||||
|
|
@ -342,7 +314,7 @@ in
|
||||||
state_dir = a.sessionDir;
|
state_dir = a.sessionDir;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
|
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
|
||||||
) accounts
|
) config.services.hyperhive.agent.matrixAccounts
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
# Rasterized agent icon path for the daemon's avatar sync. Only set
|
# Rasterized agent icon path for the daemon's avatar sync. Only set
|
||||||
|
|
@ -375,7 +347,7 @@ in
|
||||||
# the daemon would exit 0 silently on first boot and the MCP
|
# the daemon would exit 0 silently on first boot and the MCP
|
||||||
# would have no backend until next restart. See
|
# would have no backend until next restart. See
|
||||||
# `docs/agent-lifecycle/persistence.md` (same section as above).
|
# `docs/agent-lifecycle/persistence.md` (same section as above).
|
||||||
systemd.paths.hive-matrix-daemon = lib.mkIf matrixEnabled {
|
systemd.paths.hive-matrix-daemon = lib.mkIf config.services.hyperhive.agent.matrix.enable {
|
||||||
description = "trigger hive-matrix-daemon when a matrix token appears";
|
description = "trigger hive-matrix-daemon when a matrix token appears";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
# `matrix-token*` (not just `matrix-token`) so a secondary
|
# `matrix-token*` (not just `matrix-token`) so a secondary
|
||||||
|
|
|
||||||
|
|
@ -273,8 +273,7 @@ in
|
||||||
# Auto-inject the built-in bash MCP server — always present, every
|
# Auto-inject the built-in bash MCP server — always present, every
|
||||||
# agent needs bash tools. `lib.mkDefault` so the operator's own
|
# agent needs bash tools. `lib.mkDefault` so the operator's own
|
||||||
# agent.nix can override the entry. (The matrix sibling lives in
|
# agent.nix can override the entry. (The matrix sibling lives in
|
||||||
# ./matrix.nix, gated on that agent having a
|
# ./matrix.nix, gated on services.hyperhive.agent.matrix.enable.) `hive-bash-daemon`
|
||||||
# services.hyperhive.agent.matrixAccounts entry at all.) `hive-bash-daemon`
|
|
||||||
# serves its MCP tools directly over streamable-http (no stdio bridge,
|
# serves its MCP tools directly over streamable-http (no stdio bridge,
|
||||||
# no round-trip socket) — see the `hive-bash-daemon` service below.
|
# no round-trip socket) — see the `hive-bash-daemon` service below.
|
||||||
services.hyperhive.agent.extraMcpServers.bash = lib.mkDefault {
|
services.hyperhive.agent.extraMcpServers.bash = lib.mkDefault {
|
||||||
|
|
@ -285,9 +284,8 @@ in
|
||||||
|
|
||||||
# Auto-inject the subagent MCP server — default-on for every agent for
|
# Auto-inject the subagent MCP server — default-on for every agent for
|
||||||
# now (operator's call: "default on for now, should be a capability
|
# now (operator's call: "default on for now, should be a capability
|
||||||
# later" — unconditional, where `matrix.nix`'s sibling entry appears only
|
# later" — not gated behind an enable option yet, unlike `matrix.nix`'s
|
||||||
# for an agent that has a matrix account to serve).
|
# pattern). `lib.mkDefault` so an agent.nix can still override/disable
|
||||||
# `lib.mkDefault` so an agent.nix can still override/disable
|
|
||||||
# the entry in the meantime.
|
# the entry in the meantime.
|
||||||
services.hyperhive.agent.extraMcpServers.subagent = lib.mkDefault {
|
services.hyperhive.agent.extraMcpServers.subagent = lib.mkDefault {
|
||||||
type = "http";
|
type = "http";
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,10 @@
|
||||||
[ "hyperhive" "logs" "queryUrl" ]
|
[ "hyperhive" "logs" "queryUrl" ]
|
||||||
[ "services" "hyperhive" "agent" "logs" "queryUrl" ]
|
[ "services" "hyperhive" "agent" "logs" "queryUrl" ]
|
||||||
)
|
)
|
||||||
# `matrix.enable` is deliberately absent from this list: the option it
|
(lib.mkRenamedOptionModule
|
||||||
# renamed to no longer exists, and a rename pointing at an undeclared
|
[ "hyperhive" "matrix" "enable" ]
|
||||||
# option breaks evaluation for everyone. Both of its spellings are
|
[ "services" "hyperhive" "agent" "matrix" "enable" ]
|
||||||
# `mkRemovedOptionModule` in ./default.nix instead.
|
)
|
||||||
(lib.mkRenamedOptionModule
|
(lib.mkRenamedOptionModule
|
||||||
[ "hyperhive" "matrix" "url" ]
|
[ "hyperhive" "matrix" "url" ]
|
||||||
[ "services" "hyperhive" "agent" "matrix" "url" ]
|
[ "services" "hyperhive" "agent" "matrix" "url" ]
|
||||||
|
|
|
||||||
|
|
@ -611,29 +611,6 @@ let
|
||||||
# RAM percentage and so hands the module no byte count to size against.
|
# RAM percentage and so hands the module no byte count to size against.
|
||||||
agentCapped = agent { claudeMemoryMaxBytes = 8589934592; };
|
agentCapped = agent { claudeMemoryMaxBytes = 8589934592; };
|
||||||
agentUncapped = agent { };
|
agentUncapped = agent { };
|
||||||
|
|
||||||
# Matrix's enable signal, which is the account set itself — there is no
|
|
||||||
# `matrix.enable` option left to read. Three arms, because the property has
|
|
||||||
# three distinct shapes and only one of them is the common case:
|
|
||||||
#
|
|
||||||
# - a homeserver URL, which is what the module turns into a `main` account;
|
|
||||||
# - neither URL nor operator account, the state that replaced
|
|
||||||
# `matrix.enable = false`. ⚠️ **This is the arm that matters.** `main` is
|
|
||||||
# declared by the module itself, so "any account declared" would be
|
|
||||||
# trivially true — and matrix would render for every agent in every hive —
|
|
||||||
# the moment that declaration stops being gated on the URL. Nothing else in
|
|
||||||
# this suite would notice;
|
|
||||||
# - an operator account carrying its own homeserver and no hive one, which is
|
|
||||||
# matrix on with no `main` at all.
|
|
||||||
agentMatrix = agent { matrix.url = "https://chat.t.local"; };
|
|
||||||
agentNoMatrix = agent { };
|
|
||||||
agentMatrixExternalOnly = agent {
|
|
||||||
matrixAccounts.ccc = {
|
|
||||||
tokenFile = "/agents/a1/state/matrix-token-ccc";
|
|
||||||
sessionDir = "/agents/a1/state/matrix-sdk-state-ccc";
|
|
||||||
homeserver = "https://matrix.example.invalid";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
agentHarness = machine: machine.systemd.services.hive-agent;
|
agentHarness = machine: machine.systemd.services.hive-agent;
|
||||||
agentSubagentDaemon = machine: machine.systemd.services.hive-subagent-daemon;
|
agentSubagentDaemon = machine: machine.systemd.services.hive-subagent-daemon;
|
||||||
agentSettings = machine: machine.services.opentelemetry-collector.settings;
|
agentSettings = machine: machine.services.opentelemetry-collector.settings;
|
||||||
|
|
@ -1816,62 +1793,6 @@ let
|
||||||
name = "one subagent losing the OOM draw does not stop the daemon";
|
name = "one subagent losing the OOM draw does not stop the daemon";
|
||||||
ok = (agentSubagentDaemon agentUncapped).serviceConfig.OOMPolicy == "continue";
|
ok = (agentSubagentDaemon agentUncapped).serviceConfig.OOMPolicy == "continue";
|
||||||
}
|
}
|
||||||
{
|
|
||||||
# A homeserver URL is the whole input: from it the module derives the
|
|
||||||
# hive-internal `main` account, and from a non-empty account set the three
|
|
||||||
# things that used to hang off `matrix.enable`.
|
|
||||||
name = "an agent with a homeserver gets a main account and the matrix units";
|
|
||||||
ok =
|
|
||||||
let
|
|
||||||
a = agentMatrix.services.hyperhive.agent.matrixAccounts;
|
|
||||||
in
|
|
||||||
lib.attrNames a == [ "main" ]
|
|
||||||
&& a.main.tokenFile == "/agents/a1/state/matrix-token"
|
|
||||||
&& a.main.homeserver == "https://chat.t.local"
|
|
||||||
&& agentMatrix.systemd.services ? hive-matrix-daemon
|
|
||||||
&& agentMatrix.systemd.paths ? hive-matrix-daemon
|
|
||||||
&& agentMatrix.services.hyperhive.agent.extraMcpServers ? matrix;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
# The absence arm, and the reason the enable signal is not vacuous. An
|
|
||||||
# agent the hive gave no homeserver, whose operator declared nothing, must
|
|
||||||
# come out with an EMPTY account set — not a `main` that can never log in
|
|
||||||
# — and therefore with none of the three. Assert the emptiness itself and
|
|
||||||
# not just the units: it is the account set that is load-bearing now, and
|
|
||||||
# a `main` sneaking back in is the regression this case exists to name.
|
|
||||||
name = "an agent with no homeserver and no declared account gets no matrix at all";
|
|
||||||
ok =
|
|
||||||
agentNoMatrix.services.hyperhive.agent.matrixAccounts == { }
|
|
||||||
&& !(agentNoMatrix.systemd.services ? hive-matrix-daemon)
|
|
||||||
&& !(agentNoMatrix.systemd.paths ? hive-matrix-daemon)
|
|
||||||
&& !(agentNoMatrix.services.hyperhive.agent.extraMcpServers ? matrix);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
# Matrix without a hive homeserver: one operator account, its own
|
|
||||||
# homeserver, no `main`. Under the deleted `matrix.enable` this config was
|
|
||||||
# an assertion failure ("extras require enable") even though every account
|
|
||||||
# in it was complete; the account set being the signal is what makes it
|
|
||||||
# expressible, and the serialized env var is where that has to show up.
|
|
||||||
name = "an external-only account enables matrix with no main entry";
|
|
||||||
ok =
|
|
||||||
let
|
|
||||||
accts = agentMatrixExternalOnly.services.hyperhive.agent.matrixAccounts;
|
|
||||||
env = agentMatrixExternalOnly.systemd.services.hive-matrix-daemon.environment;
|
|
||||||
in
|
|
||||||
lib.attrNames accts == [ "ccc" ]
|
|
||||||
&& !(accts ? main)
|
|
||||||
&&
|
|
||||||
builtins.fromJSON env.HIVE_MATRIX_ACCOUNTS == [
|
|
||||||
{
|
|
||||||
name = "ccc";
|
|
||||||
token_file = "/agents/a1/state/matrix-token-ccc";
|
|
||||||
state_dir = "/agents/a1/state/matrix-sdk-state-ccc";
|
|
||||||
homeserver = "https://matrix.example.invalid";
|
|
||||||
}
|
|
||||||
]
|
|
||||||
# No hive homeserver, so nothing may claim one.
|
|
||||||
&& !(env ? HIVE_MATRIX_URL);
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
# The doctrine three glue files state, as a property a rewrite has to
|
# The doctrine three glue files state, as a property a rewrite has to
|
||||||
# keep: a client is defined by holding a certificate the store accepts,
|
# keep: a client is defined by holding a certificate the store accepts,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue