//! Multi-account configuration + the dispatch registry. //! //! 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. //! //! The FIRST configured account is the **primary**: it is selected when //! a tool call omits `account`, so single-account callers never specify //! one. use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use matrix_sdk::Client; use serde::Deserialize; use crate::paths; /// One declared matrix account. `homeserver` is optional per account /// (defaults to the daemon-wide `HIVE_MATRIX_URL`) so accounts on the /// same homeserver need not repeat it. #[derive(Debug, Clone, Deserialize)] pub struct AccountCfg { /// Logical name the agent uses to address this account /// (`account` arg on the MCP tools). Unique within the daemon. pub name: String, /// Path to the bearer-token file for this account (hive-c0re /// writes it; the daemon reads it). pub token_file: PathBuf, /// Per-account matrix-sdk sqlite store dir (crypto keys + cache). pub state_dir: PathBuf, /// Homeserver URL; falls back to [`paths::homeserver_url`] when absent. #[serde(default)] pub homeserver: Option, } impl AccountCfg { /// Resolve the effective homeserver URL (per-account override or /// the daemon-wide default). #[must_use] pub fn homeserver(&self) -> String { self.homeserver .clone() .unwrap_or_else(paths::homeserver_url) } } /// 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`. /// /// # Errors /// /// Returns an error if `HIVE_MATRIX_ACCOUNTS` is set but is not valid /// JSON, is empty, or contains duplicate account names. pub fn configured() -> anyhow::Result> { 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, }]); }; let raw = raw.to_string_lossy(); let accounts: Vec = serde_json::from_str(&raw) .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 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", a.name ); } } Ok(accounts) } /// Account name → live `Client` map plus the primary-account name used /// when a request omits `account`. Built once at daemon startup from /// the accounts that successfully restored a session. pub struct Registry { primary: String, by_name: HashMap>, } impl Registry { /// Build an empty registry whose primary is `primary`. Clients are /// added with [`Registry::insert`] as each account restores. #[must_use] pub fn new(primary: String) -> Self { Self { primary, by_name: HashMap::new(), } } /// Register a restored client under `name`. pub fn insert(&mut self, name: String, client: Client) { self.by_name.insert(name, Arc::new(client)); } /// Whether any account restored successfully. #[must_use] pub fn is_empty(&self) -> bool { self.by_name.is_empty() } /// Resolve a request's `account` to a client. `None` → the primary /// account. Returns a human-readable error (listing known accounts) /// when the name is unknown — surfaced to the agent as a tool error. /// /// # Errors /// /// Errors when the named account (or the primary, if `None`) has no /// live client — e.g. an unknown name, or the primary failed to /// restore at startup. pub fn resolve(&self, account: Option<&str>) -> Result<&Arc, String> { let name = account.unwrap_or(&self.primary); self.by_name.get(name).ok_or_else(|| { let mut known: Vec<&str> = self.by_name.keys().map(String::as_str).collect(); known.sort_unstable(); format!( "unknown matrix account {name:?}; available accounts: [{}]", known.join(", ") ) }) } }