hive-matrix-mcp: read the main account's token from the store too
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.
This commit is contained in:
parent
2776e121e5
commit
ab153bda2f
9 changed files with 670 additions and 85 deletions
|
|
@ -14,12 +14,21 @@
|
|||
}:
|
||||
let
|
||||
userName = config.services.hyperhive.agent.user.name;
|
||||
# This agent's own state dir, where hive-c0re provisions the
|
||||
# hive-internal account's token (`matrix-token`) and the daemon keeps
|
||||
# its matrix-sdk store. Shared by the `main` account entry below and
|
||||
# the path-watcher glob at the bottom of this file.
|
||||
# This agent's own state dir, where a hive used to write the `main`
|
||||
# account's token (`matrix-token`, now the store's fallback) and the
|
||||
# daemon keeps 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;
|
||||
# This agent's own identity at the swarm secret store (./bao.nix). The daemon
|
||||
# reads each account's token from the store itself, `main` included (the
|
||||
# swarm mints that one), so the store's coordinates belong on its unit. The
|
||||
# same three credential ids ./bao.nix and ./forge-token.nix load.
|
||||
baoCfg = config.services.hyperhive.agent.bao;
|
||||
storeConfigured = baoCfg.addr != null;
|
||||
certCredential = "hive-agent-bao-cert";
|
||||
keyCredential = "hive-agent-bao-key";
|
||||
serverCaCredential = "hive-agent-bao-server-ca";
|
||||
# **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.
|
||||
|
|
@ -90,8 +99,9 @@ in
|
|||
Path to this account's bearer-token file. The daemon reads
|
||||
the token from here to restore the matrix session; how the
|
||||
file gets populated is the provisioner's concern
|
||||
(hive-c0re for the hive-internal `main` account, an
|
||||
operator-supplied secret for an external one). The daemon
|
||||
(for `main`, a file only when the store has no token: the
|
||||
swarm mints `main` into the store; an operator-supplied
|
||||
secret for an external one). The daemon
|
||||
skips an extra account whose token file is absent, and
|
||||
exits cleanly to wait on the path watcher when `main`'s is.
|
||||
'';
|
||||
|
|
@ -143,7 +153,7 @@ in
|
|||
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
|
||||
watcher restarts it the moment a token file appears
|
||||
(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,
|
||||
|
|
@ -173,8 +183,8 @@ in
|
|||
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
|
||||
`<state>/matrix-token` (an assertion; that is the one path
|
||||
hive-c0re provisions the hive-internal token to), and the
|
||||
`<state>/matrix-token` (an assertion; the file the daemon falls back
|
||||
to when the store has no `main` token), and the
|
||||
dashboard's link-account route refuses to create an account named
|
||||
`main` --- the entry belongs to the module, not to a provisioner.
|
||||
|
||||
|
|
@ -215,8 +225,8 @@ in
|
|||
# `main` is not 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 `<state>/matrix-token` and nowhere else, so an override there
|
||||
# token file*: the daemon's file fallback for `main` is
|
||||
# `<state>/matrix-token` and nothing 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).
|
||||
#
|
||||
|
|
@ -227,8 +237,8 @@ in
|
|||
assertion = !(accounts ? main) || accounts.main.tokenFile == "${stateDir}/matrix-token";
|
||||
message =
|
||||
"services.hyperhive.agent.matrixAccounts.main.tokenFile must stay "
|
||||
+ "\"${stateDir}/matrix-token\" --- that is where hive-c0re provisions the "
|
||||
+ "hive-internal account's token. Declare a separate account instead of "
|
||||
+ "\"${stateDir}/matrix-token\" --- that is the file the daemon falls back to for the "
|
||||
+ "main account's token. Declare a separate account instead of "
|
||||
+ "pointing `main` elsewhere.";
|
||||
}
|
||||
# Token files must land at the `matrix-token*` name the daemon
|
||||
|
|
@ -260,7 +270,7 @@ in
|
|||
# so the one it always has belongs in it. `mkDefault` per field so an
|
||||
# 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).
|
||||
# assertion above, since the daemon's fallback reads 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
|
||||
|
|
@ -309,6 +319,20 @@ in
|
|||
HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock";
|
||||
RUST_LOG = "info";
|
||||
}
|
||||
# The store's coordinates and which agent this is, so the daemon reads
|
||||
# `swarm/agents/<agent>/matrix/<account>` as itself. The NAME and not a
|
||||
# path: `swarm_secret_client` builds both the path and the cert-auth role
|
||||
# from it. Paths only — `%d` is this unit's own credentials directory.
|
||||
// lib.optionalAttrs storeConfigured {
|
||||
HIVE_AGENT_NAME = userName;
|
||||
BAO_ADDR = baoCfg.addr;
|
||||
BAO_CLIENT_CERT = "%d/${certCredential}";
|
||||
BAO_CLIENT_KEY = "%d/${keyCredential}";
|
||||
# Named even when no CA was delivered: a bare `LoadCredential=` is
|
||||
# non-fatal when absent, and the daemon treats a missing or empty file
|
||||
# as "use the container's own trust store".
|
||||
BAO_CACERT = "%d/${serverCaCredential}";
|
||||
}
|
||||
# Homeserver URL. hive-c0re writes this option per agent from the
|
||||
# hive's own gateway URL (`gatewayHost`'s vhost, `chat.<swarm-domain>`
|
||||
# by default — agents run in a private
|
||||
|
|
@ -357,7 +381,8 @@ in
|
|||
# `on-failure`, not `always`: the daemon deliberately exits 0
|
||||
# (a clean, non-failure exit) when no token is provisioned yet
|
||||
# (see the module doc above) — the `systemd.paths` watcher
|
||||
# below re-fires it the moment hive-c0re provisions one,
|
||||
# below re-fires it the moment a token file appears (the timer
|
||||
# above, for a token in the store),
|
||||
# instead of `always` busy-looping every `RestartSec` until
|
||||
# then. Once a token exists this is no different from
|
||||
# `hive-bash-daemon`'s reasoning (a down window loses the MCP
|
||||
|
|
@ -367,11 +392,33 @@ in
|
|||
RestartSec = 5;
|
||||
User = userName;
|
||||
Group = userName;
|
||||
}
|
||||
# This agent's own certificate, imported by id so systemd materialises it
|
||||
# under this unit's `User=` — the same bare form ./forge-token.nix uses.
|
||||
// lib.optionalAttrs storeConfigured {
|
||||
LoadCredential = [
|
||||
certCredential
|
||||
keyCredential
|
||||
serverCaCredential
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Re-fire the daemon when the matrix token appears (hive-c0re
|
||||
# provisions it after agent containers come up). Without this
|
||||
# Re-start the daemon while it is down, when its token lives in the store.
|
||||
# The path unit below only sees files, and a token the swarm mints or
|
||||
# replaces in the store changes no file: a daemon that exited on a missing
|
||||
# or replaced token would otherwise stay down until the container restarts.
|
||||
# Relative to the daemon's last exit, so it never fires while the daemon
|
||||
# runs; five minutes is the swarm's own re-mint cadence.
|
||||
systemd.timers.hive-matrix-daemon = lib.mkIf (matrixEnabled && storeConfigured) {
|
||||
description = "re-start hive-matrix-daemon while it is down, to re-read its token from the store";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig.OnUnitInactiveSec = "5min";
|
||||
};
|
||||
|
||||
# Re-fire the daemon when the matrix token appears (a token
|
||||
# file: an extra account the hive delivers, or a `main` from before the
|
||||
# swarm minted it). Without this
|
||||
# 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).
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ let
|
|||
;
|
||||
})
|
||||
agent
|
||||
agentWith
|
||||
runGroup
|
||||
;
|
||||
|
||||
|
|
@ -45,6 +46,15 @@ let
|
|||
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
|
||||
|
|
@ -102,6 +112,69 @@ let
|
|||
# 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue