Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/agent-matrix.nix
atlas c6717bccb0 agents: re-pull the agent's own bao mTLS leaf on a timer
swarm-controller re-issues an agent's store leaf at half its validity, but
the running container kept presenting the copy its hive delivered at
start, so a re-issued leaf reached the agent only on its next restart.

hive-agent-bao-identity now seeds a live copy of the pair in its own
runtime directory from the delivered credentials, and re-runs every 10
minutes: it logs in with the live leaf, reads its own
swarm/agents/<agent>/bao-mtls (a read render_agent already grants), and
when the leaf changed, logs in with the new pair and swaps it in only if
the store accepts it.

Every unit in the container that logs in to the store presents the live
pair: hive-agent (queue-identity.nix), hive-matrix-daemon,
hive-subagent-daemon, hive-agent-forge-token, hive-agent-forge-accounts
and hive-agent-github-token. They keep loading the server CA as a
credential. The long-running daemons are ordered after the refresher so
the live pair exists when they start.

It renews only while the held leaf is still valid: the login that reads
the new leaf is made with the current one.

Refs #5017
2026-10-11 10:00:43 +02:00

177 lines
7.5 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
# matrix pieces: daemon unit, path watcher and injected MCP entry.
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: the live pair, and the server CA from its credentials.
name = "the matrix daemon carries this agent's own store identity";
ok =
let
u = daemon agentMatrixBao;
in
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key"
&& builtins.elem "hive-agent-bao-identity.service" u.after;
}
{
# 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