matrix: make the hive-internal main account an ordinary matrixAccounts entry

`matrixAccounts` is meant to be the agent's full account list, but the
hive-internal `main` account was outside it: the nix module emitted only
the extras and `hive-matrix-daemon` prepended a `main` it synthesized
from the per-agent paths, with the option schema forbidding the name
outright.

nix/agent-modules/matrix.nix now declares `main` itself, as an ordinary
entry under `matrix.enable`, from the state-dir paths the module already
used for its token path-watcher (now a shared `stateDir` binding) plus
`matrix.url`. The whole set, `main` included, is serialized to
HIVE_MATRIX_ACCOUNTS.

accounts::configured therefore synthesizes `main` only when the parsed
list carries none, and otherwise takes the declared one verbatim —
hoisting it to index 0, since the daemon reads index 0 as the primary
and nix serializes an attrset, so `main` sorts wherever its key falls.
Declared xor synthesized: an agent whose harness predates this entry
keeps working, a current one gets its own, and there is no arrangement
where `main` is duplicated or missing.

The reserved-name assertion is replaced rather than dropped: the name
must now be legal (the module uses it), but `main`'s tokenFile stays
pinned to `<state>/matrix-token`, since hive-c0re provisions the
hive-internal token there and nowhere else — a retarget would evaluate
fine and then never restore. The other two fields are mkDefault and free
to override.

Refs #4475
This commit is contained in:
atlas 2026-09-18 03:35:00 +02:00 • committed by mara
commit c74249f371
4 changed files with 220 additions and 80 deletions

View file

@ -69,17 +69,22 @@ room you haven't read yet.
## Multiple accounts ## Multiple accounts
`services.hyperhive.agent.matrixAccounts` (declared in `agent.nix`) gives an agent `services.hyperhive.agent.matrixAccounts` (declared in `agent.nix`) is
*additional* matrix identities beyond the hive-internal one — for example an the agent's full set of matrix identities — for example an
external-facing account alongside the internal one. Each entry is external-facing account alongside the internal one. Each entry is
keyed by account name and specifies `tokenFile` (bearer token, keyed by account name and specifies `tokenFile` (bearer token,
provisioned out-of-band; basename must start with `matrix-token`), provisioned out-of-band; basename must start with `matrix-token`),
`sessionDir` (per-account matrix-sdk sqlite state — crypto keys + `sessionDir` (per-account matrix-sdk sqlite state — crypto keys +
cache), and an optional `homeserver` (defaults to cache), and an optional `homeserver` (defaults to
`services.hyperhive.agent.matrix.url`). The hive-internal account is always named `services.hyperhive.agent.matrix.url`). The hive-internal account is
`main`, synthesized from `services.hyperhive.agent.matrix.url` + agent state — this always named `main` and is always the primary; the matrix module
option only declares extras, and `main` is a reserved key here. declares it for you as an ordinary entry of this map, from
Requires `services.hyperhive.agent.matrix.enable = true`. `services.hyperhive.agent.matrix.url` + agent state, so what you add
here are the *further* accounts. Its `tokenFile` is pinned to
`<state>/matrix-token` (hive-c0re provisions it there), and the
dashboard's link-account route refuses to create an account by that
name. Declaring extras requires
`services.hyperhive.agent.matrix.enable = true`.
Every matrix tool above takes an optional `account` parameter (a name Every matrix tool above takes an optional `account` parameter (a name
from this map) to act as that identity instead of the primary one. from this map) to act as that identity instead of the primary one.

View file

