matrixAccounts: hive 'main' is implicit primary, option declares extras only

This commit is contained in:
damocles 2026-06-15 21:09:15 +02:00 committed by mara
commit 36fb041c55
2 changed files with 79 additions and 115 deletions

View file

@ -309,11 +309,9 @@ in
description = ''
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 in-hive accounts, an operator-supplied secret for an
external account). The daemon skips an account whose token
file is absent (the primary account being absent makes the
daemon exit cleanly until it appears).
file gets populated is the provisioner's concern (an
operator-supplied secret for an external account). The
daemon skips an extra account whose token file is absent.
'';
};
sessionDir = lib.mkOption {
@ -342,10 +340,6 @@ in
default = { };
example = lib.literalExpression ''
{
main = {
tokenFile = "/agents/dmatrix/state/matrix-token";
sessionDir = "/agents/dmatrix/state/matrix-sdk-state";
};
ccc = {
tokenFile = "/agents/dmatrix/state/matrix-token-ccc";
sessionDir = "/agents/dmatrix/state/matrix-sdk-state-ccc";
@ -354,35 +348,27 @@ in
}
'';
description = ''
Declare multiple matrix accounts served by a single
Declare *additional* matrix accounts served by the single
`hive-matrix-daemon` (one matrix-sdk Client + sync loop each),
replacing the wasteful "one MCP server + daemon per account"
pattern. The attribute name keys each account (unique by
construction) and is the handle the matrix MCP tools target via
their `account` argument; omitting `account` on a tool call selects
`hyperhive.matrixPrimaryAccount`.
beyond the agent's built-in hive-internal account. This replaces
the wasteful "one MCP server + daemon per account" pattern. The
attribute name keys each account (unique by construction) and is
the handle the matrix MCP tools target via their `account`
argument.
The **hive-internal account is always present and is the primary**:
it is named `main`, synthesized by the daemon from
`hyperhive.matrix.url` + `<state>/matrix-token` +
`<state>/matrix-sdk-state`, and is the account a tool call acts as
when it omits `account`. You never declare it here --- this option
is only for the extras (e.g. an external public-matrix account).
Leave empty (the default) for the common single-account case: the
daemon then synthesizes one account from `hyperhive.matrix.url` +
`<state>/matrix-token` + `<state>/matrix-sdk-state`, so existing
agents need no change. When non-empty, the set is serialized to the
daemon's `HIVE_MATRIX_ACCOUNTS` environment variable
(primary-account-first) and the legacy single-account fallback is
bypassed.
'';
};
options.hyperhive.matrixPrimaryAccount = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "main";
description = ''
Which `hyperhive.matrixAccounts` key is the agent's primary
account --- the one the matrix MCP tools act as when a tool call
omits its `account` argument. May be left null when exactly one
account is declared (that sole account is then primary); it is
required (asserted) when more than one account is declared. Ignored
when `matrixAccounts` is empty.
agent then has only `main`, exactly as before. When non-empty, the
extras are serialized to the daemon's `HIVE_MATRIX_ACCOUNTS`
environment variable and the daemon appends them after `main`.
Requires `hyperhive.matrix.enable` (there is no `main` to extend
otherwise).
'';
};
@ -774,27 +760,15 @@ in
assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon);
message = "hyperhive.icon must point to an .svg file";
}
# With more than one matrix account, the primary must be named
# explicitly --- there is no inherent order in the attrset to pick
# one from.
# Extra matrix accounts only make sense alongside the hive-internal
# `main` account they extend, which exists only when matrix is
# enabled.
{
assertion =
builtins.length (builtins.attrNames config.hyperhive.matrixAccounts) <= 1
|| config.hyperhive.matrixPrimaryAccount != null;
assertion = config.hyperhive.matrixAccounts == { } || config.hyperhive.matrix.enable;
message =
"hyperhive.matrixPrimaryAccount must be set when more than one "
+ "hyperhive.matrixAccounts entry is declared (it selects the account "
+ "used when a matrix tool call omits `account`).";
}
# When set, the primary must name an actual declared account.
{
assertion =
config.hyperhive.matrixPrimaryAccount == null
|| builtins.hasAttr config.hyperhive.matrixPrimaryAccount config.hyperhive.matrixAccounts;
message =
"hyperhive.matrixPrimaryAccount (\"${toString config.hyperhive.matrixPrimaryAccount}\") "
+ "must be one of the hyperhive.matrixAccounts keys "
+ "([ ${lib.concatStringsSep " " (builtins.attrNames config.hyperhive.matrixAccounts)} ]).";
"hyperhive.matrixAccounts requires hyperhive.matrix.enable = true "
+ "(the extras extend the hive-internal `main` account, which only "
+ "exists when matrix is enabled).";
}
# hyperhive.frontend.extraFiles[*].target is concatenated into
# $out during the mergedDist build. The option's strMatching
@ -1349,45 +1323,26 @@ in
HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket";
RUST_LOG = "info";
}
# Multi-account: serialize the declared accounts to the JSON the
# daemon parses (`accounts::configured`). Only set when accounts
# are declared, so the single-account agents (the common case)
# keep hitting the daemon's legacy fallback (which is bypassed the
# moment HIVE_MATRIX_ACCOUNTS is present). The `matrixAccounts`
# attrset is keyed by account name (unique by construction); we
# emit a primary-first JSON array (the daemon treats index 0 as the
# primary) with each entry in the daemon's `AccountCfg` serde shape:
# name / token_file / state_dir / optional homeserver.
// lib.optionalAttrs (config.hyperhive.matrixAccounts != { }) (
let
accts = config.hyperhive.matrixAccounts;
names = builtins.attrNames accts;
# Primary: the explicit option, or the sole account's name when
# only one is declared. The assertions below guarantee this
# resolves to a real key.
primary =
if config.hyperhive.matrixPrimaryAccount != null then
config.hyperhive.matrixPrimaryAccount
else
builtins.head names;
# Primary first, then the remaining names (attrNames is sorted).
ordered = [ primary ] ++ builtins.filter (n: n != primary) names;
toEntry =
name:
let
a = accts.${name};
in
# Multi-account: serialize the *extra* accounts to the JSON the
# daemon parses (`accounts::configured`). Only set when extras are
# declared; the daemon always synthesizes the primary `main`
# (hive-internal) account itself from the per-agent paths and
# prepends it, so we emit extras only. Each entry is in the
# daemon's `AccountCfg` serde shape: name (the attr key) /
# token_file / state_dir / optional homeserver.
// lib.optionalAttrs (config.hyperhive.matrixAccounts != { }) {
HIVE_MATRIX_ACCOUNTS = builtins.toJSON (
lib.mapAttrsToList (
name: a:
{
inherit name;
token_file = a.tokenFile;
state_dir = a.sessionDir;
}
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; };
in
{
HIVE_MATRIX_ACCOUNTS = builtins.toJSON (map toEntry ordered);
}
);
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
) config.hyperhive.matrixAccounts
);
};
serviceConfig = {
ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon";
Restart = "on-failure";