From 53447842bcba8ad3ff6f6f7ff1142314e1dad55f Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 30 May 2026 20:34:59 +0200 Subject: [PATCH] hivectl: add operator-facing host CLI with forge + matrix create-user verbs (#655) --- CLAUDE.md | 23 ++++-- hive-c0re/src/bin/hivectl.rs | 133 +++++++++++++++++++++++++++++++++++ hive-c0re/src/dashboard.rs | 6 +- hive-c0re/src/lib.rs | 44 ++++++++++++ hive-c0re/src/main.rs | 43 +++-------- nix/modules/hive-c0re.nix | 6 +- 6 files changed, 214 insertions(+), 41 deletions(-) create mode 100644 hive-c0re/src/bin/hivectl.rs create mode 100644 hive-c0re/src/lib.rs diff --git a/CLAUDE.md b/CLAUDE.md index 856f07e4..96fa6871 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,11 +16,24 @@ when you need depth on a subsystem. This file is the index. ## File map ``` -hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched) - src/main.rs clap setup; serve / spawn / kill / rebuild / list / - pending / approve / deny / destroy [--purge] / - request-spawn / set-parent (--parent / --root); - periodic vacuum tasks +hive-c0re/ host daemon + sibling operator CLI (lib + 2 bins) + src/lib.rs `pub mod` re-exports for every module; shared by + both binaries so they import from the same + tree (no per-bin module duplication). + src/main.rs hive-c0re binary: clap setup; serve / spawn / + kill / rebuild / list / pending / approve / + deny / destroy [--purge] / request-spawn / + set-parent (--parent / --root); periodic + vacuum tasks. + src/bin/hivectl.rs `hivectl` binary (#655): operator-facing host + CLI for ad-hoc administration that doesn't + go through the broker. v0 verbs: `forge + create-user `, `matrix create-user + ` — wrap the same idempotent + `forge::ensure_user_for` / + `matrix::ensure_user_for` flows c0re runs + in its boot sweeps, callable manually for + recovery / debug. src/server.rs host admin socket (HostRequest → dispatch) src/client.rs admin-socket client src/manager_server.rs manager-privileged socket (ManagerRequest) diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs new file mode 100644 index 00000000..9d2f64e4 --- /dev/null +++ b/hive-c0re/src/bin/hivectl.rs @@ -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 ``. + /// 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//state/forge-token`. + CreateUser { + /// Container/agent name (the `` in `h-`; manager + /// agent uses the literal `manager`). + name: String, + }, +} + +#[derive(Subcommand)] +enum MatrixCmd { + /// Create or refresh the matrix account + access token for ``. + /// Idempotent: skips registration entirely when the token file is + /// already populated. To force re-registration, delete the token + /// file at `/var/lib/hyperhive/agents//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, ®ister_token) + .await + .with_context(|| format!("matrix create-user {name}"))?; + println!("matrix: provisioned user '{name}' (idempotent)"); + Ok(()) +} diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 79339c00..55e640f3 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -277,7 +277,7 @@ struct PortConflict { } #[derive(Serialize, Clone, Debug)] -pub(crate) struct TombstoneView { +pub struct TombstoneView { pub name: String, /// Bytes used by the state dir tree. Cheap-ish to compute; let the /// operator know how much they're holding onto. @@ -462,7 +462,7 @@ fn build_port_conflicts(containers: &[ContainerView]) -> Vec { } #[derive(Serialize, Clone, Debug)] -pub(crate) struct MetaInputView { +pub struct MetaInputView { /// Input key in meta's `flake.nix` — `hyperhive`, `agent-`, etc. pub name: String, /// Full locked sha. Not displayed verbatim; the dashboard @@ -1507,7 +1507,7 @@ pub(crate) fn emit_meta_inputs_snapshot(coord: &Coordinator) { /// allow-list + `is_file` check happens via the same /// `resolve_state_path` helper the read endpoint uses, so the /// security rules can't drift. -pub(crate) fn scan_validated_paths(body: &str) -> Vec { +pub fn scan_validated_paths(body: &str) -> Vec { const PREFIXES: [&str; 4] = [ "/agents/", "/shared/", diff --git a/hive-c0re/src/lib.rs b/hive-c0re/src/lib.rs new file mode 100644 index 00000000..0ba297c9 --- /dev/null +++ b/hive-c0re/src/lib.rs @@ -0,0 +1,44 @@ +//! `hive-c0re` library — module surface shared by the `hive-c0re` +//! daemon binary and the `hivectl` operator CLI. +//! +//! `hive-c0re` (daemon) keeps the systemd service shape it always had: +//! coordinator, broker, axum dashboard, admin/manager/agent unix +//! sockets, background sweepers. `hivectl` (sibling bin under +//! `src/bin/hivectl.rs`) reuses a thin subset (`forge`, `matrix`, +//! `lifecycle`) to expose host-side administration verbs — manually +//! provisioning forge / matrix users for an agent, etc. (#655). +//! +//! Every module is re-exported `pub` so anything in the crate is +//! addressable from either binary; the lib doesn't have a curated +//! surface beyond "this is where the modules live". + +pub mod actions; +pub mod agent_server; +pub mod approvals; +pub mod auto_update; +pub mod broker; +pub mod client; +pub mod container_view; +pub mod coordinator; +pub mod crash_watch; +pub mod dashboard; +pub mod dashboard_events; +pub mod events_vacuum; +pub mod flake_check; +pub mod forge; +pub mod lifecycle; +pub mod limits; +pub mod loose_ends; +pub mod manager_server; +pub mod matrix; +pub mod meta; +pub mod migrate; +pub mod operator_questions; +pub mod questions; +pub mod rebuild_queue; +pub mod reminder_scheduler; +pub mod scheduled_prompts; +pub mod scheduled_prompts_worker; +pub mod server; +pub mod stats_vacuum; +pub mod topology; diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 717a67b7..35ec3f96 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -5,38 +5,17 @@ use anyhow::{Context as _, Result, bail}; use clap::{Parser, Subcommand}; use hive_sh4re::{HostRequest, HostResponse}; -mod actions; -mod agent_server; -mod approvals; -mod auto_update; -mod broker; -mod client; -mod container_view; -mod coordinator; -mod crash_watch; -mod dashboard; -mod dashboard_events; -mod events_vacuum; -mod stats_vacuum; -mod flake_check; -mod forge; -mod lifecycle; -mod scheduled_prompts; -mod scheduled_prompts_worker; -mod limits; -mod loose_ends; -mod manager_server; -mod matrix; -mod meta; -mod migrate; -mod operator_questions; -mod questions; -mod rebuild_queue; -mod topology; -mod reminder_scheduler; -mod server; - -use coordinator::Coordinator; +// Every module hangs off the `hive_c0re` library (see `src/lib.rs`). +// The daemon and the `hivectl` sibling binary share the same module +// tree — no per-binary duplication. Enumerated rather than wildcard +// so clippy stays happy + the lib surface this bin consumes is +// explicit (any new daemon entry point reads off the next add). +use hive_c0re::coordinator::Coordinator; +use hive_c0re::{ + auto_update, broker, client, crash_watch, dashboard, dashboard_events, events_vacuum, forge, + manager_server, matrix, migrate, rebuild_queue, reminder_scheduler, scheduled_prompts_worker, + server, stats_vacuum, +}; #[derive(Parser)] #[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")] diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index b67a729f..d5e5c4f1 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -63,7 +63,11 @@ in type = lib.types.package; default = hyperhivePackage pkgs.stdenv.hostPlatform.system; defaultText = lib.literalExpression "hyperhive.packages.\${system}.default"; - description = "Package that provides /bin/hive-c0re."; + description = '' + hyperhive workspace package. Provides `/bin/hive-c0re` + (coordinator daemon + admin-socket CLI) and `/bin/hivectl` + (operator-facing host CLI for ad-hoc administration; #655). + ''; }; frontend = lib.mkOption { type = lib.types.package;