hivectl: add operator-facing host CLI with forge + matrix create-user verbs (#655)

This commit is contained in:
damocles 2026-05-30 20:34:59 +02:00
commit 53447842bc
6 changed files with 214 additions and 41 deletions

View file

@ -0,0 +1,133 @@
//! `hivectl` — operator-facing host CLI for hyperhive (#655).
//!
//! Sibling binary to the `hive-c0re` daemon. Where `hive-c0re`'s
//! subcommands focus on the broker / approval / topology surface
//! (`spawn`, `kill`, `rebuild`, `approve` …), `hivectl` covers
//! host-side administration that doesn't need the daemon running —
//! starting with manual user provisioning on the bundled forge +
//! matrix containers when c0re's automatic boot-time sweep is
//! inappropriate (recovery, debugging, single-shot reprovisioning,
//! verifying the registration token path post-#644).
//!
//! Verbs read configuration off the same on-disk paths c0re uses
//! (`/var/lib/hyperhive/forge-core-token`,
//! `/var/lib/hyperhive/matrix-register-token`, per-agent state
//! dirs) and reuse the `forge` / `matrix` modules from the
//! `hive-c0re` lib — single source of truth, no duplication.
use anyhow::{Context as _, Result, bail};
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "hivectl",
about = "hyperhive host CLI — operator-facing administration",
long_about = "\
Sibling to the `hive-c0re` daemon binary. Covers host-side admin \
operations that don't go through the broker manual user \
provisioning on the bundled forge + matrix containers, plus future \
recovery / debugging verbs.\
"
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Forgejo user provisioning. Manual entry point to the same
/// idempotent flow c0re runs automatically at boot
/// (`forge::ensure_all`) — useful for recovery, ad-hoc reprovisioning,
/// or single-agent fixes without bouncing the daemon.
Forge {
#[command(subcommand)]
cmd: ForgeCmd,
},
/// 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.
Matrix {
#[command(subcommand)]
cmd: MatrixCmd,
},
}
#[derive(Subcommand)]
enum ForgeCmd {
/// Create or refresh the Forgejo account + token for `<name>`.
/// Idempotent: skips user creation when the account exists,
/// skips token mint when the token file is already populated.
/// To force re-minting, delete the token file at
/// `/var/lib/hyperhive/agents/<name>/state/forge-token`.
CreateUser {
/// Container/agent name (the `<name>` in `h-<name>`; manager
/// agent uses the literal `manager`).
name: String,
},
}
#[derive(Subcommand)]
enum MatrixCmd {
/// Create or refresh the matrix account + access token for `<name>`.
/// Idempotent: skips registration entirely when the token file is
/// already populated. To force re-registration, delete the token
/// file at `/var/lib/hyperhive/agents/<name>/state/matrix-token`.
CreateUser {
/// Container/agent name.
name: String,
},
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
match cli.cmd {
Cmd::Forge { cmd } => match cmd {
ForgeCmd::CreateUser { name } => forge_create_user(&name).await,
},
Cmd::Matrix { cmd } => match cmd {
MatrixCmd::CreateUser { name } => matrix_create_user(&name).await,
},
}
}
async fn forge_create_user(name: &str) -> Result<()> {
if !hive_c0re::forge::is_present().await {
bail!(
"hive-forge container not running — start it (services.hyperhive.forge.enable = true) before provisioning forge users"
);
}
hive_c0re::forge::ensure_user_for(name)
.await
.with_context(|| format!("forge create-user {name}"))?;
println!("forge: provisioned user '{name}' (idempotent)");
Ok(())
}
async fn matrix_create_user(name: &str) -> Result<()> {
if !hive_c0re::matrix::is_present().await {
bail!(
"hive-matrix container not running — start it (services.hyperhive.matrix.enable = true) before provisioning matrix users"
);
}
let register_token =
hive_c0re::matrix::ensure_register_token().context("read matrix register token")?;
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.context("build reqwest client")?;
hive_c0re::matrix::ensure_user_for(&client, name, &register_token)
.await
.with_context(|| format!("matrix create-user {name}"))?;
println!("matrix: provisioned user '{name}' (idempotent)");
Ok(())
}