Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc3d6bc753 | ||
|
|
8ea19b3b12 | ||
|
|
72d9422a7a | ||
|
|
8464cb95cb |
4 changed files with 167 additions and 3 deletions
|
|
@ -24,6 +24,7 @@ This document contains the help content for the `hivectl` command-line program.
|
||||||
* [`hivectl wg init`↴](#hivectl-wg-init)
|
* [`hivectl wg init`↴](#hivectl-wg-init)
|
||||||
* [`hivectl wg peer`↴](#hivectl-wg-peer)
|
* [`hivectl wg peer`↴](#hivectl-wg-peer)
|
||||||
* [`hivectl wg status`↴](#hivectl-wg-status)
|
* [`hivectl wg status`↴](#hivectl-wg-status)
|
||||||
|
* [`hivectl peer-config`↴](#hivectl-peer-config)
|
||||||
* [`hivectl choom`↴](#hivectl-choom)
|
* [`hivectl choom`↴](#hivectl-choom)
|
||||||
* [`hivectl stop`↴](#hivectl-stop)
|
* [`hivectl stop`↴](#hivectl-stop)
|
||||||
* [`hivectl start`↴](#hivectl-start)
|
* [`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
|
* `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)
|
* `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`)
|
* `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. 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
|
* `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 <name>` (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
|
* `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 <name>` (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 <name>`). Requires the hive-c0re daemon
|
* `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 <name>`). Requires the hive-c0re daemon
|
||||||
|
|
@ -344,6 +346,19 @@ 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. 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:**
|
||||||
|
|
||||||
|
* `--wg-address <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 <WG_ENDPOINT>` — This hive's public WireGuard endpoint (`host:port`), emitted as `wireguardEndpoint`. Omit when peers dial in / no mesh
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl choom`
|
## `hivectl choom`
|
||||||
|
|
||||||
Open an interactive Claude session inside an agent container.
|
Open an interactive Claude session inside an agent container.
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,25 @@ enum Cmd {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
cmd: WgCmd,
|
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. 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 {
|
||||||
|
/// 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<String>,
|
||||||
|
/// This hive's public WireGuard endpoint (`host:port`), emitted as
|
||||||
|
/// `wireguardEndpoint`. Omit when peers dial in / no mesh.
|
||||||
|
#[arg(long)]
|
||||||
|
wg_endpoint: Option<String>,
|
||||||
|
},
|
||||||
/// Open an interactive Claude session inside an agent container.
|
/// Open an interactive Claude session inside an agent container.
|
||||||
///
|
///
|
||||||
/// Replaces the current process with `machinectl shell
|
/// Replaces the current process with `machinectl shell
|
||||||
|
|
@ -562,7 +581,7 @@ async fn main() -> Result<()> {
|
||||||
AgentsCmd::RestartAll => agents_restart_all(&socket).await,
|
AgentsCmd::RestartAll => agents_restart_all(&socket).await,
|
||||||
},
|
},
|
||||||
Cmd::Wg { cmd } => match cmd {
|
Cmd::Wg { cmd } => match cmd {
|
||||||
WgCmd::Init { address } => wg_init(address.as_deref()),
|
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
|
||||||
WgCmd::Peer {
|
WgCmd::Peer {
|
||||||
domain,
|
domain,
|
||||||
pubkey,
|
pubkey,
|
||||||
|
|
@ -574,6 +593,14 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
WgCmd::Status => wg_status(),
|
WgCmd::Status => wg_status(),
|
||||||
},
|
},
|
||||||
|
Cmd::PeerConfig {
|
||||||
|
wg_address,
|
||||||
|
wg_endpoint,
|
||||||
|
} => {
|
||||||
|
let domain = require_hive_domain(&socket).await?;
|
||||||
|
peer_config(&domain, wg_address.as_deref(), wg_endpoint.as_deref());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Cmd::Stop { scope, graceful } => stop(&socket, scope.to_scope(), graceful).await,
|
Cmd::Stop { scope, graceful } => stop(&socket, scope.to_scope(), graceful).await,
|
||||||
Cmd::Start { scope } => start(&socket, scope.to_scope()).await,
|
Cmd::Start { scope } => start(&socket, scope.to_scope()).await,
|
||||||
Cmd::Restart { scope, graceful } => restart(&socket, scope.to_scope(), graceful).await,
|
Cmd::Restart { scope, graceful } => restart(&socket, scope.to_scope(), graceful).await,
|
||||||
|
|
@ -612,9 +639,41 @@ const WG_KEY_PATH: &str = "/etc/wireguard/hive.key";
|
||||||
/// The mesh interface name hive-c0re's nix module brings up.
|
/// The mesh interface name hive-c0re's nix module brings up.
|
||||||
const WG_INTERFACE: &str = "wg-hive";
|
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.<d>.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";
|
||||||
|
|
||||||
|
/// 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<String> {
|
||||||
|
hive_c0re::client::request(socket, hive_sh4re::HostRequest::HiveDomain)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|r| r.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String> {
|
||||||
|
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?",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// `wg init` — generate (if absent) the hive's WireGuard key, print its
|
/// `wg init` — generate (if absent) the hive's WireGuard key, print its
|
||||||
/// public key + the nix snippet to enable the mesh.
|
/// public key + the nix snippet to enable the mesh, then (best-effort)
|
||||||
fn wg_init(address: Option<&str>) -> Result<()> {
|
/// the `peer-config` block peers paste to federate with this hive, so a
|
||||||
|
/// 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 _;
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
let key_path = Path::new(WG_KEY_PATH);
|
let key_path = Path::new(WG_KEY_PATH);
|
||||||
if key_path.exists() {
|
if key_path.exists() {
|
||||||
|
|
@ -657,6 +716,14 @@ fn wg_init(address: Option<&str>) -> Result<()> {
|
||||||
println!(" address = \"{addr}\";");
|
println!(" address = \"{addr}\";");
|
||||||
println!(" # listenPort = 51820; # default");
|
println!(" # listenPort = 51820; # default");
|
||||||
println!(" }};");
|
println!(" }};");
|
||||||
|
|
||||||
|
// Also print the block a peer pastes to federate with us (CA + this
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -702,6 +769,51 @@ fn wg_peer(domain: &str, pubkey: &str, address: &str, endpoint: Option<&str>) {
|
||||||
println!(" }};");
|
println!(" }};");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `peer-config` — print the `swarm.peers."<domain>"` 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`),
|
/// `wg status` — show the live mesh interface (`wg show wg-hive`),
|
||||||
/// inheriting stdout so the operator sees it directly.
|
/// inheriting stdout so the operator sees it directly.
|
||||||
fn wg_status() -> Result<()> {
|
fn wg_status() -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,16 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
}
|
}
|
||||||
HostRequest::Rebuild { name } => handle_rebuild(&coord, name).await?,
|
HostRequest::Rebuild { name } => handle_rebuild(&coord, name).await?,
|
||||||
HostRequest::List => HostResponse::list(lifecycle::list().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(
|
||||||
|
// 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::Pending => HostResponse::pending(coord.approvals.pending()?),
|
||||||
HostRequest::Approve { id } => {
|
HostRequest::Approve { id } => {
|
||||||
actions::approve(coord.clone(), *id).await?;
|
actions::approve(coord.clone(), *id).await?;
|
||||||
|
|
@ -210,6 +220,7 @@ async fn handle_restart_all() -> Result<HostResponse> {
|
||||||
error: Some(errors.join("; ")),
|
error: Some(errors.join("; ")),
|
||||||
agents: Some(ok_agents),
|
agents: Some(ok_agents),
|
||||||
approvals: None,
|
approvals: None,
|
||||||
|
domain: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -368,6 +379,7 @@ fn finish_lifecycle(ok_items: Vec<String>, errors: &[String]) -> HostResponse {
|
||||||
error: Some(errors.join("; ")),
|
error: Some(errors.join("; ")),
|
||||||
agents: Some(ok_items),
|
agents: Some(ok_items),
|
||||||
approvals: None,
|
approvals: None,
|
||||||
|
domain: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,11 @@ pub enum HostRequest {
|
||||||
Rebuild { name: String },
|
Rebuild { name: String },
|
||||||
/// List managed containers.
|
/// List managed containers.
|
||||||
List,
|
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.
|
/// List pending approval requests.
|
||||||
Pending,
|
Pending,
|
||||||
/// Approve a pending request by id; the action runs immediately.
|
/// Approve a pending request by id; the action runs immediately.
|
||||||
|
|
@ -131,6 +136,10 @@ pub struct HostResponse {
|
||||||
pub agents: Option<Vec<String>>,
|
pub agents: Option<Vec<String>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub approvals: Option<Vec<Approval>>,
|
pub approvals: Option<Vec<Approval>>,
|
||||||
|
/// 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<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row in the approval queue. `commit_ref` is overloaded per
|
/// One row in the approval queue. `commit_ref` is overloaded per
|
||||||
|
|
@ -218,6 +227,7 @@ impl HostResponse {
|
||||||
error: None,
|
error: None,
|
||||||
agents: None,
|
agents: None,
|
||||||
approvals: None,
|
approvals: None,
|
||||||
|
domain: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,6 +238,7 @@ impl HostResponse {
|
||||||
error: Some(message.into()),
|
error: Some(message.into()),
|
||||||
agents: None,
|
agents: None,
|
||||||
approvals: None,
|
approvals: None,
|
||||||
|
domain: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,6 +249,7 @@ impl HostResponse {
|
||||||
error: None,
|
error: None,
|
||||||
agents: Some(agents),
|
agents: Some(agents),
|
||||||
approvals: None,
|
approvals: None,
|
||||||
|
domain: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,6 +260,19 @@ impl HostResponse {
|
||||||
error: None,
|
error: None,
|
||||||
agents: None,
|
agents: None,
|
||||||
approvals: Some(approvals),
|
approvals: Some(approvals),
|
||||||
|
domain: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `HiveDomain` result — this hive's canonical domain (or `None`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn hive_domain(domain: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
ok: true,
|
||||||
|
error: None,
|
||||||
|
agents: None,
|
||||||
|
approvals: None,
|
||||||
|
domain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue