The daemon reads each account's token from `swarm/agents/<agent>/matrix/` as the agent itself, inside its own container, and falls back to the file only when the store has none. This is #4519's read, without its `main` carve-out: the swarm now mints `main` there and no hive writes the file. The daemon unit gets the agent's store identity, spelled the way forge-token.nix spells it. A timer re-starts it while it is down: a token the swarm mints or replaces in the store changes no file, so the path watcher never fires for it, and a daemon that exited on a replaced token would otherwise stay down until the container restarts.
180 lines
7.4 KiB
Nix
180 lines
7.4 KiB
Nix
# `checks.module-eval-agent-matrix` — see ./lib.nix for the shared
|
|
# rationale (why this suite exists, naming convention, "evaluates
|
|
# not executes").
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
agent
|
|
agentWith
|
|
runGroup
|
|
;
|
|
|
|
# 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";
|
|
};
|
|
};
|
|
# The daemon that reads its tokens from the store, `main` included. Paired
|
|
# with `agentMatrix` above — same daemon, no store — so each case below can
|
|
# tell "carries the store's coordinates" from "every matrix daemon does".
|
|
agentMatrixBao = agentWith {
|
|
services.hyperhive.agent.bao.addr = "https://bao.t.local:8200";
|
|
services.hyperhive.agent.matrix.url = "https://chat.t.local";
|
|
};
|
|
|
|
daemon = machine: machine.systemd.services.hive-matrix-daemon;
|
|
cases = [
|
|
{
|
|
# 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 daemon reads this agent's tokens from the store ITSELF, as itself,
|
|
# from inside this container — `main` too, since the swarm mints it and
|
|
# no hive does. So it needs the same identity ./agent-forge-bao.nix's
|
|
# fetch carries, in its own credentials directory.
|
|
name = "the matrix daemon carries this agent's own store identity";
|
|
ok =
|
|
let
|
|
u = daemon agentMatrixBao;
|
|
in
|
|
u.serviceConfig.LoadCredential == [
|
|
"hive-agent-bao-cert"
|
|
"hive-agent-bao-key"
|
|
"hive-agent-bao-server-ca"
|
|
]
|
|
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
|
|
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
|
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
|
|
}
|
|
{
|
|
# The agent's name, and nothing derived from it: the daemon builds its
|
|
# store path and its cert-auth role from this one string.
|
|
name = "the matrix daemon is told which agent it is and not where its credentials live";
|
|
ok =
|
|
let
|
|
e = (daemon agentMatrixBao).environment;
|
|
in
|
|
e.HIVE_AGENT_NAME == agentMatrixBao.services.hyperhive.agent.user.name
|
|
&& !(lib.any (lib.hasInfix "swarm/agents") (lib.attrValues e));
|
|
}
|
|
{
|
|
# 🩸 A secret is a path: every `BAO_*` entry is the store's address or a
|
|
# file under this unit's own credentials directory, never bytes in an
|
|
# environment `/proc/<pid>/environ` publishes.
|
|
name = "the matrix daemon is handed store paths and never store values";
|
|
ok =
|
|
let
|
|
store = lib.filterAttrs (n: _: lib.hasPrefix "BAO_" n) (daemon agentMatrixBao).environment;
|
|
in
|
|
store != { }
|
|
&& lib.all (n: n == "BAO_ADDR" || lib.hasPrefix "%d/" store.${n}) (lib.attrNames store);
|
|
}
|
|
{
|
|
# A token the swarm mints or replaces in the store changes no file, so
|
|
# the path watcher never sees it. The timer is what re-starts a daemon
|
|
# that exited on a missing or replaced token.
|
|
name = "a store-backed matrix daemon is re-started while it is down";
|
|
ok =
|
|
agentMatrixBao.systemd.timers.hive-matrix-daemon.timerConfig.OnUnitInactiveSec or null == "5min";
|
|
}
|
|
{
|
|
# The absence arm for the four above: with no store, no identity, no
|
|
# timer, and the file is the whole mechanism.
|
|
name = "a matrix daemon on an agent with no store declares no identity and no timer";
|
|
ok =
|
|
let
|
|
u = daemon agentMatrix;
|
|
in
|
|
!(u.serviceConfig ? LoadCredential)
|
|
&& !(u.environment ? BAO_ADDR)
|
|
&& !(u.environment ? HIVE_AGENT_NAME)
|
|
&& !(agentMatrix.systemd.timers ? hive-matrix-daemon);
|
|
}
|
|
];
|
|
in
|
|
runGroup "agent-matrix" cases
|