hivectl: wireguard mesh setup verbs (#1756)
One-time-setup convenience for the inter-hive WireGuard mesh (services.hyperhive.swarm) so nobody has to remember the wg dance: - hivectl wg init [--address X] — generate (if absent) the hive's private key at /etc/wireguard/hive.key (0400, never clobbered), derive + print the public key, and print the swarm.wireguard nix snippet to enable the mesh. - hivectl wg peer <domain> --pubkey --address [--endpoint] — print the swarm.peers.<domain> nix snippet to add a remote hive. - hivectl wg status — wrap wg show wg-hive. Hybrid model per the design: the verb owns the imperative state (the key file), the operator pastes the printed nix into host config (kept in git) — nothing mutates declarative config behind their back. hivectl-only (root host ops, like the gateway htpasswd verbs); no priv/wire/c0re changes. flake: wrap hivectl with wireguard-tools on PATH so wg resolves even before the mesh config (which would otherwise pull it in) exists — wg init is the first setup step. Add clippy.toml doc-valid-idents for the WireGuard proper noun. Regenerate hivectl-cli.md.
This commit is contained in:
parent
c7612dcf2b
commit
5336be7813
4 changed files with 252 additions and 4 deletions
4
clippy.toml
Normal file
4
clippy.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Proper nouns / product names that clippy's `doc_markdown` lint would
|
||||
# otherwise flag as un-backticked identifiers in doc-comments. `".."`
|
||||
# keeps clippy's built-in default list (GitHub, OAuth, …) and extends it.
|
||||
doc-valid-idents = ["WireGuard", ".."]
|
||||
|
|
@ -20,6 +20,10 @@ This document contains the help content for the `hivectl` command-line program.
|
|||
* [`hivectl agents`↴](#hivectl-agents)
|
||||
* [`hivectl agents restart`↴](#hivectl-agents-restart)
|
||||
* [`hivectl agents restart-all`↴](#hivectl-agents-restart-all)
|
||||
* [`hivectl wg`↴](#hivectl-wg)
|
||||
* [`hivectl wg init`↴](#hivectl-wg-init)
|
||||
* [`hivectl wg peer`↴](#hivectl-wg-peer)
|
||||
* [`hivectl wg status`↴](#hivectl-wg-status)
|
||||
* [`hivectl choom`↴](#hivectl-choom)
|
||||
* [`hivectl stop`↴](#hivectl-stop)
|
||||
* [`hivectl start`↴](#hivectl-start)
|
||||
|
|
@ -38,6 +42,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
|
|||
* `matrix` — matrix-tuwunel user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`matrix::ensure_all`) — useful when the boot-time sweep skipped an agent (e.g. matrix container wasn't up yet) or to re-register after wiping a token file
|
||||
* `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`)
|
||||
* `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
|
||||
* `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
|
||||
|
|
@ -277,6 +282,60 @@ Stop and restart ALL managed agent containers in sequence. Iterates the live con
|
|||
|
||||
|
||||
|
||||
## `hivectl wg`
|
||||
|
||||
WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`).
|
||||
|
||||
One-time-setup convenience so nobody has to remember the `wg` dance: `wg init` generates + stores this hive's private key and prints the public key plus the nix snippet to enable the mesh; `wg peer` prints the snippet to add a remote hive; `wg status` wraps `wg show`. The verbs own the imperative state (the key file); the printed nix goes into the operator's host config (kept in git), so nothing here mutates declarative config behind the operator's back.
|
||||
|
||||
**Usage:** `hivectl wg <COMMAND>`
|
||||
|
||||
###### **Subcommands:**
|
||||
|
||||
* `init` — Generate (if absent) this hive's WireGuard private key, print its public key, and print the nix snippet to enable the mesh. Idempotent: an existing key is reused, never clobbered (clobbering would break a live mesh). Share the printed public key with peer hives
|
||||
* `peer` — Print the nix snippet to add a peer hive to the mesh. Pure output — paste it into this hive's config. Get `<pubkey>` from the peer's `hivectl wg init`
|
||||
* `status` — Show the live mesh interface state (`wg show wg-hive`). Requires the mesh to be enabled + up
|
||||
|
||||
|
||||
|
||||
## `hivectl wg init`
|
||||
|
||||
Generate (if absent) this hive's WireGuard private key, print its public key, and print the nix snippet to enable the mesh. Idempotent: an existing key is reused, never clobbered (clobbering would break a live mesh). Share the printed public key with peer hives
|
||||
|
||||
**Usage:** `hivectl wg init [OPTIONS]`
|
||||
|
||||
###### **Options:**
|
||||
|
||||
* `--address <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
|
||||
|
||||
|
||||
|
||||
## `hivectl wg peer`
|
||||
|
||||
Print the nix snippet to add a peer hive to the mesh. Pure output — paste it into this hive's config. Get `<pubkey>` from the peer's `hivectl wg init`
|
||||
|
||||
**Usage:** `hivectl wg peer [OPTIONS] --pubkey <PUBKEY> --address <ADDRESS> <DOMAIN>`
|
||||
|
||||
###### **Arguments:**
|
||||
|
||||
* `<DOMAIN>` — Peer hive's DNS domain (the `swarm.peers` attrset key)
|
||||
|
||||
###### **Options:**
|
||||
|
||||
* `--pubkey <PUBKEY>` — Peer's WireGuard public key (from its `hivectl wg init`)
|
||||
* `--address <ADDRESS>` — Peer's mesh address (e.g. `10.42.0.2/32`)
|
||||
* `--endpoint <ENDPOINT>` — Peer's `host:port` endpoint (omit for a peer that only dials out, e.g. one behind NAT — it must set an endpoint pointing back here)
|
||||
|
||||
|
||||
|
||||
## `hivectl wg status`
|
||||
|
||||
Show the live mesh interface state (`wg show wg-hive`). Requires the mesh to be enabled + up
|
||||
|
||||
**Usage:** `hivectl wg status`
|
||||
|
||||
|
||||
|
||||
## `hivectl choom`
|
||||
|
||||
Open an interactive Claude session inside an agent container.
|
||||
|
|
|
|||
21
flake.nix
21
flake.nix
|
|
@ -164,10 +164,14 @@
|
|||
default = craneLib.buildPackage {
|
||||
src = cleanSrc;
|
||||
inherit cargoArtifacts;
|
||||
# `installShellFiles` provides `installShellCompletion` for the
|
||||
# postInstall below; appended (not in the shared set) so it's a
|
||||
# build input only of this binary derivation.
|
||||
nativeBuildInputs = nativeBuildInputs ++ [ pkgs.installShellFiles ];
|
||||
# `installShellFiles` provides `installShellCompletion` and
|
||||
# `makeWrapper` provides `wrapProgram` for the postInstall below;
|
||||
# appended (not in the shared set) so they're build inputs only of
|
||||
# this binary derivation.
|
||||
nativeBuildInputs = nativeBuildInputs ++ [
|
||||
pkgs.installShellFiles
|
||||
pkgs.makeWrapper
|
||||
];
|
||||
pname = "hyperhive-workspace";
|
||||
version = "0.1.0";
|
||||
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-root)";
|
||||
|
|
@ -178,11 +182,20 @@
|
|||
# `$out/share/{zsh/site-functions,bash-completion,fish}/…`; an
|
||||
# operator gets working completion as soon as hivectl is in their
|
||||
# system/user profile with the shell's completion enabled.
|
||||
#
|
||||
# Then wrap hivectl with `wireguard-tools` on PATH so its `wg`
|
||||
# subcommands (`wg init`/`peer`/`status`) work even before the
|
||||
# WireGuard mesh is configured — `wg init` is the *first* setup
|
||||
# step, run before `swarm.wireguard.enable` (which would otherwise
|
||||
# be what pulls wireguard-tools onto the system). Completion
|
||||
# generation runs first since wrapProgram renames the real binary.
|
||||
postInstall = ''
|
||||
installShellCompletion --cmd hivectl \
|
||||
--bash <("$out/bin/hivectl" completions bash) \
|
||||
--zsh <("$out/bin/hivectl" completions zsh) \
|
||||
--fish <("$out/bin/hivectl" completions fish)
|
||||
wrapProgram "$out/bin/hivectl" \
|
||||
--prefix PATH : ${pkgs.wireguard-tools}/bin
|
||||
'';
|
||||
};
|
||||
# Bundled browser assets — see ./nix/frontend.nix. Output is
|
||||
|
|
|
|||
|
|
@ -76,6 +76,19 @@ enum Cmd {
|
|||
#[command(subcommand)]
|
||||
cmd: AgentsCmd,
|
||||
},
|
||||
/// WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`).
|
||||
///
|
||||
/// One-time-setup convenience so nobody has to remember the `wg` dance:
|
||||
/// `wg init` generates + stores this hive's private key and prints the
|
||||
/// public key plus the nix snippet to enable the mesh; `wg peer` prints
|
||||
/// the snippet to add a remote hive; `wg status` wraps `wg show`. The
|
||||
/// verbs own the imperative state (the key file); the printed nix goes
|
||||
/// into the operator's host config (kept in git), so nothing here mutates
|
||||
/// declarative config behind the operator's back.
|
||||
Wg {
|
||||
#[command(subcommand)]
|
||||
cmd: WgCmd,
|
||||
},
|
||||
/// Open an interactive Claude session inside an agent container.
|
||||
///
|
||||
/// Replaces the current process with `machinectl shell
|
||||
|
|
@ -381,6 +394,40 @@ enum GatewayCmd {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum WgCmd {
|
||||
/// Generate (if absent) this hive's WireGuard private key, print its
|
||||
/// public key, and print the nix snippet to enable the mesh. Idempotent:
|
||||
/// an existing key is reused, never clobbered (clobbering would break a
|
||||
/// live mesh). Share the printed public key with peer hives.
|
||||
Init {
|
||||
/// 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.
|
||||
#[arg(long)]
|
||||
address: Option<String>,
|
||||
},
|
||||
/// Print the nix snippet to add a peer hive to the mesh. Pure output —
|
||||
/// paste it into this hive's config. Get `<pubkey>` from the peer's
|
||||
/// `hivectl wg init`.
|
||||
Peer {
|
||||
/// Peer hive's DNS domain (the `swarm.peers` attrset key).
|
||||
domain: String,
|
||||
/// Peer's WireGuard public key (from its `hivectl wg init`).
|
||||
#[arg(long)]
|
||||
pubkey: String,
|
||||
/// Peer's mesh address (e.g. `10.42.0.2/32`).
|
||||
#[arg(long)]
|
||||
address: String,
|
||||
/// Peer's `host:port` endpoint (omit for a peer that only dials out,
|
||||
/// e.g. one behind NAT — it must set an endpoint pointing back here).
|
||||
#[arg(long)]
|
||||
endpoint: Option<String>,
|
||||
},
|
||||
/// Show the live mesh interface state (`wg show wg-hive`). Requires the
|
||||
/// mesh to be enabled + up.
|
||||
Status,
|
||||
}
|
||||
|
||||
/// Default host admin socket path. Must match `hive-c0re`'s default in
|
||||
/// `main.rs` (`/run/hyperhive/host.sock`) — the daemon binds there and
|
||||
/// `hivectl agents` connects to it.
|
||||
|
|
@ -445,6 +492,19 @@ async fn main() -> Result<()> {
|
|||
AgentsCmd::Restart { name } => agents_restart(&socket, &name).await,
|
||||
AgentsCmd::RestartAll => agents_restart_all(&socket).await,
|
||||
},
|
||||
Cmd::Wg { cmd } => match cmd {
|
||||
WgCmd::Init { address } => wg_init(address.as_deref()),
|
||||
WgCmd::Peer {
|
||||
domain,
|
||||
pubkey,
|
||||
address,
|
||||
endpoint,
|
||||
} => {
|
||||
wg_peer(&domain, &pubkey, &address, endpoint.as_deref());
|
||||
Ok(())
|
||||
}
|
||||
WgCmd::Status => wg_status(),
|
||||
},
|
||||
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,
|
||||
|
|
@ -469,6 +529,118 @@ fn generate_completions(shell: clap_complete::Shell) {
|
|||
clap_complete::generate(shell, &mut cmd, "hivectl", &mut std::io::stdout());
|
||||
}
|
||||
|
||||
/// Host path of this hive's WireGuard private key (matches the
|
||||
/// `privateKeyFile` example in the swarm.wireguard nix options).
|
||||
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";
|
||||
|
||||
/// `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<()> {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
let key_path = Path::new(WG_KEY_PATH);
|
||||
if key_path.exists() {
|
||||
println!(
|
||||
"WireGuard private key already present at {WG_KEY_PATH} — reusing (not regenerating)."
|
||||
);
|
||||
} else {
|
||||
if let Some(parent) = key_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create {}", parent.display()))?;
|
||||
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).ok();
|
||||
}
|
||||
let out = std::process::Command::new("wg")
|
||||
.arg("genkey")
|
||||
.output()
|
||||
.context("run `wg genkey` (is wireguard-tools installed?)")?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"wg genkey failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
std::fs::write(key_path, &out.stdout)
|
||||
.with_context(|| format!("write {}", key_path.display()))?;
|
||||
std::fs::set_permissions(key_path, std::fs::Permissions::from_mode(0o400))
|
||||
.with_context(|| format!("chmod 400 {}", key_path.display()))?;
|
||||
println!("Generated WireGuard private key at {WG_KEY_PATH} (0400).");
|
||||
}
|
||||
|
||||
let privkey =
|
||||
std::fs::read(key_path).with_context(|| format!("read {}", key_path.display()))?;
|
||||
let pubkey = wg_pubkey(&privkey)?;
|
||||
let addr = address.unwrap_or("<MESH_ADDRESS e.g. 10.42.0.1/32>");
|
||||
println!("\nPublic key (share this with peer hives — they pass it to `hivectl wg peer`):");
|
||||
println!(" {pubkey}");
|
||||
println!("\nAdd to this hive's NixOS config:");
|
||||
println!(" services.hyperhive.swarm.wireguard = {{");
|
||||
println!(" enable = true;");
|
||||
println!(" privateKeyFile = \"{WG_KEY_PATH}\";");
|
||||
println!(" address = \"{addr}\";");
|
||||
println!(" # listenPort = 51820; # default");
|
||||
println!(" }};");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Derive a WireGuard public key from a private key by piping it through
|
||||
/// `wg pubkey`.
|
||||
fn wg_pubkey(privkey: &[u8]) -> Result<String> {
|
||||
use std::io::Write as _;
|
||||
use std::process::{Command, Stdio};
|
||||
let mut child = Command::new("wg")
|
||||
.arg("pubkey")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("run `wg pubkey` (is wireguard-tools installed?)")?;
|
||||
child
|
||||
.stdin
|
||||
.take()
|
||||
.context("wg pubkey: stdin unavailable")?
|
||||
.write_all(privkey)
|
||||
.context("write private key to `wg pubkey`")?;
|
||||
let out = child.wait_with_output().context("wait for `wg pubkey`")?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"wg pubkey failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
|
||||
}
|
||||
|
||||
/// `wg peer` — print the nix snippet to add a peer hive to the mesh.
|
||||
/// Pure output (no fallible work), so it returns `()`; the dispatch arm
|
||||
/// wraps it in `Ok` to match the sibling verbs.
|
||||
fn wg_peer(domain: &str, pubkey: &str, address: &str, endpoint: Option<&str>) {
|
||||
println!("Add to this hive's NixOS config:");
|
||||
println!(" services.hyperhive.swarm.peers.\"{domain}\" = {{");
|
||||
println!(" wireguardPublicKey = \"{pubkey}\";");
|
||||
println!(" wireguardAddress = \"{address}\";");
|
||||
if let Some(ep) = endpoint {
|
||||
println!(" wireguardEndpoint = \"{ep}\";");
|
||||
}
|
||||
println!(" }};");
|
||||
}
|
||||
|
||||
/// `wg status` — show the live mesh interface (`wg show wg-hive`),
|
||||
/// inheriting stdout so the operator sees it directly.
|
||||
fn wg_status() -> Result<()> {
|
||||
let status = std::process::Command::new("wg")
|
||||
.args(["show", WG_INTERFACE])
|
||||
.status()
|
||||
.context("run `wg show` (is wireguard-tools installed?)")?;
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"`wg show {WG_INTERFACE}` failed — is the mesh enabled + up? \
|
||||
(services.hyperhive.swarm.wireguard.enable = true, then deploy)"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// True when `name` matches an existing hyperhive agent — i.e. it has a
|
||||
/// persistent state dir under `/var/lib/hyperhive/agents/`. We use the
|
||||
/// state dir (not the live container list) so kept-state tombstones
|
||||
|
|
|
|||
Loading…
Reference in a new issue