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

@ -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 <name>`, `matrix create-user
<name>` — 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)

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(())
}

View file

@ -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<PortConflict> {
}
#[derive(Serialize, Clone, Debug)]
pub(crate) struct MetaInputView {
pub struct MetaInputView {
/// Input key in meta's `flake.nix` — `hyperhive`, `agent-<n>`, 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<String> {
pub fn scan_validated_paths(body: &str) -> Vec<String> {
const PREFIXES: [&str; 4] = [
"/agents/",
"/shared/",

44
hive-c0re/src/lib.rs Normal file
View file

@ -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;

View file

@ -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")]

View file

@ -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;