matrixAccounts: hive 'main' is implicit primary, option declares extras only
This commit is contained in:
parent
63db108516
commit
36fb041c55
2 changed files with 79 additions and 115 deletions
|
|
@ -2,16 +2,18 @@
|
||||||
//!
|
//!
|
||||||
//! A single `hive-matrix-daemon` can serve N matrix accounts (one
|
//! A single `hive-matrix-daemon` can serve N matrix accounts (one
|
||||||
//! matrix-sdk `Client` each, with its own session/store dir + sync
|
//! matrix-sdk `Client` each, with its own session/store dir + sync
|
||||||
//! loop). The account list comes from the `HIVE_MATRIX_ACCOUNTS` env
|
//! loop). The **hive-internal account** (named `main`) is always
|
||||||
//! var (JSON, written by the nix harness module from
|
//! present and is the primary: it is synthesized from the per-agent
|
||||||
//! `hyperhive.matrixAccounts`); when that var is absent the daemon
|
//! single-account paths (`<state>/matrix-token` +
|
||||||
//! synthesizes the single legacy account from the existing
|
//! `<state>/matrix-sdk-state`) and the daemon-wide `HIVE_MATRIX_URL`.
|
||||||
//! `HIVE_MATRIX_*` env so every current single-account agent keeps
|
//! Any **extra** accounts come from the `HIVE_MATRIX_ACCOUNTS` env var
|
||||||
//! working with zero config change.
|
//! (JSON, written by the nix harness module from
|
||||||
|
//! `hyperhive.matrixAccounts`) and are appended after `main`.
|
||||||
//!
|
//!
|
||||||
//! The FIRST configured account is the **primary**: it is selected when
|
//! So a single-account agent (no `HIVE_MATRIX_ACCOUNTS`) gets exactly
|
||||||
//! a tool call omits `account`, so single-account callers never specify
|
//! `main` — zero config, same behaviour as before. The **primary** is
|
||||||
//! one.
|
//! `main`: it is selected when a tool call omits `account`, so callers
|
||||||
|
//! never specify one for the hive account.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -51,36 +53,43 @@ impl AccountCfg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the configured account list. Parses `HIVE_MATRIX_ACCOUNTS`
|
/// Build the account list: the always-present hive-internal `main`
|
||||||
/// (JSON array) when set; otherwise returns the single legacy account
|
/// account (primary, synthesized from the per-agent single-account
|
||||||
/// built from `HIVE_MATRIX_*` / `HYPERHIVE_STATE_DIR`.
|
/// paths) followed by any extras declared in `HIVE_MATRIX_ACCOUNTS`.
|
||||||
|
///
|
||||||
|
/// With no `HIVE_MATRIX_ACCOUNTS` set this returns just `main`, so a
|
||||||
|
/// single-account agent is unchanged. When set, the env var carries
|
||||||
|
/// only the *extra* accounts (the nix `matrixAccounts` option never
|
||||||
|
/// redeclares the hive account); they are appended after `main`, which
|
||||||
|
/// stays index 0 = primary.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if `HIVE_MATRIX_ACCOUNTS` is set but is not valid
|
/// Returns an error if `HIVE_MATRIX_ACCOUNTS` is set but is not valid
|
||||||
/// JSON, is empty, or contains duplicate account names.
|
/// JSON, or if an extra account's name collides with `main` or another
|
||||||
|
/// extra.
|
||||||
pub fn configured() -> anyhow::Result<Vec<AccountCfg>> {
|
pub fn configured() -> anyhow::Result<Vec<AccountCfg>> {
|
||||||
let Some(raw) = std::env::var_os("HIVE_MATRIX_ACCOUNTS") else {
|
// The hive-internal account: always primary, named `main`, built
|
||||||
// Legacy single-account fallback — the only deployed shape until
|
// from the legacy single-account paths + the daemon-wide homeserver.
|
||||||
// the nix `matrixAccounts` option lands.
|
let hive = AccountCfg {
|
||||||
return Ok(vec![AccountCfg {
|
name: "main".to_owned(),
|
||||||
name: "default".to_owned(),
|
token_file: paths::token_file(),
|
||||||
token_file: paths::token_file(),
|
state_dir: paths::matrix_state_dir(),
|
||||||
state_dir: paths::matrix_state_dir(),
|
homeserver: None,
|
||||||
homeserver: None,
|
|
||||||
}]);
|
|
||||||
};
|
};
|
||||||
let raw = raw.to_string_lossy();
|
let Some(raw) = std::env::var_os("HIVE_MATRIX_ACCOUNTS") else {
|
||||||
let accounts: Vec<AccountCfg> = serde_json::from_str(&raw)
|
return Ok(vec![hive]);
|
||||||
|
};
|
||||||
|
let extras: Vec<AccountCfg> = serde_json::from_str(&raw.to_string_lossy())
|
||||||
.map_err(|e| anyhow::anyhow!("parse HIVE_MATRIX_ACCOUNTS as JSON array: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("parse HIVE_MATRIX_ACCOUNTS as JSON array: {e}"))?;
|
||||||
if accounts.is_empty() {
|
let mut accounts = Vec::with_capacity(extras.len() + 1);
|
||||||
anyhow::bail!("HIVE_MATRIX_ACCOUNTS is an empty array — declare at least one account");
|
accounts.push(hive);
|
||||||
}
|
accounts.extend(extras);
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
for a in &accounts {
|
for a in &accounts {
|
||||||
if !seen.insert(a.name.as_str()) {
|
if !seen.insert(a.name.as_str()) {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"duplicate matrix account name {:?} in HIVE_MATRIX_ACCOUNTS",
|
"duplicate matrix account name {:?} (the hive-internal account is named \"main\")",
|
||||||
a.name
|
a.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,11 +309,9 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Path to this account's bearer-token file. The daemon reads
|
Path to this account's bearer-token file. The daemon reads
|
||||||
the token from here to restore the matrix session; how the
|
the token from here to restore the matrix session; how the
|
||||||
file gets populated is the provisioner's concern (hive-c0re
|
file gets populated is the provisioner's concern (an
|
||||||
for in-hive accounts, an operator-supplied secret for an
|
operator-supplied secret for an external account). The
|
||||||
external account). The daemon skips an account whose token
|
daemon skips an extra account whose token file is absent.
|
||||||
file is absent (the primary account being absent makes the
|
|
||||||
daemon exit cleanly until it appears).
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
sessionDir = lib.mkOption {
|
sessionDir = lib.mkOption {
|
||||||
|
|
@ -342,10 +340,6 @@ in
|
||||||
default = { };
|
default = { };
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
{
|
{
|
||||||
main = {
|
|
||||||
tokenFile = "/agents/dmatrix/state/matrix-token";
|
|
||||||
sessionDir = "/agents/dmatrix/state/matrix-sdk-state";
|
|
||||||
};
|
|
||||||
ccc = {
|
ccc = {
|
||||||
tokenFile = "/agents/dmatrix/state/matrix-token-ccc";
|
tokenFile = "/agents/dmatrix/state/matrix-token-ccc";
|
||||||
sessionDir = "/agents/dmatrix/state/matrix-sdk-state-ccc";
|
sessionDir = "/agents/dmatrix/state/matrix-sdk-state-ccc";
|
||||||
|
|
@ -354,35 +348,27 @@ in
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
description = ''
|
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),
|
`hive-matrix-daemon` (one matrix-sdk Client + sync loop each),
|
||||||
replacing the wasteful "one MCP server + daemon per account"
|
beyond the agent's built-in hive-internal account. This replaces
|
||||||
pattern. The attribute name keys each account (unique by
|
the wasteful "one MCP server + daemon per account" pattern. The
|
||||||
construction) and is the handle the matrix MCP tools target via
|
attribute name keys each account (unique by construction) and is
|
||||||
their `account` argument; omitting `account` on a tool call selects
|
the handle the matrix MCP tools target via their `account`
|
||||||
`hyperhive.matrixPrimaryAccount`.
|
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
|
Leave empty (the default) for the common single-account case: the
|
||||||
daemon then synthesizes one account from `hyperhive.matrix.url` +
|
agent then has only `main`, exactly as before. When non-empty, the
|
||||||
`<state>/matrix-token` + `<state>/matrix-sdk-state`, so existing
|
extras are serialized to the daemon's `HIVE_MATRIX_ACCOUNTS`
|
||||||
agents need no change. When non-empty, the set is serialized to the
|
environment variable and the daemon appends them after `main`.
|
||||||
daemon's `HIVE_MATRIX_ACCOUNTS` environment variable
|
Requires `hyperhive.matrix.enable` (there is no `main` to extend
|
||||||
(primary-account-first) and the legacy single-account fallback is
|
otherwise).
|
||||||
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.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -774,27 +760,15 @@ in
|
||||||
assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon);
|
assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon);
|
||||||
message = "hyperhive.icon must point to an .svg file";
|
message = "hyperhive.icon must point to an .svg file";
|
||||||
}
|
}
|
||||||
# With more than one matrix account, the primary must be named
|
# Extra matrix accounts only make sense alongside the hive-internal
|
||||||
# explicitly --- there is no inherent order in the attrset to pick
|
# `main` account they extend, which exists only when matrix is
|
||||||
# one from.
|
# enabled.
|
||||||
{
|
{
|
||||||
assertion =
|
assertion = config.hyperhive.matrixAccounts == { } || config.hyperhive.matrix.enable;
|
||||||
builtins.length (builtins.attrNames config.hyperhive.matrixAccounts) <= 1
|
|
||||||
|| config.hyperhive.matrixPrimaryAccount != null;
|
|
||||||
message =
|
message =
|
||||||
"hyperhive.matrixPrimaryAccount must be set when more than one "
|
"hyperhive.matrixAccounts requires hyperhive.matrix.enable = true "
|
||||||
+ "hyperhive.matrixAccounts entry is declared (it selects the account "
|
+ "(the extras extend the hive-internal `main` account, which only "
|
||||||
+ "used when a matrix tool call omits `account`).";
|
+ "exists when matrix is enabled).";
|
||||||
}
|
|
||||||
# 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.frontend.extraFiles[*].target is concatenated into
|
# hyperhive.frontend.extraFiles[*].target is concatenated into
|
||||||
# $out during the mergedDist build. The option's strMatching
|
# $out during the mergedDist build. The option's strMatching
|
||||||
|
|
@ -1349,45 +1323,26 @@ in
|
||||||
HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket";
|
HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket";
|
||||||
RUST_LOG = "info";
|
RUST_LOG = "info";
|
||||||
}
|
}
|
||||||
# Multi-account: serialize the declared accounts to the JSON the
|
# Multi-account: serialize the *extra* accounts to the JSON the
|
||||||
# daemon parses (`accounts::configured`). Only set when accounts
|
# daemon parses (`accounts::configured`). Only set when extras are
|
||||||
# are declared, so the single-account agents (the common case)
|
# declared; the daemon always synthesizes the primary `main`
|
||||||
# keep hitting the daemon's legacy fallback (which is bypassed the
|
# (hive-internal) account itself from the per-agent paths and
|
||||||
# moment HIVE_MATRIX_ACCOUNTS is present). The `matrixAccounts`
|
# prepends it, so we emit extras only. Each entry is in the
|
||||||
# attrset is keyed by account name (unique by construction); we
|
# daemon's `AccountCfg` serde shape: name (the attr key) /
|
||||||
# emit a primary-first JSON array (the daemon treats index 0 as the
|
# token_file / state_dir / optional homeserver.
|
||||||
# primary) with each entry in the daemon's `AccountCfg` serde shape:
|
// lib.optionalAttrs (config.hyperhive.matrixAccounts != { }) {
|
||||||
# name / token_file / state_dir / optional homeserver.
|
HIVE_MATRIX_ACCOUNTS = builtins.toJSON (
|
||||||
// lib.optionalAttrs (config.hyperhive.matrixAccounts != { }) (
|
lib.mapAttrsToList (
|
||||||
let
|
name: a:
|
||||||
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
|
|
||||||
{
|
{
|
||||||
inherit name;
|
inherit name;
|
||||||
token_file = a.tokenFile;
|
token_file = a.tokenFile;
|
||||||
state_dir = a.sessionDir;
|
state_dir = a.sessionDir;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; };
|
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
|
||||||
in
|
) config.hyperhive.matrixAccounts
|
||||||
{
|
);
|
||||||
HIVE_MATRIX_ACCOUNTS = builtins.toJSON (map toEntry ordered);
|
};
|
||||||
}
|
|
||||||
);
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon";
|
ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue