From dcc059b4b8a7366380beffef1ad642e10b9496b6 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 15 Jun 2026 20:48:11 +0200 Subject: [PATCH 1/4] feat: hyperhive.matrixAccounts nix option (matrix multi-account layer 2) --- nix/templates/harness-base.nix | 96 +++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) 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 From 63db1085167d67180e85b9e4b63dce9abe6b6533 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 15 Jun 2026 20:57:50 +0200 Subject: [PATCH 2/4] rework matrixAccounts to attrset keyed by name + matrixPrimaryAccount --- nix/templates/harness-base.nix | 121 ++++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 31 deletions(-) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index b0361bec..e2b420f7 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -300,20 +300,9 @@ in }; options.hyperhive.matrixAccounts = lib.mkOption { - type = lib.types.listOf ( + type = lib.types.attrsOf ( 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"; @@ -350,21 +339,50 @@ in }; } ); - default = [ ]; + default = { }; + example = lib.literalExpression '' + { + main = { + tokenFile = "/agents/dmatrix/state/matrix-token"; + sessionDir = "/agents/dmatrix/state/matrix-sdk-state"; + }; + ccc = { + tokenFile = "/agents/dmatrix/state/matrix-token-ccc"; + sessionDir = "/agents/dmatrix/state/matrix-sdk-state-ccc"; + homeserver = "https://matrix.example.org"; + }; + } + ''; 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. + pattern. The attribute name keys each account (unique by + construction) and is the handle the matrix MCP tools target via + their `account` argument; omitting `account` on a tool call selects + `hyperhive.matrixPrimaryAccount`. 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. + agents need no change. When non-empty, the set is serialized to the + daemon's `HIVE_MATRIX_ACCOUNTS` environment variable + (primary-account-first) and the legacy single-account fallback is + 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. ''; }; @@ -756,6 +774,28 @@ in assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon); message = "hyperhive.icon must point to an .svg file"; } + # With more than one matrix account, the primary must be named + # explicitly --- there is no inherent order in the attrset to pick + # one from. + { + assertion = + builtins.length (builtins.attrNames config.hyperhive.matrixAccounts) <= 1 + || config.hyperhive.matrixPrimaryAccount != null; + message = + "hyperhive.matrixPrimaryAccount must be set when more than one " + + "hyperhive.matrixAccounts entry is declared (it selects the account " + + "used when a matrix tool call omits `account`)."; + } + # 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 # $out during the mergedDist build. The option's strMatching # type already rejects leading `/`, leading `.`, and the @@ -1313,22 +1353,41 @@ in # 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: + # moment HIVE_MATRIX_ACCOUNTS is present). The `matrixAccounts` + # attrset is keyed by account name (unique by construction); we + # emit a primary-first JSON array (the daemon treats index 0 as the + # primary) with each entry in the daemon's `AccountCfg` serde shape: + # name / token_file / state_dir / optional homeserver. + // lib.optionalAttrs (config.hyperhive.matrixAccounts != { }) ( + let + 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 (a) name; + inherit name; token_file = a.tokenFile; state_dir = a.sessionDir; } - // lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; } - ) config.hyperhive.matrixAccounts - ); - }; + // lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }; + in + { + HIVE_MATRIX_ACCOUNTS = builtins.toJSON (map toEntry ordered); + } + ); serviceConfig = { ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon"; Restart = "on-failure"; From 36fb041c55376e804bb075872b59454bbfa7f4cb Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 15 Jun 2026 21:09:15 +0200 Subject: [PATCH 3/4] matrixAccounts: hive 'main' is implicit primary, option declares extras only --- hive-matrix-mcp/src/accounts.rs | 65 +++++++++------- nix/templates/harness-base.nix | 131 +++++++++++--------------------- 2 files changed, 80 insertions(+), 116 deletions(-) diff --git a/hive-matrix-mcp/src/accounts.rs b/hive-matrix-mcp/src/accounts.rs index 9e95b06b..0dd3fcb9 100644 --- a/hive-matrix-mcp/src/accounts.rs +++ b/hive-matrix-mcp/src/accounts.rs @@ -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 (`/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`. //! -//! 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> { - 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 = serde_json::from_str(&raw) + let Some(raw) = std::env::var_os("HIVE_MATRIX_ACCOUNTS") else { + return Ok(vec![hive]); + }; + let extras: Vec = 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 ); } diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index e2b420f7..0e3fda97 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -309,11 +309,9 @@ in 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). + 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 { @@ -342,10 +340,6 @@ in default = { }; example = lib.literalExpression '' { - main = { - tokenFile = "/agents/dmatrix/state/matrix-token"; - sessionDir = "/agents/dmatrix/state/matrix-sdk-state"; - }; ccc = { tokenFile = "/agents/dmatrix/state/matrix-token-ccc"; sessionDir = "/agents/dmatrix/state/matrix-sdk-state-ccc"; @@ -354,35 +348,27 @@ in } ''; 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), - replacing 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; omitting `account` on a tool call selects - `hyperhive.matrixPrimaryAccount`. + 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 - daemon then synthesizes one account from `hyperhive.matrix.url` + - `/matrix-token` + `/matrix-sdk-state`, so existing - agents need no change. When non-empty, the set is serialized to the - daemon's `HIVE_MATRIX_ACCOUNTS` environment variable - (primary-account-first) and the legacy single-account fallback is - 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. + 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). ''; }; @@ -774,27 +760,15 @@ in assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon); message = "hyperhive.icon must point to an .svg file"; } - # With more than one matrix account, the primary must be named - # explicitly --- there is no inherent order in the attrset to pick - # one from. + # Extra matrix accounts only make sense alongside the hive-internal + # `main` account they extend, which exists only when matrix is + # enabled. { - assertion = - builtins.length (builtins.attrNames config.hyperhive.matrixAccounts) <= 1 - || config.hyperhive.matrixPrimaryAccount != null; + assertion = config.hyperhive.matrixAccounts == { } || config.hyperhive.matrix.enable; message = - "hyperhive.matrixPrimaryAccount must be set when more than one " - + "hyperhive.matrixAccounts entry is declared (it selects the account " - + "used when a matrix tool call omits `account`)."; - } - # 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.matrixAccounts requires hyperhive.matrix.enable = true " + + "(the extras extend the hive-internal `main` account, which only " + + "exists when matrix is enabled)."; } # hyperhive.frontend.extraFiles[*].target is concatenated into # $out during the mergedDist build. The option's strMatching @@ -1349,45 +1323,26 @@ in 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). The `matrixAccounts` - # attrset is keyed by account name (unique by construction); we - # emit a primary-first JSON array (the daemon treats index 0 as the - # primary) with each entry in the daemon's `AccountCfg` serde shape: - # name / token_file / state_dir / optional homeserver. - // lib.optionalAttrs (config.hyperhive.matrixAccounts != { }) ( - let - 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 + # 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; }; - in - { - HIVE_MATRIX_ACCOUNTS = builtins.toJSON (map toEntry ordered); - } - ); + // lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; } + ) config.hyperhive.matrixAccounts + ); + }; serviceConfig = { ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon"; Restart = "on-failure"; From 8ba0abcf271a0d3e86040cb850a49c37d4f33082 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 15 Jun 2026 21:16:53 +0200 Subject: [PATCH 4/4] matrixAccounts: assert no extra named 'main' (reserved for hive account) --- nix/templates/harness-base.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 0e3fda97..8d3b91ee 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -770,6 +770,14 @@ in + "(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