@ -2,13 +2,18 @@
//! //!
//! A single `hive-matrix-daemon` can serve N matrix accounts (one //! A single `hive-matrix-daemon` can serve N matrix accounts (one
//! matrix-sdk `Client` each, with its own session/store dir + sync //! matrix-sdk `Client` each, with its own session/store dir + sync
//! loop). The **hive-internal account** (named `main`) is always //! loop). Accounts come from the `HIVE_MATRIX_ACCOUNTS` env var (JSON,
//! present and is the primary: it is synthesized from the per-agent //! written by the nix harness module from
//! single-account paths (`<state>/matrix-token` + //! `services.hyperhive.agent.matrixAccounts`), which declares the
//! **hive-internal account** (named `main`) as an ordinary entry
//! alongside any others.
//!
//! `main` is always present and is always the primary: it is moved to
//! index 0 whichever position it was declared at, and if the env var
//! declares no `main` at all (it is unset, or an agent's harness
//! predates the nix module emitting it) one is synthesized from the
//! per-agent single-account paths (`<state>/matrix-token` +
//! `<state>/matrix-sdk-state`) and the daemon-wide `HIVE_MATRIX_URL`. //! `<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
//! `services.hyperhive.agent.matrixAccounts`) and are appended after `main`.
//! //!
//! So a single-account agent (no `HIVE_MATRIX_ACCOUNTS`) gets exactly //! So a single-account agent (no `HIVE_MATRIX_ACCOUNTS`) gets exactly
//! `main` — zero config, same behaviour as before: omitting `account` on //! `main` — zero config, same behaviour as before: omitting `account` on
@ -58,46 +63,29 @@ impl AccountCfg {
} }
} }
/// Build the account list: the always-present hive-internal `main` /// Name of the hive-internal account: the primary, and what a tool call
/// account (primary, synthesized from the per-agent single-account /// resolves to when it omits `account`.
/// paths) followed by any extras declared in `HIVE_MATRIX_ACCOUNTS`. const HIVE_ACCOUNT: &str = "main";
/// Build the account list: everything declared in `HIVE_MATRIX_ACCOUNTS`,
/// with the hive-internal `main` account hoisted to index 0 (= primary)
/// or synthesized there when the declaration doesn't carry one, plus any
/// dashboard-provisioned accounts discovered on disk.
/// ///
/// With no `HIVE_MATRIX_ACCOUNTS` set this returns just `main`, so a /// With no `HIVE_MATRIX_ACCOUNTS` set this returns just `main`, so a
/// single-account agent is unchanged. When set, the env var carries /// single-account agent is unchanged.
/// only the *extra* accounts (the nix `matrixAccounts` option never
/// redeclares the hive account); they are appended after `main`, which
/// stays index 0 = primary.
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an error if `HIVE_MATRIX_ACCOUNTS` is set but is not valid /// 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 /// JSON, or if two declared accounts share a name.
/// extra.
pub fn configured() -> anyhow::Result<Vec<AccountCfg>> { pub fn configured() -> anyhow::Result<Vec<AccountCfg>> {
// The hive-internal account: always primary, named `main`, built let declared: Vec<AccountCfg> = match std::env::var_os("HIVE_MATRIX_ACCOUNTS") {
// from the legacy single-account paths + the daemon-wide homeserver. Some(raw) => serde_json::from_str(&raw.to_string_lossy())
let hive = AccountCfg { .map_err(|e| anyhow::anyhow!("parse HIVE_MATRIX_ACCOUNTS as JSON array: {e}"))?,
name: "main".to_owned(), None => Vec::new(),
token_file: paths::token_file(),
state_dir: paths::matrix_state_dir(),
homeserver: None,
}; };
let mut accounts = vec![hive]; let mut accounts = ensure_hive_account(declared)?;
if let Some(raw) = std::env::var_os("HIVE_MATRIX_ACCOUNTS") {
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}"))?;
accounts.extend(extras);
}
// Reject duplicate names among the *configured* set (hive + 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 {:?} (the hive-internal account is named \"main\")",
a.name
);
}
}
// Append dashboard-provisioned accounts (a `matrix-token-<name>` file + // Append dashboard-provisioned accounts (a `matrix-token-<name>` file +
// its `matrix-account-<name>.json` homeserver sidecar) that aren't // its `matrix-account-<name>.json` homeserver sidecar) that aren't
// already declared in config, so an account logged in via the dashboard // already declared in config, so an account logged in via the dashboard
@ -112,6 +100,56 @@ pub fn configured() -> anyhow::Result<Vec<AccountCfg>> {
Ok(accounts) Ok(accounts)
} }
/// Put the hive-internal `main` account at index 0 of `declared`, then
/// reject duplicate names.
///
/// `main` is synthesized from the per-agent single-account paths **only
/// if `declared` doesn't already carry one** — the nix harness module
/// emits it as an ordinary `matrixAccounts` entry, so on a current
/// harness the declaration is authoritative and is used verbatim (just
/// moved to the front, since the module serializes an attrset and `main`
/// sorts wherever its key falls). A harness that predates that emits
/// nothing for it, and the synthesized account keeps that agent working
/// unchanged. Never both: `main` is declared xor synthesized, so there
/// is no arrangement where it is duplicated or missing.
///
/// Index 0 is load-bearing: the daemon's startup loop (`main.rs`) takes
/// index 0 as the primary — the account a tool call acts as when it
/// omits `account`, and the only one whose failure to restore is fatal.
///
/// # Errors
///
/// Returns an error if two declared accounts share a name.
fn ensure_hive_account(mut declared: Vec<AccountCfg>) -> anyhow::Result<Vec<AccountCfg>> {
match declared.iter().position(|a| a.name == HIVE_ACCOUNT) {
// Declared: keep it verbatim, just make it the primary. `remove` +
// `insert` rather than `swap`, so the other accounts keep their
// declared order.
Some(idx) => {
let hive = declared.remove(idx);
declared.insert(0, hive);
}
// Not declared: synthesize it from the legacy single-account paths
// + the daemon-wide homeserver.
None => declared.insert(
0,
AccountCfg {
name: HIVE_ACCOUNT.to_owned(),
token_file: paths::token_file(),
state_dir: paths::matrix_state_dir(),
homeserver: None,
},
),
}
let mut seen = std::collections::HashSet::new();
for a in &declared {
if !seen.insert(a.name.as_str()) {
anyhow::bail!("duplicate matrix account name {:?}", a.name);
}
}
Ok(declared)
}
/// Scan the agent state dir for extra matrix accounts provisioned via the /// Scan the agent state dir for extra matrix accounts provisioned via the
/// dashboard login form: each is a `matrix-token-<name>` file plus a /// dashboard login form: each is a `matrix-token-<name>` file plus a
/// `matrix-account-<name>.json` sidecar carrying the homeserver. Returns one /// `matrix-account-<name>.json` sidecar carrying the homeserver. Returns one
@ -417,11 +455,71 @@ fn write_accounts_snapshot_inner(
mod tests { mod tests {
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf;
use super::{ use super::{
AccountStatus, discover_token_accounts_in, heartbeat_accounts_snapshot, pick_name, AccountCfg, AccountStatus, discover_token_accounts_in, ensure_hive_account,
write_accounts_snapshot, heartbeat_accounts_snapshot, pick_name, write_accounts_snapshot,
}; };
/// A declared account, as the nix module would serialize it.
fn cfg(name: &str) -> AccountCfg {
AccountCfg {
name: name.to_owned(),
token_file: PathBuf::from(format!("/agents/a/state/matrix-token-{name}")),
state_dir: PathBuf::from(format!("/agents/a/state/matrix-sdk-state-{name}")),
homeserver: Some(format!("https://{name}.example")),
}
}
#[test]
fn declared_main_is_used_verbatim_and_not_duplicated() {
// A current harness declares `main` itself; it must be taken as-is
// (its own paths, not the synthesized ones) and must not gain a
// second, synthesized copy.
let out = ensure_hive_account(vec![cfg("main")]).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].name, "main");
assert_eq!(
out[0].token_file,
PathBuf::from("/agents/a/state/matrix-token-main")
);
assert_eq!(out[0].homeserver.as_deref(), Some("https://main.example"));
}
#[test]
fn missing_main_is_synthesized_as_primary() {
// A harness predating the nix module's `main` entry declares extras
// only (or nothing at all): `main` is synthesized at index 0 from
// the per-agent paths, so such an agent keeps working.
let out = ensure_hive_account(Vec::new()).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].name, "main");
let out = ensure_hive_account(vec![cfg("public")]).unwrap();
let names: Vec<&str> = out.iter().map(|a| a.name.as_str()).collect();
assert_eq!(names, vec!["main", "public"]);
// Synthesized, so it carries no per-account homeserver and falls
// back to the daemon-wide `HIVE_MATRIX_URL`.
assert!(out[0].homeserver.is_none());
}
#[test]
fn declared_main_alongside_extras_is_not_a_collision() {
// The nix module serializes an attrset, so `main` arrives wherever
// its key sorts. It is hoisted to index 0 (= primary), the extras
// keep their declared order, and nothing trips the duplicate check.
let out = ensure_hive_account(vec![cfg("ccc"), cfg("main"), cfg("public")]).unwrap();
let names: Vec<&str> = out.iter().map(|a| a.name.as_str()).collect();
assert_eq!(names, vec!["main", "ccc", "public"]);
}
#[test]
fn two_accounts_of_the_same_name_are_rejected() {
let err = ensure_hive_account(vec![cfg("public"), cfg("public")]).unwrap_err();
assert!(err.to_string().contains("duplicate matrix account name"));
}
#[test] #[test]
fn discovery_skips_configured_names_and_orphan_tokens() { fn discovery_skips_configured_names_and_orphan_tokens() {
let dir = std::env::temp_dir().join(format!( let dir = std::env::temp_dir().join(format!(

View file

@ -11,6 +11,11 @@
}: }:
let let
userName = config.services.hyperhive.agent.user.name; userName = config.services.hyperhive.agent.user.name;
# This agent's own state dir, where hive-c0re provisions the
# hive-internal account's token (`matrix-token`) and the daemon keeps
# its matrix-sdk store. Shared by the `main` account entry below and
# the path-watcher glob at the bottom of this file.
stateDir = "/agents/${userName}/state";
# Rasterize the operator-set agent icon (`services.hyperhive.agent.icon`, an SVG) to a # Rasterize the operator-set agent icon (`services.hyperhive.agent.icon`, an SVG) to a
# 512x512 PNG so the matrix daemon can upload it as each account's avatar # 512x512 PNG so the matrix daemon can upload it as each account's avatar
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar). # over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
@ -92,9 +97,11 @@ in
description = '' description = ''
Path to this account's bearer-token file. The daemon reads Path to this account's bearer-token file. The daemon reads
the token from here to restore the matrix session; how the the token from here to restore the matrix session; how the
file gets populated is the provisioner's concern (an file gets populated is the provisioner's concern
operator-supplied secret for an external account). The (hive-c0re for the hive-internal `main` account, an
daemon skips an extra account whose token file is absent. operator-supplied secret for an external one). The daemon
skips an extra account whose token file is absent, and
exits cleanly to wait on the path watcher when `main`'s is.
''; '';
}; };
sessionDir = lib.mkOption { sessionDir = lib.mkOption {
@ -131,24 +138,29 @@ in
} }
''; '';
description = '' description = ''
Declare *additional* matrix accounts served by the single Every matrix account served by the single `hive-matrix-daemon`
`hive-matrix-daemon` (one matrix-sdk Client + sync loop each), (one matrix-sdk Client + sync loop each). The attribute name keys
beyond the agent's built-in hive-internal account. The each account (unique by construction) and is the handle the matrix
attribute name keys each account (unique by construction) and is MCP tools target via their `account` argument.
the handle the matrix MCP tools target via their `account`
argument.
The **hive-internal account is always present and is the primary**: The **hive-internal account is always present and is the primary**:
it is named `main`, synthesized by the daemon from it is named `main`, and this module declares it for you (when
`services.hyperhive.agent.matrix.enable` is set) from
`services.hyperhive.agent.matrix.url` + `<state>/matrix-token` + `services.hyperhive.agent.matrix.url` + `<state>/matrix-token` +
`<state>/matrix-sdk-state`, and is the account a tool call acts as `<state>/matrix-sdk-state`. It is the account a tool call acts as
when it omits `account`. You never declare it here --- this option when it omits `account`. It is an ordinary entry of this option
is only for the extras (e.g. an external public-matrix account). like any other, so it shows up in the account list --- what you
add here are the *further* accounts (e.g. an external
public-matrix account). Its `tokenFile` stays pinned to
`<state>/matrix-token` (an assertion; that is the one path
hive-c0re provisions the hive-internal token to), and the
dashboard's link-account route refuses to create an account named
`main` --- the entry belongs to the module, not to a provisioner.
Leave empty (the default) for the common single-account case: the Leave it alone (the default) for the common single-account case:
agent then has only `main`. When non-empty, the extras are the agent then has only `main`. The whole set is serialized to the
serialized to the daemon's `HIVE_MATRIX_ACCOUNTS` environment daemon's `HIVE_MATRIX_ACCOUNTS` environment variable, `main`
variable and the daemon appends them after `main`. Requires first. Declaring extras requires
`services.hyperhive.agent.matrix.enable` (there is no `main` to extend otherwise). `services.hyperhive.agent.matrix.enable` (there is no `main` to extend otherwise).
''; '';
}; };
@ -185,13 +197,22 @@ in
+ "(the extras extend the hive-internal `main` account, which only " + "(the extras extend the hive-internal `main` account, which only "
+ "exists when matrix is enabled)."; + "exists when matrix is enabled).";
} }
# `main` is reserved for the synthesized hive-internal account; a # `main` is no longer a forbidden key --- this module declares it
# declared extra by that name would silently collide with it. # itself (see the `matrixAccounts.main` definition below), so the
# name must be allowed. What stays rejected is retargeting *its
# token file*: hive-c0re writes the hive-internal account's token
# to `<state>/matrix-token` and nowhere else, so an override there
# is an account that evaluates fine and then never restores. The
# other two fields are free to override (a `mkDefault` each).
{ {
assertion = !builtins.hasAttr "main" config.services.hyperhive.agent.matrixAccounts; assertion =
!config.services.hyperhive.agent.matrix.enable
|| config.services.hyperhive.agent.matrixAccounts.main.tokenFile == "${stateDir}/matrix-token";
message = message =
"services.hyperhive.agent.matrixAccounts cannot contain a key named \"main\" " "services.hyperhive.agent.matrixAccounts.main.tokenFile must stay "
+ "--- that name is reserved for the hive-internal account."; + "\"${stateDir}/matrix-token\" --- that is where hive-c0re provisions the "
+ "hive-internal account's token. Declare a separate account instead of "
+ "pointing `main` elsewhere.";
} }
# Token files must land at the `matrix-token*` name the daemon # Token files must land at the `matrix-token*` name the daemon
# path-watcher globs (`matrix-token*` inside this agent's own state # path-watcher globs (`matrix-token*` inside this agent's own state
@ -218,6 +239,21 @@ in
} }
]; ];
# The hive-internal account as an ordinary `matrixAccounts` entry,
# rather than something the daemon conjures behind the option's
# back: `matrixAccounts` is the list of *all* this agent's accounts,
# so the one it always has belongs in it. `mkDefault` per field so an
# operator can retarget e.g. the homeserver without a
# conflicting-definition error (the token file is pinned by an
# assertion above, since hive-c0re owns that path).
services.hyperhive.agent.matrixAccounts = lib.mkIf config.services.hyperhive.agent.matrix.enable {
main = {
tokenFile = lib.mkDefault "${stateDir}/matrix-token";
sessionDir = lib.mkDefault "${stateDir}/matrix-sdk-state";
homeserver = lib.mkDefault config.services.hyperhive.agent.matrix.url;
};
};
# Auto-inject the matrix MCP entry alongside the bash entry from # Auto-inject the matrix MCP entry alongside the bash entry from
# ./mcp.nix. `lib.mkDefault` so the operator's own agent.nix can # ./mcp.nix. `lib.mkDefault` so the operator's own agent.nix can
# override it. Points at the daemon's own persistent # override it. Points at the daemon's own persistent
@ -260,13 +296,14 @@ in
// lib.optionalAttrs (config.services.hyperhive.agent.matrix.url != null) { // lib.optionalAttrs (config.services.hyperhive.agent.matrix.url != null) {
HIVE_MATRIX_URL = config.services.hyperhive.agent.matrix.url; HIVE_MATRIX_URL = config.services.hyperhive.agent.matrix.url;
} }
# Multi-account: serialize the *extra* accounts to the JSON the # Serialize the whole account set --- `main` included --- to the
# daemon parses (`accounts::configured`). Only set when extras are # JSON the daemon parses (`accounts::configured`). Each entry is in
# declared; the daemon always synthesizes the primary `main` # the daemon's `AccountCfg` serde shape: name (the attr key) /
# (hive-internal) account itself from the per-agent paths and # token_file / state_dir / optional homeserver. The daemon hoists
# prepends it, so we emit extras only. Each entry is in the # `main` to primary wherever the attr key sorted, and falls back to
# daemon's `AccountCfg` serde shape: name (the attr key) / # synthesizing it from the per-agent paths only when this JSON
# token_file / state_dir / optional homeserver. # carries no `main` --- which is how an agent whose harness
# predates this entry keeps working.
// lib.optionalAttrs (config.services.hyperhive.agent.matrixAccounts != { }) { // lib.optionalAttrs (config.services.hyperhive.agent.matrixAccounts != { }) {
HIVE_MATRIX_ACCOUNTS = builtins.toJSON ( HIVE_MATRIX_ACCOUNTS = builtins.toJSON (
lib.mapAttrsToList ( lib.mapAttrsToList (
@ -325,7 +362,7 @@ in
# sibling's token still matches, and it fires again until the start # sibling's token still matches, and it fires again until the start
# limit stops it. Scoped to this agent, the condition is false exactly # limit stops it. Scoped to this agent, the condition is false exactly
# when the daemon would have nothing to do. # when the daemon would have nothing to do.
pathConfig.PathExistsGlob = "/agents/${userName}/state/matrix-token*"; pathConfig.PathExistsGlob = "${stateDir}/matrix-token*";
}; };
}; };
} }

