From 8464cb95cbc752adc6bf08696389bd3785a404bd Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 22 Jun 2026 17:06:28 +0200 Subject: [PATCH 1/4] feat(#1897): hivectl peer-config verb to generate a federation peer block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `hivectl peer-config --domain `: prints the `services.hyperhive.swarm.peers.""` nix block a peer operator pastes to federate with this hive. Emits `caCert = ./-ca.pem` plus a `cp /var/lib/hive-tls/ca.pem ./-ca.pem` line when this hive serves a self-signed CA (the cert file exists); omits caCert for ACME/public-CA hives (trusted by the default bundle). Includes the WireGuard public key when the mesh key exists, and the wireguardAddress/Endpoint passed via flags. `wg init` gains an optional --domain; when set it calls peer-config at the end, so a fresh mesh setup prints the hand-over block in one command. Pure output — reads local state (TLS CA cert presence, wg key), never mutates. Regenerated docs/tools/hivectl-cli.md. Closes #1897. --- docs/tools/hivectl-cli.md | 17 ++++++ hive-c0re/src/bin/hivectl.rs | 103 ++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 4b466440..ee93a381 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -24,6 +24,7 @@ This document contains the help content for the `hivectl` command-line program. * [`hivectl wg init`↴](#hivectl-wg-init) * [`hivectl wg peer`↴](#hivectl-wg-peer) * [`hivectl wg status`↴](#hivectl-wg-status) +* [`hivectl peer-config`↴](#hivectl-peer-config) * [`hivectl choom`↴](#hivectl-choom) * [`hivectl stop`↴](#hivectl-stop) * [`hivectl start`↴](#hivectl-start) @@ -49,6 +50,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that * `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required * `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket) * `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`) +* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Pure output — reads local state (the TLS CA cert, the wg key), never mutates. `wg init` calls this at the end when given a `--domain`, so a fresh mesh setup prints the hand-over block too * `choom` — Open an interactive Claude session inside an agent container * `stop` — Stop containers hive-wide in one operator action. Bare `hivectl stop` stops **everything** — all sub-agents plus the ci, forge, gateway, and matrix infra containers. Narrow it with scope flags: `--agents` (all sub-agents), `--ci` / `--forge` / `--gateway` / `--matrix` (named infra), and `--agent ` (repeatable) for specific sub-agents. Flags are additive (e.g. `--agents --matrix`). Requires the hive-c0re daemon (connects to the host admin socket). hive-c0re itself is never stopped — it services the request * `start` — Start containers hive-wide — the inverse of `hivectl stop`. Bare `hivectl start` starts everything back up; the same scope flags as `stop` narrow it (`--agents`, `--ci`, `--forge`, `--gateway`, `--matrix`, `--agent `). Requires the hive-c0re daemon @@ -315,6 +317,7 @@ Generate (if absent) this hive's WireGuard private key, print its public key, an ###### **Options:** * `--address
` — This hive's mesh address (e.g. `10.42.0.1/32`) to bake into the printed snippet. Omit to get a placeholder you fill in +* `--domain ` — This hive's DNS domain. When set, `init` also prints the `peer-config` block peers paste to federate with this hive (CA + this mesh key), so setup is one command. Omit to skip that and just enable the mesh locally @@ -344,6 +347,20 @@ Show the live mesh interface state (`wg show wg-hive`). Requires the mesh to be +## `hivectl peer-config` + +Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Pure output — reads local state (the TLS CA cert, the wg key), never mutates. `wg init` calls this at the end when given a `--domain`, so a fresh mesh setup prints the hand-over block too + +**Usage:** `hivectl peer-config [OPTIONS] --domain ` + +###### **Options:** + +* `--domain ` — This hive's DNS domain — the `swarm.peers` attrset key the peer declares. Required: hivectl has no other source for it +* `--wg-address ` — This hive's WireGuard mesh address (e.g. `10.42.0.1/32`), emitted as `wireguardAddress`. Omit when not running the mesh +* `--wg-endpoint ` — This hive's public WireGuard endpoint (`host:port`), emitted as `wireguardEndpoint`. Omit when peers dial in / no mesh + + + ## `hivectl choom` Open an interactive Claude session inside an agent container. diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 17d328c7..f3027e50 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -89,6 +89,28 @@ enum Cmd { #[command(subcommand)] cmd: WgCmd, }, + /// Generate the federation peer-config block for THIS hive — the nix + /// a peer operator pastes into their `services.hyperhive.swarm.peers` + /// to trust + reach this hive. Emits `caCert` (+ a `cp` line for the + /// cert) when this hive serves a self-signed CA, the WireGuard public + /// key when the mesh key exists, and the `wireguard{Address,Endpoint}` + /// you pass. Pure output — reads local state (the TLS CA cert, the wg + /// key), never mutates. `wg init` calls this at the end when given a + /// `--domain`, so a fresh mesh setup prints the hand-over block too. + PeerConfig { + /// This hive's DNS domain — the `swarm.peers` attrset key the peer + /// declares. Required: hivectl has no other source for it. + #[arg(long)] + domain: String, + /// This hive's WireGuard mesh address (e.g. `10.42.0.1/32`), + /// emitted as `wireguardAddress`. Omit when not running the mesh. + #[arg(long)] + wg_address: Option, + /// This hive's public WireGuard endpoint (`host:port`), emitted as + /// `wireguardEndpoint`. Omit when peers dial in / no mesh. + #[arg(long)] + wg_endpoint: Option, + }, /// Open an interactive Claude session inside an agent container. /// /// Replaces the current process with `machinectl shell @@ -429,6 +451,12 @@ enum WgCmd { /// printed snippet. Omit to get a placeholder you fill in. #[arg(long)] address: Option, + /// This hive's DNS domain. When set, `init` also prints the + /// `peer-config` block peers paste to federate with this hive + /// (CA + this mesh key), so setup is one command. Omit to skip + /// that and just enable the mesh locally. + #[arg(long)] + domain: Option, }, /// Print the nix snippet to add a peer hive to the mesh. Pure output — /// paste it into this hive's config. Get `` from the peer's @@ -562,7 +590,7 @@ async fn main() -> Result<()> { AgentsCmd::RestartAll => agents_restart_all(&socket).await, }, Cmd::Wg { cmd } => match cmd { - WgCmd::Init { address } => wg_init(address.as_deref()), + WgCmd::Init { address, domain } => wg_init(address.as_deref(), domain.as_deref()), WgCmd::Peer { domain, pubkey, @@ -574,6 +602,14 @@ async fn main() -> Result<()> { } WgCmd::Status => wg_status(), }, + Cmd::PeerConfig { + domain, + wg_address, + wg_endpoint, + } => { + peer_config(&domain, wg_address.as_deref(), wg_endpoint.as_deref()); + Ok(()) + } Cmd::Stop { scope, graceful } => stop(&socket, scope.to_scope(), graceful).await, Cmd::Start { scope } => start(&socket, scope.to_scope()).await, Cmd::Restart { scope, graceful } => restart(&socket, scope.to_scope(), graceful).await, @@ -612,9 +648,18 @@ const WG_KEY_PATH: &str = "/etc/wireguard/hive.key"; /// The mesh interface name hive-c0re's nix module brings up. const WG_INTERFACE: &str = "wg-hive"; +/// Host path of this hive's self-signed CA cert (matches the +/// `services.hyperhive.tls.stateDir` default in hive-tls.nix). Its +/// existence means the gateway serves a self-signed, hive-CA-signed leaf, +/// so a federating peer needs this CA via `swarm.peers..caCert`. Absent +/// = ACME / operator cert (trusted by the default CA bundle, no `caCert`). +const HIVE_TLS_CA_PATH: &str = "/var/lib/hive-tls/ca.pem"; + /// `wg init` — generate (if absent) the hive's WireGuard key, print its -/// public key + the nix snippet to enable the mesh. -fn wg_init(address: Option<&str>) -> Result<()> { +/// public key + the nix snippet to enable the mesh. When `domain` is set, +/// also prints the `peer-config` block peers paste to federate with this +/// hive (so a fresh setup is one command). +fn wg_init(address: Option<&str>, domain: Option<&str>) -> Result<()> { use std::os::unix::fs::PermissionsExt as _; let key_path = Path::new(WG_KEY_PATH); if key_path.exists() { @@ -657,6 +702,13 @@ fn wg_init(address: Option<&str>) -> Result<()> { println!(" address = \"{addr}\";"); println!(" # listenPort = 51820; # default"); println!(" }};"); + + // When the operator names this hive's domain, also print the block a + // peer pastes to federate with us (CA + this mesh key) — one-stop setup. + if let Some(d) = domain { + println!(); + peer_config(d, address, None); + } Ok(()) } @@ -702,6 +754,51 @@ fn wg_peer(domain: &str, pubkey: &str, address: &str, endpoint: Option<&str>) { println!(" }};"); } +/// `peer-config` — print the `swarm.peers.""` block a peer +/// operator pastes to federate with THIS hive, plus a `cp` line for the +/// CA when this hive is self-signed. Reads local state only (the TLS CA +/// cert presence + the wg key); prints, never mutates. +fn peer_config(domain: &str, wg_address: Option<&str>, wg_endpoint: Option<&str>) { + let self_signed = Path::new(HIVE_TLS_CA_PATH).exists(); + // CA filename derived from the first DNS label so multiple peers' + // certs don't collide in the operator's config dir. + let ca_file = format!("{}-ca.pem", domain.split('.').next().unwrap_or("peer")); + // WireGuard public key, when this hive has a mesh key. Best-effort: + // a missing key or absent `wg` binary just omits the mesh lines. + let wg_pub = std::fs::read(WG_KEY_PATH) + .ok() + .and_then(|k| wg_pubkey(&k).ok()); + + if self_signed { + println!("# 1. copy this hive's CA cert next to the peer's config:"); + println!("cp {HIVE_TLS_CA_PATH} ./{ca_file}"); + println!(); + println!("# 2. paste into the peer hive's NixOS config:"); + } else { + println!("# paste into the peer hive's NixOS config:"); + } + println!("services.hyperhive.swarm.peers.\"{domain}\" = {{"); + if self_signed { + println!(" caCert = ./{ca_file};"); + } + if let Some(pk) = &wg_pub { + println!(" wireguardPublicKey = \"{pk}\";"); + } + if let Some(addr) = wg_address { + println!(" wireguardAddress = \"{addr}\";"); + } + if let Some(ep) = wg_endpoint { + println!(" wireguardEndpoint = \"{ep}\";"); + } + println!("}};"); + if !self_signed { + println!( + "# (this hive's cert chains to a public CA — no `caCert` needed; \ + it's trusted by the default bundle.)" + ); + } +} + /// `wg status` — show the live mesh interface (`wg show wg-hive`), /// inheriting stdout so the operator sees it directly. fn wg_status() -> Result<()> { From 72d9422a7a09e6f4d231ccae28997c6aea9f3f70 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 22 Jun 2026 18:49:09 +0200 Subject: [PATCH 2/4] feat(#1897): auto-fill the hive domain in peer-config (drop required --domain) Per operator review: hivectl shouldn't make the operator retype this hive's own domain. Add a HostRequest::HiveDomain admin-socket query (c0re answers from HYPERHIVE_HIVE_DOMAIN, already in its service env) and a domain field on HostResponse. hivectl peer-config now resolves the domain as: --domain override (kept for offline/scripted use), else the daemon query; errors with a clear hint when neither resolves. wg init resolves it best-effort too, so it prints the hand-over peer-config block without --domain (skipped, not failed, when the daemon is unreachable). Regenerated docs/tools/hivectl-cli.md. --- docs/tools/hivectl-cli.md | 8 ++--- hive-c0re/src/bin/hivectl.rs | 67 ++++++++++++++++++++++++++++-------- hive-c0re/src/server.rs | 8 +++++ hive-sh4re/src/lib.rs | 25 ++++++++++++++ 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index ee93a381..d4653290 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -50,7 +50,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that * `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required * `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket) * `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`) -* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Pure output — reads local state (the TLS CA cert, the wg key), never mutates. `wg init` calls this at the end when given a `--domain`, so a fresh mesh setup prints the hand-over block too +* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Reads local state (the TLS CA cert, the wg key) + asks the daemon for this hive's domain; never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too * `choom` — Open an interactive Claude session inside an agent container * `stop` — Stop containers hive-wide in one operator action. Bare `hivectl stop` stops **everything** — all sub-agents plus the ci, forge, gateway, and matrix infra containers. Narrow it with scope flags: `--agents` (all sub-agents), `--ci` / `--forge` / `--gateway` / `--matrix` (named infra), and `--agent ` (repeatable) for specific sub-agents. Flags are additive (e.g. `--agents --matrix`). Requires the hive-c0re daemon (connects to the host admin socket). hive-c0re itself is never stopped — it services the request * `start` — Start containers hive-wide — the inverse of `hivectl stop`. Bare `hivectl start` starts everything back up; the same scope flags as `stop` narrow it (`--agents`, `--ci`, `--forge`, `--gateway`, `--matrix`, `--agent `). Requires the hive-c0re daemon @@ -349,13 +349,13 @@ Show the live mesh interface state (`wg show wg-hive`). Requires the mesh to be ## `hivectl peer-config` -Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Pure output — reads local state (the TLS CA cert, the wg key), never mutates. `wg init` calls this at the end when given a `--domain`, so a fresh mesh setup prints the hand-over block too +Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Reads local state (the TLS CA cert, the wg key) + asks the daemon for this hive's domain; never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too -**Usage:** `hivectl peer-config [OPTIONS] --domain ` +**Usage:** `hivectl peer-config [OPTIONS]` ###### **Options:** -* `--domain ` — This hive's DNS domain — the `swarm.peers` attrset key the peer declares. Required: hivectl has no other source for it +* `--domain ` — Override this hive's DNS domain (the `swarm.peers` attrset key the peer declares). Omit to auto-fill from the running daemon (`services.hyperhive.domain`); pass it only when the daemon is down or you're scripting offline * `--wg-address ` — This hive's WireGuard mesh address (e.g. `10.42.0.1/32`), emitted as `wireguardAddress`. Omit when not running the mesh * `--wg-endpoint ` — This hive's public WireGuard endpoint (`host:port`), emitted as `wireguardEndpoint`. Omit when peers dial in / no mesh diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index f3027e50..73677da0 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -94,14 +94,16 @@ enum Cmd { /// to trust + reach this hive. Emits `caCert` (+ a `cp` line for the /// cert) when this hive serves a self-signed CA, the WireGuard public /// key when the mesh key exists, and the `wireguard{Address,Endpoint}` - /// you pass. Pure output — reads local state (the TLS CA cert, the wg - /// key), never mutates. `wg init` calls this at the end when given a - /// `--domain`, so a fresh mesh setup prints the hand-over block too. + /// you pass. Reads local state (the TLS CA cert, the wg key) + asks the + /// daemon for this hive's domain; never mutates. `wg init` calls this + /// at the end, so a fresh mesh setup prints the hand-over block too. PeerConfig { - /// This hive's DNS domain — the `swarm.peers` attrset key the peer - /// declares. Required: hivectl has no other source for it. + /// Override this hive's DNS domain (the `swarm.peers` attrset key + /// the peer declares). Omit to auto-fill from the running daemon + /// (`services.hyperhive.domain`); pass it only when the daemon is + /// down or you're scripting offline. #[arg(long)] - domain: String, + domain: Option, /// This hive's WireGuard mesh address (e.g. `10.42.0.1/32`), /// emitted as `wireguardAddress`. Omit when not running the mesh. #[arg(long)] @@ -590,7 +592,9 @@ async fn main() -> Result<()> { AgentsCmd::RestartAll => agents_restart_all(&socket).await, }, Cmd::Wg { cmd } => match cmd { - WgCmd::Init { address, domain } => wg_init(address.as_deref(), domain.as_deref()), + WgCmd::Init { address, domain } => { + wg_init(&socket, address.as_deref(), domain.as_deref()).await + } WgCmd::Peer { domain, pubkey, @@ -607,6 +611,7 @@ async fn main() -> Result<()> { wg_address, wg_endpoint, } => { + let domain = resolve_hive_domain(&socket, domain).await?; peer_config(&domain, wg_address.as_deref(), wg_endpoint.as_deref()); Ok(()) } @@ -655,11 +660,38 @@ const WG_INTERFACE: &str = "wg-hive"; /// = ACME / operator cert (trusted by the default CA bundle, no `caCert`). const HIVE_TLS_CA_PATH: &str = "/var/lib/hive-tls/ca.pem"; +/// Best-effort query for this hive's domain from the running daemon +/// (`HostRequest::HiveDomain`, which reads `HYPERHIVE_HIVE_DOMAIN` from +/// c0re's service env). `None` when the daemon is unreachable or the +/// domain is unset — callers decide whether that's fatal. +async fn query_hive_domain(socket: &Path) -> Option { + hive_c0re::client::request(socket, hive_sh4re::HostRequest::HiveDomain) + .await + .ok() + .and_then(|r| r.domain) +} + +/// Resolve this hive's domain for snippet generation: the explicit +/// `--domain` override if given, else the daemon. Errors with a clear +/// hint when neither is available, so `peer-config` never silently emits +/// a wrong key. +async fn resolve_hive_domain(socket: &Path, over: Option) -> Result { + if let Some(d) = over { + return Ok(d); + } + query_hive_domain(socket).await.context( + "could not determine this hive's domain from the daemon — is hive-c0re running \ + and `services.hyperhive.domain` set? pass --domain to override", + ) +} + /// `wg init` — generate (if absent) the hive's WireGuard key, print its -/// public key + the nix snippet to enable the mesh. When `domain` is set, -/// also prints the `peer-config` block peers paste to federate with this -/// hive (so a fresh setup is one command). -fn wg_init(address: Option<&str>, domain: Option<&str>) -> Result<()> { +/// public key + the nix snippet to enable the mesh, then (best-effort) +/// the `peer-config` block peers paste to federate with this hive, so a +/// fresh setup is one command. The domain comes from `--domain` or the +/// daemon; if neither resolves, the peer block is skipped (init still +/// succeeds — its core job is enabling the mesh locally). +async fn wg_init(socket: &Path, address: Option<&str>, domain: Option<&str>) -> Result<()> { use std::os::unix::fs::PermissionsExt as _; let key_path = Path::new(WG_KEY_PATH); if key_path.exists() { @@ -703,11 +735,16 @@ fn wg_init(address: Option<&str>, domain: Option<&str>) -> Result<()> { println!(" # listenPort = 51820; # default"); println!(" }};"); - // When the operator names this hive's domain, also print the block a - // peer pastes to federate with us (CA + this mesh key) — one-stop setup. - if let Some(d) = domain { + // Also print the block a peer pastes to federate with us (CA + this + // mesh key) — one-stop setup. Domain from --domain or the daemon; + // best-effort, so init still succeeds when neither resolves. + let resolved = match domain { + Some(d) => Some(d.to_owned()), + None => query_hive_domain(socket).await, + }; + if let Some(d) = resolved { println!(); - peer_config(d, address, None); + peer_config(&d, address, None); } Ok(()) } diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 8fc56bc5..fb7d05b2 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -114,6 +114,12 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { } HostRequest::Rebuild { name } => handle_rebuild(&coord, name).await?, HostRequest::List => HostResponse::list(lifecycle::list().await?), + // The hive domain is injected into c0re's service env by + // hive-c0re.nix (`HYPERHIVE_HIVE_DOMAIN`); surface it so the + // operator CLI can fill in this hive's own identity. + HostRequest::HiveDomain => { + HostResponse::hive_domain(std::env::var("HYPERHIVE_HIVE_DOMAIN").ok()) + } HostRequest::Pending => HostResponse::pending(coord.approvals.pending()?), HostRequest::Approve { id } => { actions::approve(coord.clone(), *id).await?; @@ -210,6 +216,7 @@ async fn handle_restart_all() -> Result { error: Some(errors.join("; ")), agents: Some(ok_agents), approvals: None, + domain: None, }) } } @@ -368,6 +375,7 @@ fn finish_lifecycle(ok_items: Vec, errors: &[String]) -> HostResponse { error: Some(errors.join("; ")), agents: Some(ok_items), approvals: None, + domain: None, } } } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index b49f1963..ed121f15 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -44,6 +44,11 @@ pub enum HostRequest { Rebuild { name: String }, /// List managed containers. List, + /// Report this hive's canonical DNS domain + /// (`services.hyperhive.domain`), or `None` when unset. Lets the + /// operator CLI fill in the hive's own identity (e.g. the federation + /// peer-config block) without the operator retyping it. + HiveDomain, /// List pending approval requests. Pending, /// Approve a pending request by id; the action runs immediately. @@ -131,6 +136,10 @@ pub struct HostResponse { pub agents: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub approvals: Option>, + /// This hive's canonical DNS domain — `HiveDomain` result. `None` + /// when the domain is unset (no `services.hyperhive.domain`). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub domain: Option, } /// One row in the approval queue. `commit_ref` is overloaded per @@ -218,6 +227,7 @@ impl HostResponse { error: None, agents: None, approvals: None, + domain: None, } } @@ -228,6 +238,7 @@ impl HostResponse { error: Some(message.into()), agents: None, approvals: None, + domain: None, } } @@ -238,6 +249,7 @@ impl HostResponse { error: None, agents: Some(agents), approvals: None, + domain: None, } } @@ -248,6 +260,19 @@ impl HostResponse { error: None, agents: None, approvals: Some(approvals), + domain: None, + } + } + + /// `HiveDomain` result — this hive's canonical domain (or `None`). + #[must_use] + pub fn hive_domain(domain: Option) -> Self { + Self { + ok: true, + error: None, + agents: None, + approvals: None, + domain, } } } From 8ea19b3b1286bd0bd373958fe1eabea0775eb960 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 22 Jun 2026 18:59:35 +0200 Subject: [PATCH 3/4] =?UTF-8?q?feat(#1897):=20always=20auto-fill=20the=20h?= =?UTF-8?q?ive=20domain=20=E2=80=94=20drop=20the=20--domain=20override?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per operator review (#1899): the hive's own domain should never be typed by hand. Remove the --domain flag from peer-config and wg init entirely; both now resolve it from the daemon (HostRequest::HiveDomain). peer-config errors with a clear hint if the daemon can't answer; wg init prints the peer block best-effort (skips it, still enables the mesh, if unresolved). Regenerated docs/tools/hivectl-cli.md. --- docs/tools/hivectl-cli.md | 6 ++-- hive-c0re/src/bin/hivectl.rs | 58 +++++++++++------------------------- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index d4653290..49b83751 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -50,7 +50,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that * `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required * `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket) * `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`) -* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Reads local state (the TLS CA cert, the wg key) + asks the daemon for this hive's domain; never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too +* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. The hive's own domain is filled in automatically from the running daemon (`services.hyperhive.domain`). Reads local state (the TLS CA cert, the wg key); never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too * `choom` — Open an interactive Claude session inside an agent container * `stop` — Stop containers hive-wide in one operator action. Bare `hivectl stop` stops **everything** — all sub-agents plus the ci, forge, gateway, and matrix infra containers. Narrow it with scope flags: `--agents` (all sub-agents), `--ci` / `--forge` / `--gateway` / `--matrix` (named infra), and `--agent ` (repeatable) for specific sub-agents. Flags are additive (e.g. `--agents --matrix`). Requires the hive-c0re daemon (connects to the host admin socket). hive-c0re itself is never stopped — it services the request * `start` — Start containers hive-wide — the inverse of `hivectl stop`. Bare `hivectl start` starts everything back up; the same scope flags as `stop` narrow it (`--agents`, `--ci`, `--forge`, `--gateway`, `--matrix`, `--agent `). Requires the hive-c0re daemon @@ -317,7 +317,6 @@ Generate (if absent) this hive's WireGuard private key, print its public key, an ###### **Options:** * `--address
` — This hive's mesh address (e.g. `10.42.0.1/32`) to bake into the printed snippet. Omit to get a placeholder you fill in -* `--domain ` — This hive's DNS domain. When set, `init` also prints the `peer-config` block peers paste to federate with this hive (CA + this mesh key), so setup is one command. Omit to skip that and just enable the mesh locally @@ -349,13 +348,12 @@ Show the live mesh interface state (`wg show wg-hive`). Requires the mesh to be ## `hivectl peer-config` -Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. Reads local state (the TLS CA cert, the wg key) + asks the daemon for this hive's domain; never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too +Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. The hive's own domain is filled in automatically from the running daemon (`services.hyperhive.domain`). Reads local state (the TLS CA cert, the wg key); never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too **Usage:** `hivectl peer-config [OPTIONS]` ###### **Options:** -* `--domain ` — Override this hive's DNS domain (the `swarm.peers` attrset key the peer declares). Omit to auto-fill from the running daemon (`services.hyperhive.domain`); pass it only when the daemon is down or you're scripting offline * `--wg-address ` — This hive's WireGuard mesh address (e.g. `10.42.0.1/32`), emitted as `wireguardAddress`. Omit when not running the mesh * `--wg-endpoint ` — This hive's public WireGuard endpoint (`host:port`), emitted as `wireguardEndpoint`. Omit when peers dial in / no mesh diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 73677da0..a7c1c25d 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -94,16 +94,11 @@ enum Cmd { /// to trust + reach this hive. Emits `caCert` (+ a `cp` line for the /// cert) when this hive serves a self-signed CA, the WireGuard public /// key when the mesh key exists, and the `wireguard{Address,Endpoint}` - /// you pass. Reads local state (the TLS CA cert, the wg key) + asks the - /// daemon for this hive's domain; never mutates. `wg init` calls this - /// at the end, so a fresh mesh setup prints the hand-over block too. + /// you pass. The hive's own domain is filled in automatically from the + /// running daemon (`services.hyperhive.domain`). Reads local state (the + /// TLS CA cert, the wg key); never mutates. `wg init` calls this at the + /// end, so a fresh mesh setup prints the hand-over block too. PeerConfig { - /// Override this hive's DNS domain (the `swarm.peers` attrset key - /// the peer declares). Omit to auto-fill from the running daemon - /// (`services.hyperhive.domain`); pass it only when the daemon is - /// down or you're scripting offline. - #[arg(long)] - domain: Option, /// This hive's WireGuard mesh address (e.g. `10.42.0.1/32`), /// emitted as `wireguardAddress`. Omit when not running the mesh. #[arg(long)] @@ -453,12 +448,6 @@ enum WgCmd { /// printed snippet. Omit to get a placeholder you fill in. #[arg(long)] address: Option, - /// This hive's DNS domain. When set, `init` also prints the - /// `peer-config` block peers paste to federate with this hive - /// (CA + this mesh key), so setup is one command. Omit to skip - /// that and just enable the mesh locally. - #[arg(long)] - domain: Option, }, /// Print the nix snippet to add a peer hive to the mesh. Pure output — /// paste it into this hive's config. Get `` from the peer's @@ -592,9 +581,7 @@ async fn main() -> Result<()> { AgentsCmd::RestartAll => agents_restart_all(&socket).await, }, Cmd::Wg { cmd } => match cmd { - WgCmd::Init { address, domain } => { - wg_init(&socket, address.as_deref(), domain.as_deref()).await - } + WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await, WgCmd::Peer { domain, pubkey, @@ -607,11 +594,10 @@ async fn main() -> Result<()> { WgCmd::Status => wg_status(), }, Cmd::PeerConfig { - domain, wg_address, wg_endpoint, } => { - let domain = resolve_hive_domain(&socket, domain).await?; + let domain = require_hive_domain(&socket).await?; peer_config(&domain, wg_address.as_deref(), wg_endpoint.as_deref()); Ok(()) } @@ -671,27 +657,23 @@ async fn query_hive_domain(socket: &Path) -> Option { .and_then(|r| r.domain) } -/// Resolve this hive's domain for snippet generation: the explicit -/// `--domain` override if given, else the daemon. Errors with a clear -/// hint when neither is available, so `peer-config` never silently emits -/// a wrong key. -async fn resolve_hive_domain(socket: &Path, over: Option) -> Result { - if let Some(d) = over { - return Ok(d); - } +/// Require this hive's domain from the daemon for snippet generation. +/// Errors with a clear hint when it can't be resolved, so `peer-config` +/// never silently emits a wrong key. +async fn require_hive_domain(socket: &Path) -> Result { query_hive_domain(socket).await.context( "could not determine this hive's domain from the daemon — is hive-c0re running \ - and `services.hyperhive.domain` set? pass --domain to override", + and `services.hyperhive.domain` set?", ) } /// `wg init` — generate (if absent) the hive's WireGuard key, print its /// public key + the nix snippet to enable the mesh, then (best-effort) /// the `peer-config` block peers paste to federate with this hive, so a -/// fresh setup is one command. The domain comes from `--domain` or the -/// daemon; if neither resolves, the peer block is skipped (init still -/// succeeds — its core job is enabling the mesh locally). -async fn wg_init(socket: &Path, address: Option<&str>, domain: Option<&str>) -> Result<()> { +/// fresh setup is one command. The domain comes from the daemon; if it +/// can't be resolved, the peer block is skipped (init still succeeds — +/// its core job is enabling the mesh locally). +async fn wg_init(socket: &Path, address: Option<&str>) -> Result<()> { use std::os::unix::fs::PermissionsExt as _; let key_path = Path::new(WG_KEY_PATH); if key_path.exists() { @@ -736,13 +718,9 @@ async fn wg_init(socket: &Path, address: Option<&str>, domain: Option<&str>) -> println!(" }};"); // Also print the block a peer pastes to federate with us (CA + this - // mesh key) — one-stop setup. Domain from --domain or the daemon; - // best-effort, so init still succeeds when neither resolves. - let resolved = match domain { - Some(d) => Some(d.to_owned()), - None => query_hive_domain(socket).await, - }; - if let Some(d) = resolved { + // mesh key) — one-stop setup. Domain comes from the daemon; + // best-effort, so init still succeeds when it can't be resolved. + if let Some(d) = query_hive_domain(socket).await { println!(); peer_config(&d, address, None); } From dc3d6bc753c35f6a2fa94a2c3d95cdf6f4f57f63 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 22 Jun 2026 19:01:55 +0200 Subject: [PATCH 4/4] fix(#1897): treat empty HYPERHIVE_HIVE_DOMAIN as unset in HiveDomain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per argus review: std::env::var(..).ok() yields Some("") for an empty env value, so require_hive_domain would 'succeed' with an empty domain and emit invalid nix (swarm.peers."" = …). Filter empty so it resolves to None → the CLI's clear 'domain unset' error fires instead. --- hive-c0re/src/server.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index fb7d05b2..2248cb90 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -117,9 +117,13 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { // The hive domain is injected into c0re's service env by // hive-c0re.nix (`HYPERHIVE_HIVE_DOMAIN`); surface it so the // operator CLI can fill in this hive's own identity. - HostRequest::HiveDomain => { - HostResponse::hive_domain(std::env::var("HYPERHIVE_HIVE_DOMAIN").ok()) - } + HostRequest::HiveDomain => HostResponse::hive_domain( + // Treat an empty env value as unset — otherwise the CLI + // would emit `swarm.peers."" = …`, invalid nix. + std::env::var("HYPERHIVE_HIVE_DOMAIN") + .ok() + .filter(|d| !d.is_empty()), + ), HostRequest::Pending => HostResponse::pending(coord.approvals.pending()?), HostRequest::Approve { id } => { actions::approve(coord.clone(), *id).await?;