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

@ -2,16 +2,18 @@
//!
//! A single `hive-matrix-daemon` can serve N matrix accounts (one
//! matrix-sdk `Client` each, with its own session/store dir + sync
//! loop). The account list comes from the `HIVE_MATRIX_ACCOUNTS` env
//! var (JSON, written by the nix harness module from
//! `hyperhive.matrixAccounts`); when that var is absent the daemon
//! synthesizes the single legacy account from the existing
//! `HIVE_MATRIX_*` env so every current single-account agent keeps
//! working with zero config change.
//! loop). The **hive-internal account** (named `main`) is always
//! present and is the primary: it is synthesized from the per-agent
//! single-account paths (`<state>/matrix-token` +
//! `<state>/matrix-sdk-state`) and the daemon-wide `HIVE_MATRIX_URL`.
//! Any **extra** accounts come from the `HIVE_MATRIX_ACCOUNTS` env var
//! (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
//! a tool call omits `account`, so single-account callers never specify
//! one.
//! So a single-account agent (no `HIVE_MATRIX_ACCOUNTS`) gets exactly
//! `main` — zero config, same behaviour as before. The **primary** is
//! `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::path::PathBuf;
@ -51,36 +53,43 @@ impl AccountCfg {
}
}
/// Read the configured account list. Parses `HIVE_MATRIX_ACCOUNTS`
/// (JSON array) when set; otherwise returns the single legacy account
/// built from `HIVE_MATRIX_*` / `HYPERHIVE_STATE_DIR`.
/// Build the account list: the always-present hive-internal `main`
/// account (primary, synthesized from the per-agent single-account
/// 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
///
/// 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>> {
let Some(raw) = std::env::var_os("HIVE_MATRIX_ACCOUNTS") else {
// Legacy single-account fallback — the only deployed shape until
// the nix `matrixAccounts` option lands.
return Ok(vec![AccountCfg {
name: "default".to_owned(),
token_file: paths::token_file(),
state_dir: paths::matrix_state_dir(),
homeserver: None,
}]);
// The hive-internal account: always primary, named `main`, built
// from the legacy single-account paths + the daemon-wide homeserver.
let hive = AccountCfg {
name: "main".to_owned(),
token_file: paths::token_file(),
state_dir: paths::matrix_state_dir(),
homeserver: None,
};
let raw = raw.to_string_lossy();
let accounts: Vec<AccountCfg> = serde_json::from_str(&raw)
let Some(raw) = std::env::var_os("HIVE_MATRIX_ACCOUNTS") else {
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}"))?;
if accounts.is_empty() {
anyhow::bail!("HIVE_MATRIX_ACCOUNTS is an empty array — declare at least one account");
}
let mut accounts = Vec::with_capacity(extras.len() + 1);
accounts.push(hive);
accounts.extend(extras);
let mut seen = std::collections::HashSet::new();
for a in &accounts {
if !seen.insert(a.name.as_str()) {
anyhow::bail!(
"duplicate matrix account name {:?} in HIVE_MATRIX_ACCOUNTS",
"duplicate matrix account name {:?} (the hive-internal account is named \"main\")",
a.name
);
}