Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ba0abcf27 | ||
|
|
36fb041c55 | ||
|
|
63db108516 | ||
|
|
dcc059b4b8 |
2 changed files with 152 additions and 29 deletions
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,6 +299,79 @@ 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` + `<state>/matrix-token` +
|
||||
`<state>/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;
|
||||
|
|
@ -687,6 +760,24 @@ 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
|
||||
|
|
@ -1239,6 +1330,26 @@ 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";
|
||||
|
|
@ -1318,9 +1429,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
|
||||
|
|
|
|||
Loading…
Reference in a new issue