Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ea81aae65 | ||
|
|
3662eda440 |
51 changed files with 669 additions and 411 deletions
|
|
@ -82,7 +82,7 @@
|
|||
agentPackages =
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
hyperhive.packages = lib.mapAttrs (_: lib.mkDefault) {
|
||||
services.hyperhive.agent.packages = lib.mapAttrs (_: lib.mkDefault) {
|
||||
inherit (self.packages.${pkgs.stdenv.hostPlatform.system})
|
||||
hive-agent
|
||||
hive-agent-mcp
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ impl AgentServer {
|
|||
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
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.
|
||||
if let Err(refusal) = crate::send_allow::check_send_allowed(&to) {
|
||||
return run_tool_envelope("send", log, async move { refusal }).await;
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ pub(super) struct MatrixRoomUnread {
|
|||
}
|
||||
|
||||
/// 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
|
||||
/// exposes; unset in practice since a single fixed port is safe (each
|
||||
/// agent container is its own network namespace — see docs/networking/network.md).
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ pub fn check_send_allowed(to: &str) -> Result<(), String> {
|
|||
return Ok(());
|
||||
}
|
||||
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 \
|
||||
parent is always reachable — route through `send(to: \"{}\", …)` \
|
||||
if you need to reach someone outside the allow-list.",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//! `/etc/hyperhive/extra-mcp.json`, plus the parsing and per-entry
|
||||
//! JSON-rendering logic every consumer needs. Lives in `hive-agent-sock`
|
||||
|
|
@ -16,7 +16,7 @@ use std::path::Path;
|
|||
/// `--mcp-config`.
|
||||
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
|
||||
/// 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
|
||||
|
|
@ -55,7 +55,7 @@ impl ExtraMcpServer {
|
|||
}
|
||||
|
||||
/// 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
|
||||
/// this flag is the only path an entry reaches a subagent's own
|
||||
/// `--mcp-config` at all — see `hive-subagent-mcp`'s `mcp_config`
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
//! signals its own turn loop directly, so hive-c0re is not in either path —
|
||||
//! 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`
|
||||
//! (the subagent-eligible subset) — not a socket protocol at all, but this
|
||||
//! crate is the natural home since both consumers already depend on it.
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ impl Bus {
|
|||
}
|
||||
};
|
||||
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.
|
||||
// The nix config always wins on rebuild; the persisted file is kept
|
||||
// for within-session tracking only (see persist_model / set_model).
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ pub(crate) fn write_api_key_mode(enabled: bool) {
|
|||
pub const DEFAULT_MODEL: &str = "haiku";
|
||||
|
||||
/// 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
|
||||
/// override so that nix config changes always take effect on rebuild.
|
||||
#[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
|
||||
/// `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
|
||||
/// runtime override takes precedence over this baseline (see `Bus::new`):
|
||||
/// the operator's effort pick sticks across harness restart, with the nix
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ impl LoginState {
|
|||
|
||||
/// Whether this agent is configured to authenticate to its backend with an
|
||||
/// 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
|
||||
/// than cached: it's an env var, not a value worth a `OnceLock` for.
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Claude launch-config layer: resolves the agent's tool-group / capability
|
||||
//! set into the `--allowedTools` / `--tools` argument strings and renders the
|
||||
//! `--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
|
||||
//! running MCP server (a separate binary) — the `send` allow-list check that
|
||||
//! 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
|
||||
/// (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
|
||||
/// 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).
|
||||
|
|
@ -65,7 +65,7 @@ fn allowed_capability_tools() -> Vec<String> {
|
|||
|
||||
/// 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
|
||||
/// 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
|
||||
|
|
@ -135,7 +135,7 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::ToolGroup]) -> Vec<S
|
|||
.filter(|t| seen.insert(*t))
|
||||
.map(|t| format!("mcp__{SERVER_NAME}__{t}"))
|
||||
.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`
|
||||
// pattern list to `mcp__<server>__<pattern>` so claude can call
|
||||
// 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
|
||||
/// 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
|
||||
/// 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
|
||||
/// `type`.
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! 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
|
||||
//! shells out `claude plugin install <spec>` for each entry. Runs once
|
||||
//! 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.
|
||||
async fn auto_update_enabled() -> bool {
|
||||
match tokio::fs::read_to_string(AUTO_UPDATE_PATH).await {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ pub fn render(
|
|||
.replace("{operator_pronouns}", operator_pronouns)
|
||||
.replace("{hive_identity}", &hive_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
|
||||
// sentence so the agent knows they exist. Additive: it doesn't replace the
|
||||
// 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).
|
||||
let hive_name = crate::identity::hive_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.
|
||||
let docs_dir = std::env::var("HIVE_DOCS_DIR")
|
||||
.ok()
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ impl TurnFiles {
|
|||
/// Drop the MCP config blob claude reads from `--mcp-config <path>`.
|
||||
/// 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
|
||||
/// servers declared via `hyperhive.extraMcpServers` are still stdio bridges.
|
||||
/// servers declared via `services.hyperhive.agent.extraMcpServers` are still stdio bridges.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
|
@ -576,7 +576,7 @@ fn session_store() -> hive_claude::SessionStore {
|
|||
/// tool allow-lists and the optional docs `--add-dir`.
|
||||
fn claude_config(bus: &Bus, files: &TurnFiles) -> Config {
|
||||
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.
|
||||
if let Some(docs_dir) = std::env::var_os("HIVE_DOCS_DIR")
|
||||
&& !docs_dir.is_empty()
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ pub async fn serve(
|
|||
.map(PathBuf::from)
|
||||
.context(
|
||||
"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() {
|
||||
anyhow::bail!(
|
||||
|
|
@ -130,7 +130,7 @@ pub async fn serve(
|
|||
.route("/api/stats", get(stats::api_stats))
|
||||
.route("/screen/ws", get(screen::screen_ws))
|
||||
.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`.
|
||||
let app = proxy::mount_extra_proxies(app)
|
||||
// Anything else (`/`, `/stats`, `/screen`, `/static/*`)
|
||||
|
|
@ -139,7 +139,7 @@ pub async fn serve(
|
|||
// (so `/stats` → `dist/stats.html`, `/screen` → `dist/
|
||||
// screen.html`). Per-agent `extraFiles` additions are
|
||||
// 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))
|
||||
.with_state(state);
|
||||
// `HIVE_WEB_SOCKET` opt-in: when set + non-empty, bind a
|
||||
|
|
|
|||
|
|
@ -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>/`
|
||||
//! in the per-agent web UI, forwarding every request (method, headers, body)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|||
use super::AppState;
|
||||
|
||||
/// 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
|
||||
/// server-side default any more (that was `hive_sh4re::assets::
|
||||
/// branding_svg`, now removed). Consumers fall back client-side: the
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ enum AgentLinkKind {
|
|||
/// `http://<host>:3000<url>`.
|
||||
Forge,
|
||||
/// `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,
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
// `{state_dir}/hyperhive-dashboard-links.json`). Shape on disk
|
||||
// is `{label, icon, url}` with absolute URLs — those become
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
// 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`):
|
||||
// its `~/.claude/` is empty by design (no OAuth flow to complete),
|
||||
// so an empty dir there means nothing — only the auth-failed
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ pub struct Coordinator {
|
|||
pub hyperhive_flake: String,
|
||||
/// URL of the narrow `docs/` source (no fragment). Inlined into the
|
||||
/// 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`.
|
||||
pub hyperhive_docs_flake: String,
|
||||
/// Store-path URL of the nixpkgs to wire into the meta flake as
|
||||
|
|
@ -59,7 +59,7 @@ pub struct Coordinator {
|
|||
/// `follows = "hyperhive/nixpkgs"` behaviour.
|
||||
pub nixpkgs_flake: String,
|
||||
/// 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`
|
||||
/// (which resolves the package and hands us its path). `None` = every
|
||||
/// agent keeps the `claude-code` from its own nixpkgs.
|
||||
|
|
@ -228,13 +228,13 @@ pub struct HiveEnv {
|
|||
pub hyperhive_flake: String,
|
||||
/// Store-path URL of the narrow `docs/` source, wired into the meta
|
||||
/// 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.
|
||||
pub hyperhive_docs_flake: String,
|
||||
pub nixpkgs_flake: String,
|
||||
/// Store path of the `claude-code` agents run, or `None` for "each
|
||||
/// 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.
|
||||
pub claude_code_path: Option<String>,
|
||||
pub dashboard_port: u16,
|
||||
|
|
|
|||
|
|
@ -739,21 +739,27 @@ const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
|
|||
// whose option says no queue coordinates and whose env says otherwise logs the
|
||||
// partial-config error rather than half-connecting, which is the same
|
||||
// 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",
|
||||
"hyperhive.queue.tokenEndpoint",
|
||||
"services.hyperhive.agent.queue.tokenEndpoint",
|
||||
),
|
||||
// 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
|
||||
// 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
|
||||
// it is.
|
||||
("HIVE_AGENT_LOGS_QUERY_URL", "hyperhive.logs.queryUrl"),
|
||||
("HIVE_FORGE_URL", "hyperhive.forge.url"),
|
||||
("HIVE_MATRIX_URL", "hyperhive.matrix.url"),
|
||||
("HYPERHIVE_HIVE_NAME", "hyperhive.hiveName"),
|
||||
("HYPERHIVE_SWARM_NAME", "hyperhive.swarmName"),
|
||||
(
|
||||
"HIVE_AGENT_LOGS_QUERY_URL",
|
||||
"services.hyperhive.agent.logs.queryUrl",
|
||||
),
|
||||
("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.
|
||||
|
|
@ -874,17 +880,17 @@ struct OtelConfig {
|
|||
protocol: String,
|
||||
extra_resource_attributes: Option<String>,
|
||||
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.
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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`
|
||||
/// 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> {
|
||||
let endpoint = std::env::var("HYPERHIVE_OTEL_ENDPOINT")
|
||||
.ok()
|
||||
|
|
@ -1101,9 +1107,9 @@ where
|
|||
}
|
||||
// Narrow `docs/` source as its own input so a doc edit only
|
||||
// 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);
|
||||
// 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() {
|
||||
// `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.
|
||||
|
|
@ -1169,11 +1175,11 @@ where
|
|||
);
|
||||
// Point the in-container docs dir (`$HIVE_DOCS_DIR`) at the narrow
|
||||
// `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`
|
||||
// capture. Emitted only when the input exists (docs_flake non-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
|
||||
// flake input: containers share the host's `/nix/store`, so the
|
||||
|
|
@ -1185,7 +1191,10 @@ where
|
|||
// spelled out here is text and references nothing.
|
||||
// `None` = no override; agents keep their own nixpkgs' `claude-code`.
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
// the same config lands on every agent; emitted only when enabled.
|
||||
// Mirrors the CA-cert injection above — host state -> build-time agent
|
||||
// module config.
|
||||
if let Some(otel) = otel_config() {
|
||||
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!(
|
||||
out,
|
||||
" hyperhive.otel.endpoint = \"{}\";",
|
||||
" services.hyperhive.agent.otel.endpoint = \"{}\";",
|
||||
esc(&otel.endpoint)
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" hyperhive.otel.protocol = \"{}\";",
|
||||
" services.hyperhive.agent.otel.protocol = \"{}\";",
|
||||
esc(&otel.protocol)
|
||||
);
|
||||
if let Some(attrs) = &otel.extra_resource_attributes {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" hyperhive.otel.extraResourceAttributes = \"{}\";",
|
||||
" services.hyperhive.agent.otel.extraResourceAttributes = \"{}\";",
|
||||
esc(attrs)
|
||||
);
|
||||
}
|
||||
if let Some(ms) = otel.metric_interval_ms {
|
||||
// Int option — emit a bare numeric literal (no quotes). `ms` is a
|
||||
// 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 {
|
||||
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
|
||||
// host-state -> agent-module shape as the otel block above.
|
||||
//
|
||||
|
|
@ -1271,21 +1283,21 @@ where
|
|||
// operation the tests exercise directly.
|
||||
push_forwarded_var_options(&mut out, &forwarded_env_vars());
|
||||
// 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
|
||||
// `HYPERHIVE_GITHUB_DISABLED` env on hive-c0re's unit. Only the OFF
|
||||
// override is propagated — the enabled default needs no per-agent line.
|
||||
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(
|
||||
r#" # The harness service inside the container runs as a
|
||||
# non-root unix user named after the agent (`damocles`,
|
||||
# `iris`, `root`, …). UID auto-assigned by NixOS; the
|
||||
# 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"`.
|
||||
hyperhive.user.name = name;
|
||||
services.hyperhive.agent.user.name = name;
|
||||
# Without this an agent container is called `nixos` and ships
|
||||
# every log line under that: nixpkgs sets the hostname from the
|
||||
# attr name in the `containers.<name>` submodule, and an
|
||||
|
|
@ -1295,7 +1307,7 @@ where
|
|||
# nspawn itself already call this container, so logs join to
|
||||
# both without a prefix transform.
|
||||
networking.hostName = "h-${name}";
|
||||
hyperhive.claudeMemoryMaxBytes = memoryMaxBytes;
|
||||
services.hyperhive.agent.claudeMemoryMaxBytes = memoryMaxBytes;
|
||||
programs.git.config.user = {
|
||||
name = name;
|
||||
email = "${name}@hyperhive.local";
|
||||
|
|
@ -1418,7 +1430,7 @@ where
|
|||
// Effective `MemoryMax=` for this agent (per-agent override, else
|
||||
// the hive-wide default), turned into a raw byte count so
|
||||
// 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
|
||||
// byte count to derive, dependent env var stays unset, same as
|
||||
// today's no-cap behavior.
|
||||
|
|
@ -1759,7 +1771,7 @@ mod tests {
|
|||
"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
|
||||
// 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!(
|
||||
out.contains("hyperhive-docs.url = \"path:/nix/store/bbbb-hyperhive-docs-source\""),
|
||||
"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}"
|
||||
);
|
||||
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}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1777,7 +1791,7 @@ mod tests {
|
|||
#[test]
|
||||
fn render_flake_omits_docs_input_when_docs_flake_empty() {
|
||||
// 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(
|
||||
"github:example/hyperhive",
|
||||
"",
|
||||
|
|
@ -1850,7 +1864,9 @@ mod tests {
|
|||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
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}"
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -2046,9 +2062,9 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
out,
|
||||
" hyperhive.forge.url = \"http://forge.example.test\";\n\
|
||||
\x20 hyperhive.matrix.url = \"http://matrix.example.test\";\n\
|
||||
\x20 hyperhive.hiveName = \"pr1ma\";\n",
|
||||
" services.hyperhive.agent.forge.url = \"http://forge.example.test\";\n\
|
||||
\x20 services.hyperhive.agent.matrix.url = \"http://matrix.example.test\";\n\
|
||||
\x20 services.hyperhive.agent.hiveName = \"pr1ma\";\n",
|
||||
"expected exactly the mapped options, in input order, indented for the module block"
|
||||
);
|
||||
}
|
||||
|
|
@ -2079,8 +2095,8 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
out,
|
||||
" hyperhive.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\
|
||||
\x20 hyperhive.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n"
|
||||
" services.hyperhive.agent.queue.natsUrl = \"nats://10.42.0.1:4222\";\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");
|
||||
}
|
||||
let forge_opt_at = out
|
||||
.find("hyperhive.forge.url = \"http://forge.example.test\"")
|
||||
.expect("hyperhive.forge.url must be rendered from HIVE_FORGE_URL");
|
||||
.find("services.hyperhive.agent.forge.url = \"http://forge.example.test\"")
|
||||
.expect("services.hyperhive.agent.forge.url must be rendered from HIVE_FORGE_URL");
|
||||
let matrix_opt_at = out
|
||||
.find("hyperhive.matrix.url = \"http://matrix.example.test\"")
|
||||
.expect("hyperhive.matrix.url must be rendered from HIVE_MATRIX_URL");
|
||||
.find("services.hyperhive.agent.matrix.url = \"http://matrix.example.test\"")
|
||||
.expect("services.hyperhive.agent.matrix.url must be rendered from HIVE_MATRIX_URL");
|
||||
let env_block_at = out
|
||||
.find("environment.variables = {")
|
||||
.expect("per-agent environment.variables block must exist");
|
||||
|
|
@ -2332,8 +2348,8 @@ mod tests {
|
|||
#[test]
|
||||
fn render_flake_injects_otel_when_signalled() {
|
||||
// services.hyperhive.otel.* -> HYPERHIVE_OTEL_* on hive-c0re's unit
|
||||
// -> injected as build-time hyperhive.otel.* into every agent. With
|
||||
// no endpoint signal, no hyperhive.otel lines are emitted (agents
|
||||
// -> injected as build-time services.hyperhive.agent.otel.* into every agent. With
|
||||
// no endpoint signal, no services.hyperhive.agent.otel lines are emitted (agents
|
||||
// keep the the harness modules disabled default).
|
||||
//
|
||||
// Serialised against every other env-mutating test in the crate.
|
||||
|
|
@ -2376,32 +2392,33 @@ mod tests {
|
|||
let off = render();
|
||||
|
||||
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}"
|
||||
);
|
||||
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}"
|
||||
);
|
||||
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}"
|
||||
);
|
||||
// Optional fields absent when unset.
|
||||
assert!(
|
||||
!on_minimal.contains("hyperhive.otel.extraResourceAttributes"),
|
||||
!on_minimal.contains("services.hyperhive.agent.otel.extraResourceAttributes"),
|
||||
"extraResourceAttributes must not appear when unset:\n{on_minimal}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
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}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!off.contains("hyperhive.otel"),
|
||||
!off.contains("services.hyperhive.agent.otel"),
|
||||
"no otel lines when disabled:\n{off}"
|
||||
);
|
||||
}
|
||||
|
|
@ -2409,7 +2426,7 @@ mod tests {
|
|||
#[test]
|
||||
fn render_flake_injects_github_disable_only_when_signalled() {
|
||||
// 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.
|
||||
//
|
||||
// Serialised against every other env-mutating test in the crate — the
|
||||
|
|
@ -2441,11 +2458,11 @@ mod tests {
|
|||
std::env::remove_var("HYPERHIVE_GITHUB_DISABLED");
|
||||
}
|
||||
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}"
|
||||
);
|
||||
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}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue