hive-c0re: render the new agent option paths into generated agent flakes

meta.rs writes each agent's flake, and it still named the pre-move
`hyperhive.*` paths — so every agent rebuild would print a rename
deprecation warning about a line no human wrote and no operator could fix.
A warning nobody can act on trains everyone to ignore the ones that matter,
which is the whole value of the alias shims.

Repoints the FORWARDED_VAR_OPTIONS table and every other emitted option
assignment (otel.*, docs.source, claudeCodePath, github.enable, user.name,
claudeMemoryMaxBytes) to `services.hyperhive.agent.*`, with the test
expectations that pin the rendered text. The flake input named `hyperhive`
(`hyperhive.url`, `hyperhive.inputs.nixpkgs.follows`,
`hyperhive.nixosConfigurations.*`), hive-tier `services.hyperhive.*` paths,
and the `@hyperhive.local` git identity share the word and are untouched.

Also repoints the same option paths where they appear in comments, rustdoc
and runtime message strings across the other crates — a refusal message
naming `hyperhive.allowedRecipients` sends an operator to a path that will
stop existing. Prose under docs/ is deliberately not in this commit.

Refs #4473
This commit is contained in:
atlas 2026-09-17 20:19:30 +02:00
commit 6ea81aae65
30 changed files with 138 additions and 121 deletions

View file

@ -150,7 +150,7 @@ impl AgentServer {
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String { async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
let log = format!("{args:?}"); let log = format!("{args:?}");
let to = args.to.clone(); let to = args.to.clone();
// Check per-agent allow-list (hyperhive.allowedRecipients). When no // Check per-agent allow-list (services.hyperhive.agent.allowedRecipients). When no
// policy file is present (e.g. manager containers) the check is a no-op. // policy file is present (e.g. manager containers) the check is a no-op.
if let Err(refusal) = crate::send_allow::check_send_allowed(&to) { if let Err(refusal) = crate::send_allow::check_send_allowed(&to) {
return run_tool_envelope("send", log, async move { refusal }).await; return run_tool_envelope("send", log, async move { refusal }).await;

View file

@ -318,7 +318,7 @@ pub(super) struct MatrixRoomUnread {
} }
/// Default port `hive-matrix-daemon` serves its MCP + status endpoints /// Default port `hive-matrix-daemon` serves its MCP + status endpoints
/// on (`hyperhive.mcp.matrixHttpPort`'s nix default). Overridable via /// on (`services.hyperhive.agent.mcp.matrixHttpPort`'s nix default). Overridable via
/// `HIVE_MATRIX_HTTP_PORT` for parity with the port options nix already /// `HIVE_MATRIX_HTTP_PORT` for parity with the port options nix already
/// exposes; unset in practice since a single fixed port is safe (each /// exposes; unset in practice since a single fixed port is safe (each
/// agent container is its own network namespace — see docs/networking/network.md). /// agent container is its own network namespace — see docs/networking/network.md).

View file

@ -49,7 +49,7 @@ pub fn check_send_allowed(to: &str) -> Result<(), String> {
return Ok(()); return Ok(());
} }
Err(format!( Err(format!(
"send refused: recipient '{to}' not in hyperhive.allowedRecipients \ "send refused: recipient '{to}' not in services.hyperhive.agent.allowedRecipients \
(configured in agent.nix). Allowed: {allow:?}. Your structural \ (configured in agent.nix). Allowed: {allow:?}. Your structural \
parent is always reachable route through `send(to: \"{}\", …)` \ parent is always reachable route through `send(to: \"{}\", …)` \
if you need to reach someone outside the allow-list.", if you need to reach someone outside the allow-list.",

View file

@ -1,4 +1,4 @@
//! Shared spec for `hyperhive.extraMcpServers` entries: the on-disk JSON //! Shared spec for `services.hyperhive.agent.extraMcpServers` entries: the on-disk JSON
//! shape the nix module (`nix/agent-modules/mcp.nix`) renders to //! shape the nix module (`nix/agent-modules/mcp.nix`) renders to
//! `/etc/hyperhive/extra-mcp.json`, plus the parsing and per-entry //! `/etc/hyperhive/extra-mcp.json`, plus the parsing and per-entry
//! JSON-rendering logic every consumer needs. Lives in `hive-agent-sock` //! JSON-rendering logic every consumer needs. Lives in `hive-agent-sock`
@ -16,7 +16,7 @@ use std::path::Path;
/// `--mcp-config`. /// `--mcp-config`.
pub const EXTRA_MCP_PATH: &str = "/etc/hyperhive/extra-mcp.json"; pub const EXTRA_MCP_PATH: &str = "/etc/hyperhive/extra-mcp.json";
/// An extra MCP server declared via `hyperhive.extraMcpServers`. Two /// An extra MCP server declared via `services.hyperhive.agent.extraMcpServers`. Two
/// transports: `Stdio` (the consumer spawns `command` fresh each turn, talks /// transports: `Stdio` (the consumer spawns `command` fresh each turn, talks
/// JSON-RPC over its stdin/stdout) and `Http` (point claude at a long-lived /// JSON-RPC over its stdin/stdout) and `Http` (point claude at a long-lived
/// streamable-http `url` instead — no per-turn spawn, no re-registration /// streamable-http `url` instead — no per-turn spawn, no re-registration
@ -55,7 +55,7 @@ impl ExtraMcpServer {
} }
/// Whether this entry is opted in to subagent sessions /// Whether this entry is opted in to subagent sessions
/// (`hyperhive.extraMcpServers.<name>.availableToSubagents`, default /// (`services.hyperhive.agent.extraMcpServers.<name>.availableToSubagents`, default
/// `false`). Subagents run `--strict-mcp-config` with no discovery, so /// `false`). Subagents run `--strict-mcp-config` with no discovery, so
/// this flag is the only path an entry reaches a subagent's own /// this flag is the only path an entry reaches a subagent's own
/// `--mcp-config` at all — see `hive-subagent-mcp`'s `mcp_config` /// `--mcp-config` at all — see `hive-subagent-mcp`'s `mcp_config`

View file

@ -16,7 +16,7 @@
//! signals its own turn loop directly, so hive-c0re is not in either path — //! signals its own turn loop directly, so hive-c0re is not in either path —
//! no broker round-trip, no long-poll, no marker files. //! no broker round-trip, no long-poll, no marker files.
//! //!
//! **`extra_mcp`**: the `hyperhive.extraMcpServers` spec + parsing, shared by //! **`extra_mcp`**: the `services.hyperhive.agent.extraMcpServers` spec + parsing, shared by
//! `hive-agent` (the main session's `--mcp-config`) and `hive-subagent-mcp` //! `hive-agent` (the main session's `--mcp-config`) and `hive-subagent-mcp`
//! (the subagent-eligible subset) — not a socket protocol at all, but this //! (the subagent-eligible subset) — not a socket protocol at all, but this
//! crate is the natural home since both consumers already depend on it. //! crate is the natural home since both consumers already depend on it.

View file

@ -385,7 +385,7 @@ impl Bus {
} }
}; };
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY); let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
// Priority: HIVE_DEFAULT_MODEL (from hyperhive.model in agent.nix) > // Priority: HIVE_DEFAULT_MODEL (from services.hyperhive.agent.model in agent.nix) >
// persisted runtime override > compiled-in DEFAULT_MODEL. // persisted runtime override > compiled-in DEFAULT_MODEL.
// The nix config always wins on rebuild; the persisted file is kept // The nix config always wins on rebuild; the persisted file is kept
// for within-session tracking only (see persist_model / set_model). // for within-session tracking only (see persist_model / set_model).

View file

@ -189,7 +189,7 @@ pub(crate) fn write_api_key_mode(enabled: bool) {
pub const DEFAULT_MODEL: &str = "haiku"; pub const DEFAULT_MODEL: &str = "haiku";
/// Return the model declared in `HIVE_DEFAULT_MODEL` (set from /// Return the model declared in `HIVE_DEFAULT_MODEL` (set from
/// `hyperhive.model` in `agent.nix`), or `None` if the env var is absent / /// `services.hyperhive.agent.model` in `agent.nix`), or `None` if the env var is absent /
/// empty. When `Some`, this takes precedence over any persisted runtime /// empty. When `Some`, this takes precedence over any persisted runtime
/// override so that nix config changes always take effect on rebuild. /// override so that nix config changes always take effect on rebuild.
#[must_use] #[must_use]
@ -216,7 +216,7 @@ pub fn is_valid_effort(level: &str) -> bool {
} }
/// Return the effort level declared in `HIVE_DEFAULT_EFFORT` (set from /// Return the effort level declared in `HIVE_DEFAULT_EFFORT` (set from
/// `hyperhive.effortLevel` in `agent.nix`), or `None` if absent / empty. /// `services.hyperhive.agent.effortLevel` in `agent.nix`), or `None` if absent / empty.
/// Mirrors [`configured_model`]'s env shape. Unlike model, the persisted /// Mirrors [`configured_model`]'s env shape. Unlike model, the persisted
/// runtime override takes precedence over this baseline (see `Bus::new`): /// runtime override takes precedence over this baseline (see `Bus::new`):
/// the operator's effort pick sticks across harness restart, with the nix /// the operator's effort pick sticks across harness restart, with the nix

View file

@ -119,7 +119,7 @@ impl LoginState {
/// Whether this agent is configured to authenticate to its backend with an /// Whether this agent is configured to authenticate to its backend with an
/// API key rather than a Claude OAuth session — set by /// API key rather than a Claude OAuth session — set by
/// `hyperhive.useApiKey` via the `HIVE_USE_API_KEY` env var /// `services.hyperhive.agent.useApiKey` via the `HIVE_USE_API_KEY` env var
/// (`nix/agent-modules/agent-service.nix`). Read fresh on every call rather /// (`nix/agent-modules/agent-service.nix`). Read fresh on every call rather
/// than cached: it's an env var, not a value worth a `OnceLock` for. /// than cached: it's an env var, not a value worth a `OnceLock` for.
#[must_use] #[must_use]

View file

@ -1,7 +1,7 @@
//! Claude launch-config layer: resolves the agent's tool-group / capability //! Claude launch-config layer: resolves the agent's tool-group / capability
//! set into the `--allowedTools` / `--tools` argument strings and renders the //! set into the `--allowedTools` / `--tools` argument strings and renders the
//! `--mcp-config` blob claude reads at spawn (built-in hyperhive server + //! `--mcp-config` blob claude reads at spawn (built-in hyperhive server +
//! any `hyperhive.extraMcpServers`). Pure config-string generation consumed by //! any `services.hyperhive.agent.extraMcpServers`). Pure config-string generation consumed by
//! [`crate::turn`] when it builds the claude command. It never touches the //! [`crate::turn`] when it builds the claude command. It never touches the
//! running MCP server (a separate binary) — the `send` allow-list check that //! running MCP server (a separate binary) — the `send` allow-list check that
//! server enforces lives alongside it in the `hive-agent-mcp` crate. //! server enforces lives alongside it in the `hive-agent-mcp` crate.
@ -12,7 +12,7 @@ pub const SERVER_NAME: &str = "hyperhive";
/// Default loopback port the built-in hyperhive MCP surface is served on /// Default loopback port the built-in hyperhive MCP surface is served on
/// (streamable HTTP, via the persistent `hive-mcp-http` daemon). Overridable /// (streamable HTTP, via the persistent `hive-mcp-http` daemon). Overridable
/// via `hyperhive.mcp.httpPort`; **must match that option's default** in /// via `services.hyperhive.agent.mcp.httpPort`; **must match that option's default** in
/// `nix/agent-modules/mcp.nix`. Safe as a single fixed value across all /// `nix/agent-modules/mcp.nix`. Safe as a single fixed value across all
/// agents because each container runs in its own private network namespace, /// agents because each container runs in its own private network namespace,
/// so `127.0.0.1:<port>` is per-container-private (no cross-agent collision). /// so `127.0.0.1:<port>` is per-container-private (no cross-agent collision).
@ -65,7 +65,7 @@ fn allowed_capability_tools() -> Vec<String> {
/// Tool group an extra (out-of-process) MCP server is gated behind, if any. /// Tool group an extra (out-of-process) MCP server is gated behind, if any.
/// ///
/// Most `hyperhive.extraMcpServers` entries are ungated — available whenever /// Most `services.hyperhive.agent.extraMcpServers` entries are ungated — available whenever
/// the operator declares them. The `bash` server is the exception: raw shell /// the operator declares them. The `bash` server is the exception: raw shell
/// execution is a privilege, so it is only exposed when the agent holds the /// execution is a privilege, so it is only exposed when the agent holds the
/// `Execution` tool group. Unlike the in-process hyperhive tools (gated at /// `Execution` tool group. Unlike the in-process hyperhive tools (gated at
@ -135,7 +135,7 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::ToolGroup]) -> Vec<S
.filter(|t| seen.insert(*t)) .filter(|t| seen.insert(*t))
.map(|t| format!("mcp__{SERVER_NAME}__{t}")) .map(|t| format!("mcp__{SERVER_NAME}__{t}"))
.collect(); .collect();
// Extra MCP servers declared via `hyperhive.extraMcpServers` in // Extra MCP servers declared via `services.hyperhive.agent.extraMcpServers` in
// the agent's NixOS config. Each entry maps its `allowedTools` // the agent's NixOS config. Each entry maps its `allowedTools`
// pattern list to `mcp__<server>__<pattern>` so claude can call // pattern list to `mcp__<server>__<pattern>` so claude can call
// them without per-tool operator approval. `["*"]` (the default) // them without per-tool operator approval. `["*"]` (the default)
@ -177,7 +177,7 @@ pub fn allowed_tools_arg() -> String {
/// The built-in `hyperhive` surface is an HTTP entry pointing at the /// The built-in `hyperhive` surface is an HTTP entry pointing at the
/// persistent `hive-mcp-http` daemon (see [`DEFAULT_MCP_HTTP_PORT`]); there /// persistent `hive-mcp-http` daemon (see [`DEFAULT_MCP_HTTP_PORT`]); there
/// is no per-turn stdio child for it. Merges in any extra MCP servers /// is no per-turn stdio child for it. Merges in any extra MCP servers
/// declared via `hyperhive.extraMcpServers` — each one is either a /// declared via `services.hyperhive.agent.extraMcpServers` — each one is either a
/// per-turn stdio bridge or another persistent HTTP entry, per its own /// per-turn stdio bridge or another persistent HTTP entry, per its own
/// `type`. /// `type`.
#[must_use] #[must_use]

View file

@ -1,5 +1,5 @@
//! Boot-time `claude plugin install` driver. Reads the list declared //! Boot-time `claude plugin install` driver. Reads the list declared
//! via the `hyperhive.claudePlugins` NixOS option (rendered to //! via the `services.hyperhive.agent.claudePlugins` NixOS option (rendered to
//! `/etc/hyperhive/claude-plugins.json` by the harness module) and //! `/etc/hyperhive/claude-plugins.json` by the harness module) and
//! shells out `claude plugin install <spec>` for each entry. Runs once //! shells out `claude plugin install <spec>` for each entry. Runs once
//! per harness boot before the turn loop; `claude plugin install` //! per harness boot before the turn loop; `claude plugin install`
@ -62,7 +62,7 @@ async fn add_marketplaces() {
} }
} }
/// Read the `hyperhive.claudePluginsAutoUpdate` flag written by the NixOS /// Read the `services.hyperhive.agent.claudePluginsAutoUpdate` flag written by the NixOS
/// module. Defaults to `false` when the file is absent or unparseable. /// module. Defaults to `false` when the file is absent or unparseable.
async fn auto_update_enabled() -> bool { async fn auto_update_enabled() -> bool {
match tokio::fs::read_to_string(AUTO_UPDATE_PATH).await { match tokio::fs::read_to_string(AUTO_UPDATE_PATH).await {

View file

@ -40,7 +40,7 @@ pub fn render(
.replace("{operator_pronouns}", operator_pronouns) .replace("{operator_pronouns}", operator_pronouns)
.replace("{hive_identity}", &hive_identity) .replace("{hive_identity}", &hive_identity)
.replace("{swarm_identity}", &swarm_identity); .replace("{swarm_identity}", &swarm_identity);
// When the reference docs are mounted in-container (`hyperhive.docs.enable` // When the reference docs are mounted in-container (`services.hyperhive.agent.docs.enable`
// wires `HIVE_DOCS_DIR` + `claude --add-dir`), append a single pointer // wires `HIVE_DOCS_DIR` + `claude --add-dir`), append a single pointer
// sentence so the agent knows they exist. Additive: it doesn't replace the // sentence so the agent knows they exist. Additive: it doesn't replace the
// agent's own memory/project instructions. Absent env → no change. // agent's own memory/project instructions. Absent env → no change.
@ -129,7 +129,7 @@ pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf>
// verbatim (single-hive deployments see no diff). // verbatim (single-hive deployments see no diff).
let hive_name = crate::identity::hive_name(); let hive_name = crate::identity::hive_name();
let swarm_name = crate::identity::swarm_name(); let swarm_name = crate::identity::swarm_name();
// `hyperhive.docs.enable` sets HIVE_DOCS_DIR (and the harness passes it to // `services.hyperhive.agent.docs.enable` sets HIVE_DOCS_DIR (and the harness passes it to
// claude via `--add-dir`); when present, render() appends a pointer line. // claude via `--add-dir`); when present, render() appends a pointer line.
let docs_dir = std::env::var("HIVE_DOCS_DIR") let docs_dir = std::env::var("HIVE_DOCS_DIR")
.ok() .ok()

View file

@ -122,7 +122,7 @@ impl TurnFiles {
/// Drop the MCP config blob claude reads from `--mcp-config <path>`. /// Drop the MCP config blob claude reads from `--mcp-config <path>`.
/// The built-in hyperhive surface is served over HTTP by the persistent /// The built-in hyperhive surface is served over HTTP by the persistent
/// `hive-mcp-http` daemon, so no per-turn stdio child is spawned; extra /// `hive-mcp-http` daemon, so no per-turn stdio child is spawned; extra
/// servers declared via `hyperhive.extraMcpServers` are still stdio bridges. /// servers declared via `services.hyperhive.agent.extraMcpServers` are still stdio bridges.
/// ///
/// # Errors /// # Errors
/// ///
@ -576,7 +576,7 @@ fn session_store() -> hive_claude::SessionStore {
/// tool allow-lists and the optional docs `--add-dir`. /// tool allow-lists and the optional docs `--add-dir`.
fn claude_config(bus: &Bus, files: &TurnFiles) -> Config { fn claude_config(bus: &Bus, files: &TurnFiles) -> Config {
let mut add_dirs = Vec::new(); let mut add_dirs = Vec::new();
// hyperhive.docs.enable wires HIVE_DOCS_DIR to the in-container reference // services.hyperhive.agent.docs.enable wires HIVE_DOCS_DIR to the in-container reference
// docs; expose it as an additional readable directory when set. // docs; expose it as an additional readable directory when set.
if let Some(docs_dir) = std::env::var_os("HIVE_DOCS_DIR") if let Some(docs_dir) = std::env::var_os("HIVE_DOCS_DIR")
&& !docs_dir.is_empty() && !docs_dir.is_empty()

View file

@ -92,7 +92,7 @@ pub async fn serve(
.map(PathBuf::from) .map(PathBuf::from)
.context( .context(
"HIVE_STATIC_DIR env var not set — point it at the merged \ "HIVE_STATIC_DIR env var not set — point it at the merged \
per-agent dist (see hyperhive.frontend.mergedDist in nix)", per-agent dist (see services.hyperhive.agent.frontend.mergedDist in nix)",
)?; )?;
if !static_dir.is_dir() { if !static_dir.is_dir() {
anyhow::bail!( anyhow::bail!(
@ -130,7 +130,7 @@ pub async fn serve(
.route("/api/stats", get(stats::api_stats)) .route("/api/stats", get(stats::api_stats))
.route("/screen/ws", get(screen::screen_ws)) .route("/screen/ws", get(screen::screen_ws))
.route("/icon", get(screen::serve_icon)); .route("/icon", get(screen::serve_icon));
// Mount any `hyperhive.extraWebProxies` under `/extra/<name>/` before the // Mount any `services.hyperhive.agent.extraWebProxies` under `/extra/<name>/` before the
// static fallback so declared proxies win over `ServeDir`. // static fallback so declared proxies win over `ServeDir`.
let app = proxy::mount_extra_proxies(app) let app = proxy::mount_extra_proxies(app)
// Anything else (`/`, `/stats`, `/screen`, `/static/*`) // Anything else (`/`, `/stats`, `/screen`, `/static/*`)
@ -139,7 +139,7 @@ pub async fn serve(
// (so `/stats` → `dist/stats.html`, `/screen` → `dist/ // (so `/stats` → `dist/stats.html`, `/screen` → `dist/
// screen.html`). Per-agent `extraFiles` additions are // screen.html`). Per-agent `extraFiles` additions are
// already layered into this same directory (see // already layered into this same directory (see
// hyperhive.frontend.mergedDist in nix). // services.hyperhive.agent.frontend.mergedDist in nix).
.fallback_service(ServeDir::new(&static_dir)) .fallback_service(ServeDir::new(&static_dir))
.with_state(state); .with_state(state);
// `HIVE_WEB_SOCKET` opt-in: when set + non-empty, bind a // `HIVE_WEB_SOCKET` opt-in: when set + non-empty, bind a

View file

@ -1,4 +1,4 @@
//! Extra web proxies (`HIVE_EXTRA_WEB_PROXIES` / `hyperhive.extraWebProxies`). //! Extra web proxies (`HIVE_EXTRA_WEB_PROXIES` / `services.hyperhive.agent.extraWebProxies`).
//! //!
//! Each declared entry mounts a transparent reverse-proxy at `/extra/<name>/` //! Each declared entry mounts a transparent reverse-proxy at `/extra/<name>/`
//! in the per-agent web UI, forwarding every request (method, headers, body) //! in the per-agent web UI, forwarding every request (method, headers, body)

View file

@ -10,7 +10,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use super::AppState; use super::AppState;
/// This agent's icon. Serves the operator-configured SVG from /// This agent's icon. Serves the operator-configured SVG from
/// `/etc/hyperhive/icon.svg` (set via the `hyperhive.icon` agent.nix /// `/etc/hyperhive/icon.svg` (set via the `services.hyperhive.agent.icon` agent.nix
/// option) when present, otherwise **404** — there is no bundled /// option) when present, otherwise **404** — there is no bundled
/// server-side default any more (that was `hive_sh4re::assets:: /// server-side default any more (that was `hive_sh4re::assets::
/// branding_svg`, now removed). Consumers fall back client-side: the /// branding_svg`, now removed). Consumers fall back client-side: the

View file

@ -255,7 +255,7 @@ enum AgentLinkKind {
/// `http://<host>:3000<url>`. /// `http://<host>:3000<url>`.
Forge, Forge,
/// `url` is already a fully-qualified absolute URL — use as-is. /// `url` is already a fully-qualified absolute URL — use as-is.
/// Agent-declared `hyperhive.dashboardLinks` extras arrive here. /// Agent-declared `services.hyperhive.agent.dashboardLinks` extras arrive here.
External, External,
} }
@ -366,7 +366,7 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
}); });
} }
// Agent-declared extras (`hyperhive.dashboardLinks` → the // Agent-declared extras (`services.hyperhive.agent.dashboardLinks` → the
// `hive-dashboard-links` NixOS oneshot writes them to // `hive-dashboard-links` NixOS oneshot writes them to
// `{state_dir}/hyperhive-dashboard-links.json`). Shape on disk // `{state_dir}/hyperhive-dashboard-links.json`). Shape on disk
// is `{label, icon, url}` with absolute URLs — those become // is `{label, icon, url}` with absolute URLs — those become

View file

@ -202,7 +202,7 @@ pub async fn build_all(hive: &crate::coordinator::HiveEnv) -> Vec<ContainerView>
// stale sentinel state is not meaningful when the harness isn't up. // stale sentinel state is not meaningful when the harness isn't up.
// //
// The first half doesn't apply to an api-key agent // The first half doesn't apply to an api-key agent
// (`hyperhive.useApiKey`, stamped as `api_key_mode` in the // (`services.hyperhive.agent.useApiKey`, stamped as `api_key_mode` in the
// consolidated state file by `hive_agent::harness_state::write_api_key_mode`): // consolidated state file by `hive_agent::harness_state::write_api_key_mode`):
// its `~/.claude/` is empty by design (no OAuth flow to complete), // its `~/.claude/` is empty by design (no OAuth flow to complete),
// so an empty dir there means nothing — only the auth-failed // so an empty dir there means nothing — only the auth-failed

View file

@ -47,7 +47,7 @@ pub struct Coordinator {
pub hyperhive_flake: String, pub hyperhive_flake: String,
/// URL of the narrow `docs/` source (no fragment). Inlined into the /// URL of the narrow `docs/` source (no fragment). Inlined into the
/// meta `flake.nix` as `inputs.hyperhive-docs.url` and threaded to /// meta `flake.nix` as `inputs.hyperhive-docs.url` and threaded to
/// each agent as `hyperhive.docs.source`. Its own store path /// each agent as `services.hyperhive.agent.docs.source`. Its own store path
/// so doc edits don't re-hash `hyperhive_flake`. /// so doc edits don't re-hash `hyperhive_flake`.
pub hyperhive_docs_flake: String, pub hyperhive_docs_flake: String,
/// Store-path URL of the nixpkgs to wire into the meta flake as /// Store-path URL of the nixpkgs to wire into the meta flake as
@ -59,7 +59,7 @@ pub struct Coordinator {
/// `follows = "hyperhive/nixpkgs"` behaviour. /// `follows = "hyperhive/nixpkgs"` behaviour.
pub nixpkgs_flake: String, pub nixpkgs_flake: String,
/// Store path of the `claude-code` build every agent runs, written /// Store path of the `claude-code` build every agent runs, written
/// into each per-agent flake as `hyperhive.claudeCodePath`. Set by /// into each per-agent flake as `services.hyperhive.agent.claudeCodePath`. Set by
/// the NixOS module option `services.hyperhive.c0re.claudeCodePackage` /// the NixOS module option `services.hyperhive.c0re.claudeCodePackage`
/// (which resolves the package and hands us its path). `None` = every /// (which resolves the package and hands us its path). `None` = every
/// agent keeps the `claude-code` from its own nixpkgs. /// agent keeps the `claude-code` from its own nixpkgs.
@ -228,13 +228,13 @@ pub struct HiveEnv {
pub hyperhive_flake: String, pub hyperhive_flake: String,
/// Store-path URL of the narrow `docs/` source, wired into the meta /// Store-path URL of the narrow `docs/` source, wired into the meta
/// flake as `inputs.hyperhive-docs.url` and threaded to each agent as /// flake as `inputs.hyperhive-docs.url` and threaded to each agent as
/// `hyperhive.docs.source`. Separate from `hyperhive_flake` /// `services.hyperhive.agent.docs.source`. Separate from `hyperhive_flake`
/// so a doc edit only re-locks this input, not the whole source. /// so a doc edit only re-locks this input, not the whole source.
pub hyperhive_docs_flake: String, pub hyperhive_docs_flake: String,
pub nixpkgs_flake: String, pub nixpkgs_flake: String,
/// Store path of the `claude-code` agents run, or `None` for "each /// Store path of the `claude-code` agents run, or `None` for "each
/// agent keeps the one out of its own nixpkgs". Travels into the /// agent keeps the one out of its own nixpkgs". Travels into the
/// container as `hyperhive.claudeCodePath` — a plain string, kept /// container as `services.hyperhive.agent.claudeCodePath` — a plain string, kept
/// alive host-side by the module that resolved it. /// alive host-side by the module that resolved it.
pub claude_code_path: Option<String>, pub claude_code_path: Option<String>,
pub dashboard_port: u16, pub dashboard_port: u16,

View file

@ -739,21 +739,27 @@ const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
// whose option says no queue coordinates and whose env says otherwise logs the // whose option says no queue coordinates and whose env says otherwise logs the
// partial-config error rather than half-connecting, which is the same // partial-config error rather than half-connecting, which is the same
// failure this map's doc describes and the reason both halves are wired. // failure this map's doc describes and the reason both halves are wired.
("HIVE_AGENT_NATS_URL", "hyperhive.queue.natsUrl"), (
"HIVE_AGENT_NATS_URL",
"services.hyperhive.agent.queue.natsUrl",
),
( (
"HIVE_AGENT_OIDC_TOKEN_ENDPOINT", "HIVE_AGENT_OIDC_TOKEN_ENDPOINT",
"hyperhive.queue.tokenEndpoint", "services.hyperhive.agent.queue.tokenEndpoint",
), ),
// Read at build time to decide whether `swarm-logs` is installed at all — // Read at build time to decide whether `swarm-logs` is installed at all —
// see `nix/agent-modules/logs.nix`. Both halves wired for this map's own // see `nix/agent-modules/logs.nix`. Both halves wired for this map's own
// reason: an agent whose option says no log store and whose env says // reason: an agent whose option says no log store and whose env says
// otherwise has a binary that is not on PATH and coordinates that claim // otherwise has a binary that is not on PATH and coordinates that claim
// it is. // it is.
("HIVE_AGENT_LOGS_QUERY_URL", "hyperhive.logs.queryUrl"), (
("HIVE_FORGE_URL", "hyperhive.forge.url"), "HIVE_AGENT_LOGS_QUERY_URL",
("HIVE_MATRIX_URL", "hyperhive.matrix.url"), "services.hyperhive.agent.logs.queryUrl",
("HYPERHIVE_HIVE_NAME", "hyperhive.hiveName"), ),
("HYPERHIVE_SWARM_NAME", "hyperhive.swarmName"), ("HIVE_FORGE_URL", "services.hyperhive.agent.forge.url"),
("HIVE_MATRIX_URL", "services.hyperhive.agent.matrix.url"),
("HYPERHIVE_HIVE_NAME", "services.hyperhive.agent.hiveName"),
("HYPERHIVE_SWARM_NAME", "services.hyperhive.agent.swarmName"),
]; ];
/// Render the forwarded-var option assignments for one agent's module block. /// Render the forwarded-var option assignments for one agent's module block.
@ -874,17 +880,17 @@ struct OtelConfig {
protocol: String, protocol: String,
extra_resource_attributes: Option<String>, extra_resource_attributes: Option<String>,
metric_interval_ms: Option<u64>, metric_interval_ms: Option<u64>,
/// `HYPERHIVE_OTEL_DEBUG=1` → `hyperhive.otel.debug = true` → /// `HYPERHIVE_OTEL_DEBUG=1` → `services.hyperhive.agent.otel.debug = true` →
/// `CLAUDE_CODE_OTEL_DIAG_STDERR=1` in every agent's env. /// `CLAUDE_CODE_OTEL_DIAG_STDERR=1` in every agent's env.
debug: bool, debug: bool,
} }
/// Read the hive-wide OTEL config from env, or `None` when OTEL is off. /// Read the hive-wide OTEL config from env, or `None` when OTEL is off.
/// Mirrors `hive_ca_source` — host state surfaced to the meta renderer /// Mirrors `hive_ca_source` — host state surfaced to the meta renderer
/// so it can bake build-time `hyperhive.otel.*` config into each agent /// so it can bake build-time `services.hyperhive.agent.otel.*` config into each agent
/// (the per-agent options the harness modules consume). Returns `None` /// (the per-agent options the harness modules consume). Returns `None`
/// when the endpoint signal is absent so the renderer emits no /// when the endpoint signal is absent so the renderer emits no
/// `hyperhive.otel.*` lines and agents keep the disabled default. /// `services.hyperhive.agent.otel.*` lines and agents keep the disabled default.
fn otel_config() -> Option<OtelConfig> { fn otel_config() -> Option<OtelConfig> {
let endpoint = std::env::var("HYPERHIVE_OTEL_ENDPOINT") let endpoint = std::env::var("HYPERHIVE_OTEL_ENDPOINT")
.ok() .ok()
@ -1101,9 +1107,9 @@ where
} }
// Narrow `docs/` source as its own input so a doc edit only // Narrow `docs/` source as its own input so a doc edit only
// re-locks THIS input instead of re-hashing the whole `hyperhive` // re-locks THIS input instead of re-hashing the whole `hyperhive`
// source. Threaded to each agent below as `hyperhive.docs.source`. // source. Threaded to each agent below as `services.hyperhive.agent.docs.source`.
// Empty = hive-c0re not built with the option wired up (legacy); // Empty = hive-c0re not built with the option wired up (legacy);
// agents then keep the harness default (`hyperhive.packages.reference-docs`). // agents then keep the harness default (`services.hyperhive.agent.packages.reference-docs`).
if !docs_flake.is_empty() { if !docs_flake.is_empty() {
// `flake = false`: the docs/ tree is a plain source (no flake.nix), // `flake = false`: the docs/ tree is a plain source (no flake.nix),
// so nix must treat it as raw source, not evaluate it as a flake. // so nix must treat it as raw source, not evaluate it as a flake.
@ -1169,11 +1175,11 @@ where
); );
// Point the in-container docs dir (`$HIVE_DOCS_DIR`) at the narrow // Point the in-container docs dir (`$HIVE_DOCS_DIR`) at the narrow
// `hyperhive-docs` input instead of the harness default // `hyperhive-docs` input instead of the harness default
// (`hyperhive.packages.reference-docs`, built from the now-docs-stripped source). // (`services.hyperhive.agent.packages.reference-docs`, built from the now-docs-stripped source).
// `inputs."hyperhive-docs"` is reachable via the outputs `@inputs` // `inputs."hyperhive-docs"` is reachable via the outputs `@inputs`
// capture. Emitted only when the input exists (docs_flake non-empty). // capture. Emitted only when the input exists (docs_flake non-empty).
if !docs_flake.is_empty() { if !docs_flake.is_empty() {
out.push_str(" hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;\n"); out.push_str(" services.hyperhive.agent.docs.source = inputs.\"hyperhive-docs\".outPath;\n");
} }
// The `claude-code` agents run, as a bare store path rather than a // The `claude-code` agents run, as a bare store path rather than a
// flake input: containers share the host's `/nix/store`, so the // flake input: containers share the host's `/nix/store`, so the
@ -1185,7 +1191,10 @@ where
// spelled out here is text and references nothing. // spelled out here is text and references nothing.
// `None` = no override; agents keep their own nixpkgs' `claude-code`. // `None` = no override; agents keep their own nixpkgs' `claude-code`.
if let Some(path) = claude_code_path { if let Some(path) = claude_code_path {
let _ = writeln!(out, " hyperhive.claudeCodePath = \"{path}\";"); let _ = writeln!(
out,
" services.hyperhive.agent.claudeCodePath = \"{path}\";"
);
} }
// CA trust: embed the hive's trust anchors so each agent validates // CA trust: embed the hive's trust anchors so each agent validates
// them at build time — the hive's own self-signed CA when active, // them at build time — the hive's own self-signed CA when active,
@ -1209,41 +1218,44 @@ where
); );
} }
// Hive-wide OTEL stats export (`services.hyperhive.otel.*`): inject the // Hive-wide OTEL stats export (`services.hyperhive.otel.*`): inject the
// build-time `hyperhive.otel.*` config the harness modules consume (its // build-time `services.hyperhive.agent.otel.*` config the harness modules consume (its
// otelEnv + otelExecStart wrapper + LoadCredential). Host-driven, so // otelEnv + otelExecStart wrapper + LoadCredential). Host-driven, so
// the same config lands on every agent; emitted only when enabled. // the same config lands on every agent; emitted only when enabled.
// Mirrors the CA-cert injection above — host state -> build-time agent // Mirrors the CA-cert injection above — host state -> build-time agent
// module config. // module config.
if let Some(otel) = otel_config() { if let Some(otel) = otel_config() {
let esc = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\""); let esc = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"");
out.push_str(" hyperhive.otel.enable = true;\n"); out.push_str(" services.hyperhive.agent.otel.enable = true;\n");
let _ = writeln!( let _ = writeln!(
out, out,
" hyperhive.otel.endpoint = \"{}\";", " services.hyperhive.agent.otel.endpoint = \"{}\";",
esc(&otel.endpoint) esc(&otel.endpoint)
); );
let _ = writeln!( let _ = writeln!(
out, out,
" hyperhive.otel.protocol = \"{}\";", " services.hyperhive.agent.otel.protocol = \"{}\";",
esc(&otel.protocol) esc(&otel.protocol)
); );
if let Some(attrs) = &otel.extra_resource_attributes { if let Some(attrs) = &otel.extra_resource_attributes {
let _ = writeln!( let _ = writeln!(
out, out,
" hyperhive.otel.extraResourceAttributes = \"{}\";", " services.hyperhive.agent.otel.extraResourceAttributes = \"{}\";",
esc(attrs) esc(attrs)
); );
} }
if let Some(ms) = otel.metric_interval_ms { if let Some(ms) = otel.metric_interval_ms {
// Int option — emit a bare numeric literal (no quotes). `ms` is a // Int option — emit a bare numeric literal (no quotes). `ms` is a
// parsed u64, so it can't inject anything into the rendered nix. // parsed u64, so it can't inject anything into the rendered nix.
let _ = writeln!(out, " hyperhive.otel.metricIntervalMs = {ms};"); let _ = writeln!(
out,
" services.hyperhive.agent.otel.metricIntervalMs = {ms};"
);
} }
if otel.debug { if otel.debug {
out.push_str(" hyperhive.otel.debug = true;\n"); out.push_str(" services.hyperhive.agent.otel.debug = true;\n");
} }
} }
// Agent-facing service URLs (`hyperhive.forge.url`, `hyperhive.matrix.url`): // Agent-facing service URLs (`services.hyperhive.agent.forge.url`, `services.hyperhive.agent.matrix.url`):
// emit the host's real values as build-time agent config, the same // emit the host's real values as build-time agent config, the same
// host-state -> agent-module shape as the otel block above. // host-state -> agent-module shape as the otel block above.
// //
@ -1271,21 +1283,21 @@ where
// operation the tests exercise directly. // operation the tests exercise directly.
push_forwarded_var_options(&mut out, &forwarded_env_vars()); push_forwarded_var_options(&mut out, &forwarded_env_vars());
// GitHub integration is on by default in every agent // GitHub integration is on by default in every agent
// (`hyperhive.github.enable`); the host turns it off hive-wide via // (`services.hyperhive.agent.github.enable`); the host turns it off hive-wide via
// `services.hyperhive.github.enable = false`, surfaced here as the // `services.hyperhive.github.enable = false`, surfaced here as the
// `HYPERHIVE_GITHUB_DISABLED` env on hive-c0re's unit. Only the OFF // `HYPERHIVE_GITHUB_DISABLED` env on hive-c0re's unit. Only the OFF
// override is propagated — the enabled default needs no per-agent line. // override is propagated — the enabled default needs no per-agent line.
if std::env::var_os("HYPERHIVE_GITHUB_DISABLED").is_some() { if std::env::var_os("HYPERHIVE_GITHUB_DISABLED").is_some() {
out.push_str(" hyperhive.github.enable = false;\n"); out.push_str(" services.hyperhive.agent.github.enable = false;\n");
} }
out.push_str( out.push_str(
r#" # The harness service inside the container runs as a r#" # The harness service inside the container runs as a
# non-root unix user named after the agent (`damocles`, # non-root unix user named after the agent (`damocles`,
# `iris`, `root`, ). UID auto-assigned by NixOS; the # `iris`, `root`, ). UID auto-assigned by NixOS; the
# per-agent override here is what makes # per-agent override here is what makes
# `hyperhive.user.name` match the agent's identity # `services.hyperhive.agent.user.name` match the agent's identity
# instead of the harness default of `"agent"`. # instead of the harness default of `"agent"`.
hyperhive.user.name = name; services.hyperhive.agent.user.name = name;
# Without this an agent container is called `nixos` and ships # Without this an agent container is called `nixos` and ships
# every log line under that: nixpkgs sets the hostname from the # every log line under that: nixpkgs sets the hostname from the
# attr name in the `containers.<name>` submodule, and an # attr name in the `containers.<name>` submodule, and an
@ -1295,7 +1307,7 @@ where
# nspawn itself already call this container, so logs join to # nspawn itself already call this container, so logs join to
# both without a prefix transform. # both without a prefix transform.
networking.hostName = "h-${name}"; networking.hostName = "h-${name}";
hyperhive.claudeMemoryMaxBytes = memoryMaxBytes; services.hyperhive.agent.claudeMemoryMaxBytes = memoryMaxBytes;
programs.git.config.user = { programs.git.config.user = {
name = name; name = name;
email = "${name}@hyperhive.local"; email = "${name}@hyperhive.local";
@ -1418,7 +1430,7 @@ where
// Effective `MemoryMax=` for this agent (per-agent override, else // Effective `MemoryMax=` for this agent (per-agent override, else
// the hive-wide default), turned into a raw byte count so // the hive-wide default), turned into a raw byte count so
// the agent's claude-settings module can derive a JSC heap ceiling // the agent's claude-settings module can derive a JSC heap ceiling
// (see `hyperhive.claudeMemoryMaxBytes`). `null` when the // (see `services.hyperhive.agent.claudeMemoryMaxBytes`). `null` when the
// effective value is `"infinity"` or a RAM percentage — no // effective value is `"infinity"` or a RAM percentage — no
// byte count to derive, dependent env var stays unset, same as // byte count to derive, dependent env var stays unset, same as
// today's no-cap behavior. // today's no-cap behavior.
@ -1759,7 +1771,7 @@ mod tests {
"old-style follows must not appear when flake args are set:\n{out}" "old-style follows must not appear when flake args are set:\n{out}"
); );
// the narrow docs source is its own non-flake input, and each // the narrow docs source is its own non-flake input, and each
// agent's docs dir resolves from it rather than hyperhive.packages.reference-docs. // agent's docs dir resolves from it rather than services.hyperhive.agent.packages.reference-docs.
assert!( assert!(
out.contains("hyperhive-docs.url = \"path:/nix/store/bbbb-hyperhive-docs-source\""), out.contains("hyperhive-docs.url = \"path:/nix/store/bbbb-hyperhive-docs-source\""),
"expected hyperhive-docs input url:\n{out}" "expected hyperhive-docs input url:\n{out}"
@ -1769,7 +1781,9 @@ mod tests {
"docs source is not a flake, must be flake = false:\n{out}" "docs source is not a flake, must be flake = false:\n{out}"
); );
assert!( assert!(
out.contains("hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;"), out.contains(
"services.hyperhive.agent.docs.source = inputs.\"hyperhive-docs\".outPath;"
),
"expected per-agent docs source wired to the input:\n{out}" "expected per-agent docs source wired to the input:\n{out}"
); );
} }
@ -1777,7 +1791,7 @@ mod tests {
#[test] #[test]
fn render_flake_omits_docs_input_when_docs_flake_empty() { fn render_flake_omits_docs_input_when_docs_flake_empty() {
// Legacy / not-wired-up: empty docs_flake emits no docs input and // Legacy / not-wired-up: empty docs_flake emits no docs input and
// leaves each agent on the harness default (hyperhive.packages.reference-docs). // leaves each agent on the harness default (services.hyperhive.agent.packages.reference-docs).
let out = render_flake( let out = render_flake(
"github:example/hyperhive", "github:example/hyperhive",
"", "",
@ -1850,7 +1864,9 @@ mod tests {
&[sample_spec("alice", false, 9001)], &[sample_spec("alice", false, 9001)],
); );
assert!( assert!(
out.contains("hyperhive.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"), out.contains(
"services.hyperhive.agent.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"
),
"claude path assigned as a plain string literal:\n{out}" "claude path assigned as a plain string literal:\n{out}"
); );
assert!( assert!(
@ -2046,9 +2062,9 @@ mod tests {
); );
assert_eq!( assert_eq!(
out, out,
" hyperhive.forge.url = \"http://forge.example.test\";\n\ " services.hyperhive.agent.forge.url = \"http://forge.example.test\";\n\
\x20 hyperhive.matrix.url = \"http://matrix.example.test\";\n\ \x20 services.hyperhive.agent.matrix.url = \"http://matrix.example.test\";\n\
\x20 hyperhive.hiveName = \"pr1ma\";\n", \x20 services.hyperhive.agent.hiveName = \"pr1ma\";\n",
"expected exactly the mapped options, in input order, indented for the module block" "expected exactly the mapped options, in input order, indented for the module block"
); );
} }
@ -2079,8 +2095,8 @@ mod tests {
); );
assert_eq!( assert_eq!(
out, out,
" hyperhive.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\ " services.hyperhive.agent.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\
\x20 hyperhive.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n" \x20 services.hyperhive.agent.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n"
); );
} }
@ -2184,11 +2200,11 @@ mod tests {
std::env::remove_var("HIVE_MATRIX_URL"); std::env::remove_var("HIVE_MATRIX_URL");
} }
let forge_opt_at = out let forge_opt_at = out
.find("hyperhive.forge.url = \"http://forge.example.test\"") .find("services.hyperhive.agent.forge.url = \"http://forge.example.test\"")
.expect("hyperhive.forge.url must be rendered from HIVE_FORGE_URL"); .expect("services.hyperhive.agent.forge.url must be rendered from HIVE_FORGE_URL");
let matrix_opt_at = out let matrix_opt_at = out
.find("hyperhive.matrix.url = \"http://matrix.example.test\"") .find("services.hyperhive.agent.matrix.url = \"http://matrix.example.test\"")
.expect("hyperhive.matrix.url must be rendered from HIVE_MATRIX_URL"); .expect("services.hyperhive.agent.matrix.url must be rendered from HIVE_MATRIX_URL");
let env_block_at = out let env_block_at = out
.find("environment.variables = {") .find("environment.variables = {")
.expect("per-agent environment.variables block must exist"); .expect("per-agent environment.variables block must exist");
@ -2332,8 +2348,8 @@ mod tests {
#[test] #[test]
fn render_flake_injects_otel_when_signalled() { fn render_flake_injects_otel_when_signalled() {
// services.hyperhive.otel.* -> HYPERHIVE_OTEL_* on hive-c0re's unit // services.hyperhive.otel.* -> HYPERHIVE_OTEL_* on hive-c0re's unit
// -> injected as build-time hyperhive.otel.* into every agent. With // -> injected as build-time services.hyperhive.agent.otel.* into every agent. With
// no endpoint signal, no hyperhive.otel lines are emitted (agents // no endpoint signal, no services.hyperhive.agent.otel lines are emitted (agents
// keep the the harness modules disabled default). // keep the the harness modules disabled default).
// //
// Serialised against every other env-mutating test in the crate. // Serialised against every other env-mutating test in the crate.
@ -2376,32 +2392,33 @@ mod tests {
let off = render(); let off = render();
assert!( assert!(
on_minimal.contains("hyperhive.otel.enable = true;"), on_minimal.contains("services.hyperhive.agent.otel.enable = true;"),
"otel enable must be injected:\n{on_minimal}" "otel enable must be injected:\n{on_minimal}"
); );
assert!( assert!(
on_minimal.contains("hyperhive.otel.endpoint = \"https://c.example/otel\";"), on_minimal
.contains("services.hyperhive.agent.otel.endpoint = \"https://c.example/otel\";"),
"otel endpoint must be injected:\n{on_minimal}" "otel endpoint must be injected:\n{on_minimal}"
); );
assert!( assert!(
on_minimal.contains("hyperhive.otel.protocol = \"grpc\";"), on_minimal.contains("services.hyperhive.agent.otel.protocol = \"grpc\";"),
"otel protocol must be injected:\n{on_minimal}" "otel protocol must be injected:\n{on_minimal}"
); );
// Optional fields absent when unset. // Optional fields absent when unset.
assert!( assert!(
!on_minimal.contains("hyperhive.otel.extraResourceAttributes"), !on_minimal.contains("services.hyperhive.agent.otel.extraResourceAttributes"),
"extraResourceAttributes must not appear when unset:\n{on_minimal}" "extraResourceAttributes must not appear when unset:\n{on_minimal}"
); );
assert!( assert!(
on_full.contains( on_full.contains(
"hyperhive.otel.extraResourceAttributes = \"deployment.environment=prod\";" "services.hyperhive.agent.otel.extraResourceAttributes = \"deployment.environment=prod\";"
), ),
"extraResourceAttributes must be injected when set:\n{on_full}" "extraResourceAttributes must be injected when set:\n{on_full}"
); );
assert!( assert!(
!off.contains("hyperhive.otel"), !off.contains("services.hyperhive.agent.otel"),
"no otel lines when disabled:\n{off}" "no otel lines when disabled:\n{off}"
); );
} }
@ -2409,7 +2426,7 @@ mod tests {
#[test] #[test]
fn render_flake_injects_github_disable_only_when_signalled() { fn render_flake_injects_github_disable_only_when_signalled() {
// services.hyperhive.github.enable = false -> HYPERHIVE_GITHUB_DISABLED // services.hyperhive.github.enable = false -> HYPERHIVE_GITHUB_DISABLED
// on hive-c0re's unit -> `hyperhive.github.enable = false` injected into // on hive-c0re's unit -> `services.hyperhive.agent.github.enable = false` injected into
// every agent. On by default, so nothing is emitted unless disabled. // every agent. On by default, so nothing is emitted unless disabled.
// //
// Serialised against every other env-mutating test in the crate — the // Serialised against every other env-mutating test in the crate — the
@ -2441,11 +2458,11 @@ mod tests {
std::env::remove_var("HYPERHIVE_GITHUB_DISABLED"); std::env::remove_var("HYPERHIVE_GITHUB_DISABLED");
} }
assert!( assert!(
!on_default.contains("hyperhive.github.enable"), !on_default.contains("services.hyperhive.agent.github.enable"),
"github.enable must not be emitted by default (agents keep the true default):\n{on_default}" "github.enable must not be emitted by default (agents keep the true default):\n{on_default}"
); );
assert!( assert!(
disabled.contains("hyperhive.github.enable = false;"), disabled.contains("services.hyperhive.agent.github.enable = false;"),
"github.enable = false must be injected when the host disables it:\n{disabled}" "github.enable = false must be injected when the host disables it:\n{disabled}"
); );
} }

View file

@ -403,7 +403,7 @@ pub async fn write_agent_matrix_token(
/// (running as root). Writes `<state>/github-token` 0600, chowned to the agent /// (running as root). Writes `<state>/github-token` 0600, chowned to the agent
/// user so the `gh` wrapper / git credential helper can read it from inside the /// user so the `gh` wrapper / git credential helper can read it from inside the
/// container. Single account per agent — no account suffix. The token value is /// container. Single account per agent — no account suffix. The token value is
/// operator-supplied (for the agent's GitHub integration, `hyperhive.github.enable`). /// operator-supplied (for the agent's GitHub integration, `services.hyperhive.agent.github.enable`).
/// ///
/// # Errors /// # Errors
/// ///

View file

@ -8,7 +8,7 @@
//! `<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 //! Any **extra** accounts come from the `HIVE_MATRIX_ACCOUNTS` env var
//! (JSON, written by the nix harness module from //! (JSON, written by the nix harness module from
//! `hyperhive.matrixAccounts`) and are appended after `main`. //! `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

View file

@ -271,7 +271,7 @@ async fn bring_up_account(
) -> Result<Option<(Client, SyncLoop)>> { ) -> Result<Option<(Client, SyncLoop)>> {
let Some(homeserver) = cfg.homeserver() else { let Some(homeserver) = cfg.homeserver() else {
// No homeserver for this account: the hive has none to offer (no // No homeserver for this account: the hive has none to offer (no
// matrix vhost) or this agent's `hyperhive.matrix.url` is null. Same // matrix vhost) or this agent's `services.hyperhive.agent.matrix.url` is null. Same
// no-op as a missing token — an absent integration, not a guess at // no-op as a missing token — an absent integration, not a guess at
// one. // one.
tracing::info!( tracing::info!(

View file

@ -11,7 +11,7 @@
//! //!
//! Multi-account: every tool carries an optional `account` arg naming //! Multi-account: every tool carries an optional `account` arg naming
//! which matrix account to act as (a `name` from //! which matrix account to act as (a `name` from
//! `hyperhive.matrixAccounts`); omitting it selects the agent's primary //! `services.hyperhive.agent.matrixAccounts`); omitting it selects the agent's primary
//! account (or errors, listing the choices, when more than one account //! account (or errors, listing the choices, when more than one account
//! is configured — see [`crate::accounts::Registry::resolve`]). //! is configured — see [`crate::accounts::Registry::resolve`]).
@ -49,7 +49,7 @@ struct SendMessageArgs {
/// Message body. Markdown is rendered to HTML by the daemon /// Message body. Markdown is rendered to HTML by the daemon
/// (`text_markdown`); plain text passes through unchanged. /// (`text_markdown`); plain text passes through unchanged.
body: String, body: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -62,7 +62,7 @@ struct SendDmArgs {
/// the recipient. /// the recipient.
user_id: String, user_id: String,
body: String, body: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -79,7 +79,7 @@ struct SendFileArgs {
/// Optional caption, sent as a follow-up text message in the room. /// Optional caption, sent as a follow-up text message in the room.
#[serde(default)] #[serde(default)]
caption: Option<String>, caption: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -90,7 +90,7 @@ struct OpenDmArgs {
/// Matrix user id (`@user:server`) to open a DM with. The DM room is /// Matrix user id (`@user:server`) to open a DM with. The DM room is
/// created if one doesn't already exist. /// created if one doesn't already exist.
user_id: String, user_id: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -104,7 +104,7 @@ struct SendReactionArgs {
/// Reaction key — usually an emoji (`👍`, `❤️`) but any string /// Reaction key — usually an emoji (`👍`, `❤️`) but any string
/// works per matrix spec. /// works per matrix spec.
key: String, key: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -115,7 +115,7 @@ struct SendReplyArgs {
room: String, room: String,
event_id: String, event_id: String,
body: String, body: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -125,7 +125,7 @@ struct SendReplyArgs {
struct MarkReadArgs { struct MarkReadArgs {
room: String, room: String,
event_id: String, event_id: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -139,7 +139,7 @@ struct SendRedactArgs {
/// Optional human-readable reason recorded on the redaction event. /// Optional human-readable reason recorded on the redaction event.
#[serde(default)] #[serde(default)]
reason: Option<String>, reason: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -147,7 +147,7 @@ struct SendRedactArgs {
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
struct ListRoomsArgs { struct ListRoomsArgs {
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -156,7 +156,7 @@ struct ListRoomsArgs {
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
struct ListRoomMembersArgs { struct ListRoomMembersArgs {
room: String, room: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -179,7 +179,7 @@ struct ReadRoomArgs {
/// Mutually exclusive with `from`. /// Mutually exclusive with `from`.
#[serde(default)] #[serde(default)]
until: Option<String>, until: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -196,7 +196,7 @@ struct DownloadFileArgs {
/// named after the attachment; the returned `path` is where to read it. /// named after the attachment; the returned `path` is where to read it.
#[serde(default)] #[serde(default)]
dest_path: Option<String>, dest_path: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -204,7 +204,7 @@ struct DownloadFileArgs {
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
struct ListInvitesArgs { struct ListInvitesArgs {
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -214,7 +214,7 @@ struct ListInvitesArgs {
struct JoinRoomArgs { struct JoinRoomArgs {
/// Matrix room id (`!abc:server`) or canonical alias (`#name:server`). /// Matrix room id (`!abc:server`) or canonical alias (`#name:server`).
room: String, room: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -228,7 +228,7 @@ struct ResolveInviteArgs {
/// What to do with the invite: `"accept"` (join the room) or /// What to do with the invite: `"accept"` (join the room) or
/// `"reject"` (decline and leave). /// `"reject"` (decline and leave).
action: String, action: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,
@ -241,7 +241,7 @@ struct InviteUserArgs {
room: String, room: String,
/// Matrix user id of the invitee (`@user:server`). /// Matrix user id of the invitee (`@user:server`).
user_id: String, user_id: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`). /// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account. /// Omit to use the agent's primary account.
#[serde(default)] #[serde(default)]
account: Option<String>, account: Option<String>,

View file

@ -2,7 +2,7 @@
//! //!
//! All paths are overridable via env vars for dev / test scenarios and //! All paths are overridable via env vars for dev / test scenarios and
//! to let the harness point the daemon at non-default locations when //! to let the harness point the daemon at non-default locations when
//! the operator overrides `hyperhive.matrix.*` options. //! the operator overrides `services.hyperhive.agent.matrix.*` options.
use std::path::PathBuf; use std::path::PathBuf;

View file

@ -510,7 +510,7 @@ pub enum PrivRequest {
/// Write `github-token` into `AGENT_STATE_ROOT/<agent_name>/state/github-token`. /// Write `github-token` into `AGENT_STATE_ROOT/<agent_name>/state/github-token`.
/// ///
/// The operator-supplied GitHub personal access token (PAT) for the /// The operator-supplied GitHub personal access token (PAT) for the
/// agent's GitHub integration (`hyperhive.github.enable`). Same write /// agent's GitHub integration (`services.hyperhive.agent.github.enable`). Same write
/// semantics as /// semantics as
/// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir /// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir
/// if absent, writes the file 0600, and chowns it to the agent so the /// if absent, writes the file 0600, and chowns it to the agent so the

View file

@ -2812,7 +2812,7 @@ fn validate_name_chars(name: &str) -> Result<()> {
/// [`validate_name_chars`] allows on purpose. /// [`validate_name_chars`] allows on purpose.
/// ///
/// An agent name is an `Ident` and lowercase by design. An account name is an /// An agent name is an `Ident` and lowercase by design. An account name is an
/// attribute name in `hyperhive.matrixAccounts`, typed `attrsOf` with no /// attribute name in `services.hyperhive.agent.matrixAccounts`, typed `attrsOf` with no
/// charset constraint, so `Ops_Relay9` is a key an operator may already have /// charset constraint, so `Ops_Relay9` is a key an operator may already have
/// written. `swarm_secret_client::path::checked_segment` accepts exactly this /// written. `swarm_secret_client::path::checked_segment` accepts exactly this
/// set for the same name in the secret store — the two must agree, or a /// set for the same name in the secret store — the two must agree, or a
@ -3502,7 +3502,7 @@ mod tests {
/// Ported from `hive-c0re`'s `credential.rs`, which used to build this path /// Ported from `hive-c0re`'s `credential.rs`, which used to build this path
/// itself. The controls are the load-bearing half: an account name is an /// itself. The controls are the load-bearing half: an account name is an
/// attrset key in `hyperhive.matrixAccounts`, so uppercase and underscore /// attrset key in `services.hyperhive.agent.matrixAccounts`, so uppercase and underscore
/// are names an operator can already have written, and the secret store /// are names an operator can already have written, and the secret store
/// accepts exactly this set for the same name. A validator narrower than /// accepts exactly this set for the same name. A validator narrower than
/// the store's reads a credential out and then refuses to land it. /// the store's reads a credential out and then refuses to land it.

View file

@ -1,7 +1,7 @@
//! `hive-screen-mcp` — stdio MCP bridge for GUI agents. //! `hive-screen-mcp` — stdio MCP bridge for GUI agents.
//! //!
//! Provides screenshot, keyboard-input, and mouse tools for agents running a //! Provides screenshot, keyboard-input, and mouse tools for agents running a
//! Weston Wayland compositor (`hyperhive.gui.enable = true`). Each tool: //! Weston Wayland compositor (`services.hyperhive.agent.gui.enable = true`). Each tool:
//! //!
//! - `screenshot` → `grim` (always available with gui enabled) //! - `screenshot` → `grim` (always available with gui enabled)
//! - `type_text` → `wtype` (Wayland virtual-keyboard protocol, no daemon) //! - `type_text` → `wtype` (Wayland virtual-keyboard protocol, no daemon)
@ -18,7 +18,7 @@
//! //!
//! `WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR` are injected globally by the //! `WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR` are injected globally by the
//! `weston-vnc` module. `HIVE_GUI_VNC_PORT` is set by the harness service //! `weston-vnc` module. `HIVE_GUI_VNC_PORT` is set by the harness service
//! when `hyperhive.gui.enable = true` (defaults to 5900 if absent). //! when `services.hyperhive.agent.gui.enable = true` (defaults to 5900 if absent).
use anyhow::Result; use anyhow::Result;
use rmcp::{ use rmcp::{
@ -203,7 +203,7 @@ impl ScreenMcp {
description = "Take a screenshot of the agent's Wayland display and save it as a PNG \ description = "Take a screenshot of the agent's Wayland display and save it as a PNG \
file under `/tmp/`. Returns the path to the saved file pass that path to the \ file under `/tmp/`. Returns the path to the saved file pass that path to the \
`Read` tool to view the image visually. Requires `grim` and a live \ `Read` tool to view the image visually. Requires `grim` and a live \
`WAYLAND_DISPLAY` (provided automatically when `hyperhive.gui.enable = true`)." `WAYLAND_DISPLAY` (provided automatically when `services.hyperhive.agent.gui.enable = true`)."
)] )]
async fn screenshot(&self) -> String { async fn screenshot(&self) -> String {
let ms = std::time::SystemTime::now() let ms = std::time::SystemTime::now()
@ -222,7 +222,7 @@ impl ScreenMcp {
injection). Supports arbitrary Unicode. For special keys (Enter, Tab, Escape, \ injection). Supports arbitrary Unicode. For special keys (Enter, Tab, Escape, \
function keys, arrow keys) or modifier combos, use `key_press` instead. \ function keys, arrow keys) or modifier combos, use `key_press` instead. \
Requires a live `WAYLAND_DISPLAY` (provided automatically when \ Requires a live `WAYLAND_DISPLAY` (provided automatically when \
`hyperhive.gui.enable = true`)." `services.hyperhive.agent.gui.enable = true`)."
)] )]
async fn type_text(&self, Parameters(args): Parameters<TypeTextArgs>) -> String { async fn type_text(&self, Parameters(args): Parameters<TypeTextArgs>) -> String {
cmd_result(run_cmd("wtype", &[&args.text]).await) cmd_result(run_cmd("wtype", &[&args.text]).await)
@ -236,7 +236,7 @@ impl ScreenMcp {
with `+`-separated parts where all but the last are modifiers (`ctrl+c`, \ with `+`-separated parts where all but the last are modifiers (`ctrl+c`, \
`ctrl+shift+t`, `super+l`, `alt+F4`). Modifier names: `ctrl`, `shift`, `alt`, \ `ctrl+shift+t`, `super+l`, `alt+F4`). Modifier names: `ctrl`, `shift`, `alt`, \
`super`. Requires a live `WAYLAND_DISPLAY` (provided automatically when \ `super`. Requires a live `WAYLAND_DISPLAY` (provided automatically when \
`hyperhive.gui.enable = true`)." `services.hyperhive.agent.gui.enable = true`)."
)] )]
async fn key_press(&self, Parameters(args): Parameters<KeyPressArgs>) -> String { async fn key_press(&self, Parameters(args): Parameters<KeyPressArgs>) -> String {
// Parse "mod1+mod2+key" into: -M mod1 -M mod2 -k key -m mod2 -m mod1 // Parse "mod1+mod2+key" into: -M mod1 -M mod2 -k key -m mod2 -m mod1

View file

@ -1,5 +1,5 @@
//! Builds a subagent's own filtered `--mcp-config`: only //! Builds a subagent's own filtered `--mcp-config`: only
//! `hyperhive.extraMcpServers` entries with `availableToSubagents = true` //! `services.hyperhive.agent.extraMcpServers` entries with `availableToSubagents = true`
//! (see `hive_agent_sock::extra_mcp::ExtraMcpServer::available_to_subagents`) ever reach //! (see `hive_agent_sock::extra_mcp::ExtraMcpServer::available_to_subagents`) ever reach
//! a subagent's claude invocation, plus this daemon's own two-tool signal //! a subagent's claude invocation, plus this daemon's own two-tool signal
//! surface. Everything else — the built-in hyperhive //! surface. Everything else — the built-in hyperhive

View file

@ -814,7 +814,7 @@ fn validate_name(name: &str) -> anyhow::Result<()> {
.map_err(|e| anyhow::anyhow!("invalid subagent name {name:?}: {e}")) .map_err(|e| anyhow::anyhow!("invalid subagent name {name:?}: {e}"))
} }
/// Env var carrying the nix-configured `hyperhive.availableModels` as a /// Env var carrying the nix-configured `services.hyperhive.agent.availableModels` as a
/// comma-separated list, forwarded onto this daemon's unit by /// comma-separated list, forwarded onto this daemon's unit by
/// `nix/agent-modules/mcp.nix` the same way `HIVE_TOOL_GROUPS` is. /// `nix/agent-modules/mcp.nix` the same way `HIVE_TOOL_GROUPS` is.
const AVAILABLE_MODELS_ENV: &str = "HIVE_AVAILABLE_MODELS"; const AVAILABLE_MODELS_ENV: &str = "HIVE_AVAILABLE_MODELS";

View file

@ -100,7 +100,7 @@ pub struct PutMatrixAccountRequest {
/// flight, with nowhere to reconstruct it from on a re-delivery. /// flight, with nowhere to reconstruct it from on a re-delivery.
/// ///
/// Optional in token mode (omitted means "resolve to the agent's own /// Optional in token mode (omitted means "resolve to the agent's own
/// `hyperhive.matrix.url` on the hive side" — this route never needs to /// `services.hyperhive.agent.matrix.url` on the hive side" — this route never needs to
/// know it itself for a blind store). **Required** in password mode: /// know it itself for a blind store). **Required** in password mode:
/// logging in needs somewhere to log in against, and unlike token mode /// logging in needs somewhere to log in against, and unlike token mode
/// there is no hive-side fallback to defer to. /// there is no hive-side fallback to defer to.
@ -163,7 +163,7 @@ 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 `hyperhive.matrix.url` — the schema there // synthesizes per agent from `services.hyperhive.agent.matrix.url` — the schema there
// forbids declaring a key by that name for the same reason this route // forbids declaring a key by that name for the same reason 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)
@ -174,7 +174,7 @@ pub async fn put_matrix_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, synthesized per agent from \
hyperhive.matrix.url it cannot be set through this route.", services.hyperhive.agent.matrix.url it cannot be set through this route.",
)); ));
} }