diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index f5efd435..b0361bec 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -299,6 +299,75 @@ in ''; }; + options.hyperhive.matrixAccounts = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + example = "ccc"; + description = '' + Logical account name the agent addresses this account by + (the `account` argument on the matrix MCP tools). Must be + unique within the agent. The FIRST account in the list is + the agent's primary account --- the one selected when a + tool call omits `account`. + ''; + }; + 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 (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). + ''; + }; + 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 = [ ]; + description = '' + Declare multiple matrix accounts served by a single + `hive-matrix-daemon` (one matrix-sdk Client + sync loop each), + replacing the wasteful "one MCP server + daemon per account" + pattern. Each entry becomes a routable account the matrix MCP + tools can target via their `account` argument; omitting `account` + on a tool call selects the first (primary) entry. + + Leave empty (the default) for the common single-account case: the + daemon then synthesizes one account from `hyperhive.matrix.url` + + `/matrix-token` + `/matrix-sdk-state`, so existing + agents need no change. When non-empty, this list is serialized to + the daemon's `HIVE_MATRIX_ACCOUNTS` environment variable and the + legacy single-account fallback is bypassed. + ''; + }; + options.hyperhive.frontend.dist = lib.mkOption { type = lib.types.package; default = pkgs.hyperhive-frontend; @@ -1239,6 +1308,26 @@ in HIVE_MATRIX_URL = config.hyperhive.matrix.url; 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). Keys match the daemon's + # `AccountCfg` serde shape: name / token_file / state_dir / + # optional homeserver. + // lib.optionalAttrs (config.hyperhive.matrixAccounts != [ ]) { + HIVE_MATRIX_ACCOUNTS = builtins.toJSON ( + map ( + a: + { + inherit (a) 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"; @@ -1318,9 +1407,12 @@ 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 matrix-token appears"; + description = "trigger hive-matrix-daemon when a matrix token appears"; wantedBy = [ "multi-user.target" ]; - pathConfig.PathExistsGlob = "/agents/*/state/matrix-token"; + # `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*"; }; # Path-trigger sibling: re-fires matrix-avatar-sync the moment