From 348fb3792a7d0495e33773720be722011f52c385 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 31 May 2026 22:48:14 +0200 Subject: [PATCH 1/2] feat(#589): swarm peers option + HYPERHIVE_PEERS env wire v0 nix: services.hyperhive.peers attrset-of-submodules option; serialises to HYPERHIVE_PEERS JSON ([{label,domain}]); forwarded to containers via FORWARDED_VARS. dashboard.rs: peer_hives: Vec in StateSnapshot, derived as {name:label, url:"http://domain/"}. identity.rs: PeerHive struct + peers() accessor for agent-side use. --- hive-ag3nt/src/identity.rs | 19 +++++++++++++ hive-c0re/src/dashboard.rs | 45 +++++++++++++++++++++++++++++++ hive-c0re/src/meta.rs | 2 ++ nix/modules/hive-c0re.nix | 55 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) diff --git a/hive-ag3nt/src/identity.rs b/hive-ag3nt/src/identity.rs index 1145ff8b..97f511df 100644 --- a/hive-ag3nt/src/identity.rs +++ b/hive-ag3nt/src/identity.rs @@ -48,6 +48,25 @@ pub fn swarm_name() -> Option { .filter(|s| !s.is_empty()) } +/// One peer hive in the same swarm. Parsed from `HYPERHIVE_PEERS`. +#[derive(Debug, Clone, serde::Deserialize)] +pub struct PeerHive { + pub label: String, + pub domain: String, +} + +/// Peer hives in the same swarm, parsed from `HYPERHIVE_PEERS` env var +/// (JSON array of `{label,domain}` objects, emitted by the c0re NixOS +/// module from `services.hyperhive.peers`). Returns empty vec on +/// single-hive deploys (env var absent). +#[must_use] +pub fn peers() -> Vec { + env::var("HYPERHIVE_PEERS") + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() +} + /// Hive-qualified agent identity. When the hive domain is configured, returns /// `${label}@${domain}` (e.g. `iris@darkest.space`); when not, returns just /// the short label so callers can render a single string regardless of diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 690a2373..d3ffdd0b 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -266,6 +266,24 @@ struct StateSnapshot { /// var, set from `services.hyperhive.swarmName`. `None` when /// unset — chrome omits the swarm segment of the breadcrumb. swarm_name: Option, + /// Peer hives in the same swarm. Parsed from `HYPERHIVE_PEERS` + /// (JSON array of `{label,domain}` objects, emitted by the c0re + /// NixOS module from `services.hyperhive.peers`). Empty on + /// single-hive deploys. Feeds iris's P33RS tab (#589). Each entry + /// exposes `name` (the operator-chosen label) and `url` (dashboard + /// link derived as `http:///`; v0 HTTP-only - HTTPS peers + /// require gateway TLS (#593/#594)). + peer_hives: Vec, +} + +/// One peer hive for the P33RS dashboard tab. Derived from +/// `HYPERHIVE_PEERS` env; `url` is the peer's dashboard root so the +/// tab can render a clickable card without knowing the remote port. +/// v0: always `http://` - HTTPS peers need gateway TLS (#593/#594). +#[derive(Serialize)] +struct PeerHiveView { + name: String, + url: String, } /// `OpQuestion` + computed `question_refs` / `answer_refs`. Built @@ -475,9 +493,36 @@ async fn api_state(headers: HeaderMap, State(state): State) -> axum::J forge_public_url: std::env::var("HIVE_FORGE_PUBLIC_URL").ok().filter(|s| !s.is_empty()), hive_name: std::env::var("HYPERHIVE_HIVE_NAME").ok().filter(|s| !s.is_empty()), swarm_name: std::env::var("HYPERHIVE_SWARM_NAME").ok().filter(|s| !s.is_empty()), + peer_hives: parse_peer_hives(), }) } +/// Parse `HYPERHIVE_PEERS` env var into dashboard-ready `PeerHiveView` +/// entries. The env var is a JSON array of `{label, domain}` objects +/// emitted by the c0re NixOS module from `services.hyperhive.peers`. +/// Each entry becomes `{ name: label, url: "http://domain/" }` for the +/// P33RS tab. Returns empty vec when unset (single-hive deploy). +fn parse_peer_hives() -> Vec { + #[derive(serde::Deserialize)] + struct Raw { + label: String, + domain: String, + } + let Ok(json) = std::env::var("HYPERHIVE_PEERS") else { + return Vec::new(); + }; + let Ok(raw): Result, _> = serde_json::from_str(&json) else { + tracing::warn!("HYPERHIVE_PEERS is not valid JSON; ignoring"); + return Vec::new(); + }; + raw.into_iter() + .map(|r| PeerHiveView { + name: r.label, + url: format!("http://{}/", r.domain), + }) + .collect() +} + /// Group live containers by their assigned web UI port; clusters with /// more than one member are port-hash collisions the operator needs /// to resolve by renaming. Manager (fixed at 8000) and sub-agents diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 15660350..a9d013b7 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -325,6 +325,8 @@ const CANONICAL_INPUTS: &[&str] = &["nixpkgs", "nixpkgs-unstable"]; /// touching process-wide env). const FORWARDED_VARS: &[&str] = &[ "HIVE_FORGE_URL", + "HIVE_FORGE_PUBLIC_URL", + "HYPERHIVE_PEERS", "HYPERHIVE_HIVE_DOMAIN", "HYPERHIVE_HIVE_NAME", "HYPERHIVE_SWARM_NAME", diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index c8a98dd3..dde7a1b6 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -92,6 +92,51 @@ in ''; }; + # Peer hives in the same swarm. Each entry declares a remote hive + # reachable from this host. Serialised to JSON and injected as + # `HYPERHIVE_PEERS` into the hive-c0re service and forwarded to agent + # containers via `meta.rs::FORWARDED_VARS`. Consumed by + # `identity.rs::peers()` + the dashboard's `peer_hives` state field + # (feeds iris's P33RS tab). See #589. + options.services.hyperhive.peers = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + domain = lib.mkOption { + type = lib.types.str; + example = "lab.example.com"; + description = '' + DNS domain of the peer hive. Used to construct the peer's + dashboard URL (`http://''${domain}/`) and for Matrix + federation auto-discovery (`matrix.''${domain}`). + Must be reachable from this host. + ''; + }; + tlsCertFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Optional path to a PEM cert/bundle to trust for this peer's + TLS. Null = system CA bundle (for Let's Encrypt peers). Set + to the peer's self-signed cert for `selfSignedTls = true` + peers. See #594. + ''; + }; + }; + }); + default = { }; + example = { + lab = { domain = "lab.example.com"; }; + edge = { domain = "edge.corp"; }; + }; + description = '' + Peer hives in the same swarm. The attrset key is a short label + used in dashboard links and log messages -- it does not need to + match the remote hive's `hiveName`. Null `tlsCertFile` uses the + system CA bundle; set it for self-signed TLS peers (#594 slot, + unimplemented in v0). + ''; + }; + options.services.hyperhive.c0re = { enable = lib.mkOption { type = lib.types.bool; @@ -309,6 +354,16 @@ in # wrong). Absent when `behindGateway = false` — dashboard # falls back to `:3000`. HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}"; + } + // lib.optionalAttrs (config.services.hyperhive.peers != { }) { + # Peer hives serialised as a JSON array of {label, domain} objects. + # Consumed by hive-ag3nt::identity::peers() + the dashboard's + # peer_hives StateSnapshot field (P33RS tab). tlsCertFile is + # nix-side-only (host nginx/trust config); rust never needs the path. + HYPERHIVE_PEERS = builtins.toJSON ( + lib.mapAttrsToList (label: p: { inherit label; inherit (p) domain; }) + config.services.hyperhive.peers + ); }; serviceConfig = { ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)}"; From 4093f4fdb43a5f56b5246d2076e6261441a1af97 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 31 May 2026 23:05:03 +0200 Subject: [PATCH 2/2] fixup: remove issue tags from code, use plain prose --- hive-c0re/src/dashboard.rs | 8 ++++---- nix/modules/hive-c0re.nix | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index d3ffdd0b..7192d64a 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -269,17 +269,17 @@ struct StateSnapshot { /// Peer hives in the same swarm. Parsed from `HYPERHIVE_PEERS` /// (JSON array of `{label,domain}` objects, emitted by the c0re /// NixOS module from `services.hyperhive.peers`). Empty on - /// single-hive deploys. Feeds iris's P33RS tab (#589). Each entry + /// single-hive deploys. Feeds the P33RS dashboard tab. Each entry /// exposes `name` (the operator-chosen label) and `url` (dashboard - /// link derived as `http:///`; v0 HTTP-only - HTTPS peers - /// require gateway TLS (#593/#594)). + /// link derived as `http:///`; HTTP-only until the gateway + /// has TLS and the scheme is threaded through). peer_hives: Vec, } /// One peer hive for the P33RS dashboard tab. Derived from /// `HYPERHIVE_PEERS` env; `url` is the peer's dashboard root so the /// tab can render a clickable card without knowing the remote port. -/// v0: always `http://` - HTTPS peers need gateway TLS (#593/#594). +/// HTTP-only until per-peer TLS is wired through the gateway layer. #[derive(Serialize)] struct PeerHiveView { name: String, diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index dde7a1b6..8de18a11 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -53,7 +53,7 @@ in message if it's missing. Exposed to agents as `HYPERHIVE_HIVE_DOMAIN`; consumed by `hive-ag3nt::identity::hive_domain()` for `@` - qualified labels (#589). + qualified labels. ''; }; @@ -86,7 +86,7 @@ in description = '' Human-readable name of the wider swarm this hive belongs to. Hives at different DNS domains can share a swarm name when - they federate together (#589). Exposed to agents as + they federate together. Exposed to agents as `HYPERHIVE_SWARM_NAME`; surfaced in the dashboard chrome and per-agent system prompt when set. ''; @@ -97,7 +97,7 @@ in # `HYPERHIVE_PEERS` into the hive-c0re service and forwarded to agent # containers via `meta.rs::FORWARDED_VARS`. Consumed by # `identity.rs::peers()` + the dashboard's `peer_hives` state field - # (feeds iris's P33RS tab). See #589. + # (feeds the P33RS dashboard tab). options.services.hyperhive.peers = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { @@ -118,7 +118,7 @@ in Optional path to a PEM cert/bundle to trust for this peer's TLS. Null = system CA bundle (for Let's Encrypt peers). Set to the peer's self-signed cert for `selfSignedTls = true` - peers. See #594. + peers. Forward-compat slot; not yet used in v0. ''; }; }; @@ -132,8 +132,8 @@ in Peer hives in the same swarm. The attrset key is a short label used in dashboard links and log messages -- it does not need to match the remote hive's `hiveName`. Null `tlsCertFile` uses the - system CA bundle; set it for self-signed TLS peers (#594 slot, - unimplemented in v0). + system CA bundle; set it for self-signed TLS peers (forward-compat + slot, not yet used in v0). ''; };