View file

@ -163,8 +163,8 @@ pub async fn put_matrix_account(
let secret_path = matrix::account_path(&agent, &account) let secret_path = matrix::account_path(&agent, &account)
.map_err(|e| error_problem(StatusCode::BAD_REQUEST, &e.to_string()))?; .map_err(|e| error_problem(StatusCode::BAD_REQUEST, &e.to_string()))?;
// `main` is the hive-internal account `nix/agent-modules/matrix.nix` // `main` is the hive-internal account `nix/agent-modules/matrix.nix`
// synthesizes per agent from `services.hyperhive.agent.matrix.url` — the schema there // declares per agent from `services.hyperhive.agent.matrix.url` — the
// forbids declaring a key by that name for the same reason this route // module owns that `matrixAccounts` entry, which is why this route
// refuses to write one: an extra account literally named `main` would // refuses to write one: an extra account literally named `main` would
// not overwrite the real one (it lands at a different token-file suffix) // not overwrite the real one (it lands at a different token-file suffix)
// but would confuse anything that lists accounts by name. Same guard // but would confuse anything that lists accounts by name. Same guard
@ -173,7 +173,7 @@ pub async fn put_matrix_account(
if is_reserved_account(&account) { if is_reserved_account(&account) {
return Err(error_problem( return Err(error_problem(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"'main' is the hive-internal account, synthesized per agent from \ "'main' is the hive-internal account, declared per agent from \
services.hyperhive.agent.matrix.url — it cannot be set through this route.", services.hyperhive.agent.matrix.url — it cannot be set through this route.",
)); ));
} }
@ -236,7 +236,7 @@ pub async fn put_matrix_account(
Ok(Json(PutMatrixAccountResponse { user_id })) Ok(Json(PutMatrixAccountResponse { user_id }))
} }
/// Whether `account` is the hive-internal name every hive synthesizes per /// Whether `account` is the hive-internal name every hive declares per
/// agent (`nix/agent-modules/matrix.nix`) — see the call site's own comment /// agent (`nix/agent-modules/matrix.nix`) — see the call site's own comment
/// for why this route must never write one. /// for why this route must never write one.
fn is_reserved_account(account: &str) -> bool { fn is_reserved_account(account: &str) -> bool {