diff --git a/hive-matrix-mcp/src/accounts.rs b/hive-matrix-mcp/src/accounts.rs index 0dd3fcb9..9e95b06b 100644 --- a/hive-matrix-mcp/src/accounts.rs +++ b/hive-matrix-mcp/src/accounts.rs @@ -2,18 +2,16 @@ //! //! A single `hive-matrix-daemon` can serve N matrix accounts (one //! matrix-sdk `Client` each, with its own session/store dir + sync -//! loop). The **hive-internal account** (named `main`) is always -//! present and is the primary: it is synthesized from the per-agent -//! single-account paths (`/matrix-token` + -//! `/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`. +//! 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. //! -//! 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. +//! 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; @@ -53,43 +51,36 @@ impl AccountCfg { } } -/// 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. +/// 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, or if an extra account's name collides with `main` or another -/// extra. +/// JSON, is empty, or contains duplicate account names. pub fn configured() -> anyhow::Result> { - // 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 Some(raw) = std::env::var_os("HIVE_MATRIX_ACCOUNTS") else { - return Ok(vec![hive]); + // 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 extras: Vec = serde_json::from_str(&raw.to_string_lossy()) + 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}"))?; - let mut accounts = Vec::with_capacity(extras.len() + 1); - accounts.push(hive); - accounts.extend(extras); + 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 {:?} (the hive-internal account is named \"main\")", + "duplicate matrix account name {:?} in HIVE_MATRIX_ACCOUNTS", a.name ); } diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 8d3b91ee..f5efd435 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -299,79 +299,6 @@ in ''; }; - options.hyperhive.matrixAccounts = lib.mkOption { - type = lib.types.attrsOf ( - lib.types.submodule { - options = { - tokenFile = lib.mkOption { - type = lib.types.str; - example = "/agents/dmatrix/state/matrix-token-ccc"; - 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 (an - operator-supplied secret for an external account). The - daemon skips an extra account whose token file is absent. - ''; - }; - sessionDir = lib.mkOption { - type = lib.types.str; - example = "/agents/dmatrix/state/matrix-sdk-state-ccc"; - description = '' - Per-account matrix-sdk sqlite store directory (crypto keys - + event cache). Must differ between accounts so their - sessions do not collide. - ''; - }; - homeserver = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "https://matrix.example.org"; - description = '' - Homeserver URL for this account. When null (the default), - the account falls back to `hyperhive.matrix.url`. Set it for - an account on a different homeserver than the agent's - default (e.g. an external public-matrix account). - ''; - }; - }; - } - ); - default = { }; - example = lib.literalExpression '' - { - ccc = { - tokenFile = "/agents/dmatrix/state/matrix-token-ccc"; - sessionDir = "/agents/dmatrix/state/matrix-sdk-state-ccc"; - homeserver = "https://matrix.example.org"; - }; - } - ''; - description = '' - Declare *additional* matrix accounts served by the single - `hive-matrix-daemon` (one matrix-sdk Client + sync loop each), - 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` + `/matrix-token` + - `/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 - 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). - ''; - }; - options.hyperhive.frontend.dist = lib.mkOption { type = lib.types.package; default = pkgs.hyperhive-frontend; @@ -760,24 +687,6 @@ in assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon); message = "hyperhive.icon must point to an .svg file"; } - # Extra matrix accounts only make sense alongside the hive-internal - # `main` account they extend, which exists only when matrix is - # enabled. - { - assertion = config.hyperhive.matrixAccounts == { } || config.hyperhive.matrix.enable; - message = - "hyperhive.matrixAccounts requires hyperhive.matrix.enable = true " - + "(the extras extend the hive-internal `main` account, which only " - + "exists when matrix is enabled)."; - } - # `main` is reserved for the synthesized hive-internal account; a - # declared extra by that name would silently collide with it. - { - assertion = !builtins.hasAttr "main" config.hyperhive.matrixAccounts; - message = - "hyperhive.matrixAccounts cannot contain a key named \"main\" " - + "--- that name is reserved for the hive-internal account."; - } # hyperhive.frontend.extraFiles[*].target is concatenated into # $out during the mergedDist build. The option's strMatching # type already rejects leading `/`, leading `.`, and the @@ -1330,26 +1239,6 @@ in HIVE_MATRIX_URL = config.hyperhive.matrix.url; HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket"; RUST_LOG = "info"; - } - # 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; } - ) config.hyperhive.matrixAccounts - ); }; serviceConfig = { ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon"; @@ -1429,12 +1318,9 @@ in # would have no backend until next restart. See # `docs/persistence.md` (same section as above). systemd.paths.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable { - description = "trigger hive-matrix-daemon when a matrix token appears"; + description = "trigger hive-matrix-daemon when matrix-token appears"; wantedBy = [ "multi-user.target" ]; - # `matrix-token*` (not just `matrix-token`) so a secondary - # multi-account token (e.g. `matrix-token-ccc`) landing also - # re-fires the daemon to pick up the freshly-provisioned account. - pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*"; + pathConfig.PathExistsGlob = "/agents/*/state/matrix-token"; }; # Path-trigger sibling: re-fires matrix-avatar-sync the moment