diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 1824171c..e93d36c6 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -546,15 +546,9 @@ the body + marker check at that point. process per agent. Serves its MCP tools directly over streamable-http (`services.hyperhive.agent.mcp.matrixHttpPort`, no stdio bridge — same shape as `hive-bash-daemon`), emits hyperhive wake signals -on incoming room events via `/run/hive/mcp.sock`. Conditional on the -agent having at least one `services.hyperhive.agent.matrixAccounts` -entry — there is no separate enable switch, and the same condition gates -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. +on incoming room events via `/run/hive/mcp.sock`. Conditional on +`services.hyperhive.agent.matrix.enable` (which both the daemon AND the +autoinjected `extraMcpServers.matrix` entry read). **First-boot ordering**: hive-c0re provisions the matrix token AFTER agent containers come up. Without the path-trigger sibling diff --git a/docs/tools/matrix.md b/docs/tools/matrix.md index 87f2ea77..b6ea6238 100644 --- a/docs/tools/matrix.md +++ b/docs/tools/matrix.md @@ -2,14 +2,10 @@ ## 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 +When `services.hyperhive.agent.matrix.enable = true` and the host-level matrix +tuwunel is configured, 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 host-level -matrix tuwunel is configured. Tools land as `mcp__matrix__`: +see Architecture below). Tools land as `mcp__matrix__`: ### Messaging @@ -75,8 +71,7 @@ room you haven't read yet. `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 +external-facing account alongside the internal one. 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 + @@ -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 `/matrix-token` (hive-c0re provisions it there), and the dashboard's link-account route refuses to create an account by that -name. - -`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. +name. Declaring extras requires +`services.hyperhive.agent.matrix.enable = true`. Every matrix tool above takes an optional `account` parameter (a name from this map) to act as that identity instead of the primary one. diff --git a/nix/agent-modules/default.nix b/nix/agent-modules/default.nix index 2ea0a867..fea45d18 100644 --- a/nix/agent-modules/default.nix +++ b/nix/agent-modules/default.nix @@ -18,25 +18,6 @@ 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 = [ ./agent-service.nix @@ -66,20 +47,6 @@ in The built-in Bash tool is fully disabled; agents use 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 { diff --git a/nix/agent-modules/matrix.nix b/nix/agent-modules/matrix.nix index 7da62f78..33f88053 100644 --- a/nix/agent-modules/matrix.nix +++ b/nix/agent-modules/matrix.nix @@ -3,9 +3,6 @@ # hive-matrix-daemon (serves its MCP tools directly over # streamable-http), its token-arrival path trigger, and the # 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, lib, @@ -19,21 +16,6 @@ let # its matrix-sdk store. Shared by the `main` account entry below and # the path-watcher glob at the bottom of this file. 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 # 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). @@ -45,6 +27,40 @@ let ''; 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 + `/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] in : …`) + 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 { type = lib.types.nullOr lib.types.str; default = null; @@ -64,18 +80,10 @@ in different host from the agents, and inside an agent's network namespace `localhost` reaches the agent rather than the 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. - - `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 MCP tools target via their `account` argument. - **This set is also the enable signal for per-agent matrix** — - there is no separate boolean. A non-empty set means the harness: - - - 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] in : …`) - 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 + The **hive-internal account is always present and is the primary**: + it is named `main`, and this module declares it for you (when + `services.hyperhive.agent.matrix.enable` is set) from `services.hyperhive.agent.matrix.url` + `/matrix-token` + - `/matrix-sdk-state` — so it is present exactly when that URL - is non-null, which on a real hive is always (hive-c0re renders it - per agent). It is the account a tool call acts as when it omits - `account`. It is an ordinary entry of this option + `/matrix-sdk-state`. 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 add here are the *further* accounts (e.g. an external public-matrix account). Its `tokenFile` stays pinned to @@ -181,7 +160,8 @@ in Leave it alone (the default) for the common single-account case: the agent then has only `main`. The whole set is serialized to the 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 = { assertions = [ - # The "extras require `matrix.enable`" assertion that used to head this - # list is gone with the option: a non-empty account set is now what - # enables matrix, so the condition it checked has become the definition - # of the thing it was checking. An operator declaring only an external - # account, with no hive homeserver, is a config this module now renders - # rather than rejects — matrix on, no `main`. - # - # `main` is not a forbidden key --- this module declares it + # Extra matrix accounts only make sense alongside the hive-internal + # `main` account they extend, which exists only when matrix is + # enabled. + { + assertion = + config.services.hyperhive.agent.matrixAccounts == { } + || config.services.hyperhive.agent.matrix.enable; + 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 # name must be allowed. What stays rejected is retargeting *its # token file*: hive-c0re writes the hive-internal account's token # to `/matrix-token` and nowhere else, so an override there # is an account that evaluates fine and then never restores. The # 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 = "services.hyperhive.agent.matrixAccounts.main.tokenFile must stay " + "\"${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. { assertion = lib.all (a: lib.hasPrefix "matrix-token" (baseNameOf a.tokenFile)) ( - lib.attrValues accounts + lib.attrValues config.services.hyperhive.agent.matrixAccounts ); message = "every services.hyperhive.agent.matrixAccounts..tokenFile basename must start with " @@ -247,7 +230,9 @@ in + "(matrix-token* in the agent's state dir) picks it up live. Offending: " + lib.concatStringsSep ", " ( 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,29 +246,19 @@ in # operator can retarget e.g. the homeserver without a # conflicting-definition error (the token file is pinned by an # assertion above, since hive-c0re owns that path). - # - # ⚠️ 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 = { - tokenFile = lib.mkDefault "${stateDir}/matrix-token"; - sessionDir = lib.mkDefault "${stateDir}/matrix-sdk-state"; - homeserver = lib.mkDefault config.services.hyperhive.agent.matrix.url; - }; - }; + services.hyperhive.agent.matrixAccounts = lib.mkIf config.services.hyperhive.agent.matrix.enable { + main = { + tokenFile = lib.mkDefault "${stateDir}/matrix-token"; + sessionDir = lib.mkDefault "${stateDir}/matrix-sdk-state"; + homeserver = lib.mkDefault config.services.hyperhive.agent.matrix.url; + }; + }; # Auto-inject the matrix MCP entry alongside the bash entry from # ./mcp.nix. `lib.mkDefault` so the operator's own agent.nix can # override it. Points at the daemon's own persistent # 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 { type = "http"; 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 # `docs/agent-lifecycle/persistence.md::Matrix per-agent daemon + token-arrival # 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"; wantedBy = [ "multi-user.target" ]; before = [ "hive-agent.service" ]; @@ -329,10 +304,7 @@ in # synthesizing it from the per-agent paths only when this JSON # carries no `main` --- which is how an agent whose harness # predates this entry keeps working. - # - # Unconditional, not `optionalAttrs (accounts != {})`: a non-empty set is - # what generated this unit at all, so the guard could only ever be true. - // { + // lib.optionalAttrs (config.services.hyperhive.agent.matrixAccounts != { }) { HIVE_MATRIX_ACCOUNTS = builtins.toJSON ( lib.mapAttrsToList ( name: a: @@ -342,7 +314,7 @@ in state_dir = a.sessionDir; } // 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 @@ -375,7 +347,7 @@ in # the daemon would exit 0 silently on first boot and the MCP # would have no backend until next restart. See # `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"; wantedBy = [ "multi-user.target" ]; # `matrix-token*` (not just `matrix-token`) so a secondary diff --git a/nix/agent-modules/mcp.nix b/nix/agent-modules/mcp.nix index 094ec4ec..0144ff8f 100644 --- a/nix/agent-modules/mcp.nix +++ b/nix/agent-modules/mcp.nix @@ -273,8 +273,7 @@ in # Auto-inject the built-in bash MCP server — always present, every # agent needs bash tools. `lib.mkDefault` so the operator's own # agent.nix can override the entry. (The matrix sibling lives in - # ./matrix.nix, gated on that agent having a - # services.hyperhive.agent.matrixAccounts entry at all.) `hive-bash-daemon` + # ./matrix.nix, gated on services.hyperhive.agent.matrix.enable.) `hive-bash-daemon` # serves its MCP tools directly over streamable-http (no stdio bridge, # no round-trip socket) — see the `hive-bash-daemon` service below. services.hyperhive.agent.extraMcpServers.bash = lib.mkDefault { @@ -285,9 +284,8 @@ in # Auto-inject the subagent MCP server — default-on for every agent for # now (operator's call: "default on for now, should be a capability - # later" — unconditional, where `matrix.nix`'s sibling entry appears only - # for an agent that has a matrix account to serve). - # `lib.mkDefault` so an agent.nix can still override/disable + # later" — not gated behind an enable option yet, unlike `matrix.nix`'s + # pattern). `lib.mkDefault` so an agent.nix can still override/disable # the entry in the meantime. services.hyperhive.agent.extraMcpServers.subagent = lib.mkDefault { type = "http"; diff --git a/nix/agent-modules/renamed-options.nix b/nix/agent-modules/renamed-options.nix index ef6e025b..cbf6d554 100644 --- a/nix/agent-modules/renamed-options.nix +++ b/nix/agent-modules/renamed-options.nix @@ -119,10 +119,10 @@ [ "hyperhive" "logs" "queryUrl" ] [ "services" "hyperhive" "agent" "logs" "queryUrl" ] ) - # `matrix.enable` is deliberately absent from this list: the option it - # renamed to no longer exists, and a rename pointing at an undeclared - # option breaks evaluation for everyone. Both of its spellings are - # `mkRemovedOptionModule` in ./default.nix instead. + (lib.mkRenamedOptionModule + [ "hyperhive" "matrix" "enable" ] + [ "services" "hyperhive" "agent" "matrix" "enable" ] + ) (lib.mkRenamedOptionModule [ "hyperhive" "matrix" "url" ] [ "services" "hyperhive" "agent" "matrix" "url" ] diff --git a/nix/module-eval.nix b/nix/module-eval.nix index cb28baec..b0b26984 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -611,29 +611,6 @@ let # RAM percentage and so hands the module no byte count to size against. agentCapped = agent { claudeMemoryMaxBytes = 8589934592; }; 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; agentSubagentDaemon = machine: machine.systemd.services.hive-subagent-daemon; agentSettings = machine: machine.services.opentelemetry-collector.settings; @@ -1816,62 +1793,6 @@ let name = "one subagent losing the OOM draw does not stop the daemon"; 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 # keep: a client is defined by holding a certificate the store accepts,