hivectl: add operator-facing host CLI with forge + matrix create-user verbs (#655)
This commit is contained in:
parent
2e40e1782a
commit
53447842bc
6 changed files with 214 additions and 41 deletions
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -16,11 +16,24 @@ when you need depth on a subsystem. This file is the index.
|
||||||
## File map
|
## File map
|
||||||
|
|
||||||
```
|
```
|
||||||
hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched)
|
hive-c0re/ host daemon + sibling operator CLI (lib + 2 bins)
|
||||||
src/main.rs clap setup; serve / spawn / kill / rebuild / list /
|
src/lib.rs `pub mod` re-exports for every module; shared by
|
||||||
pending / approve / deny / destroy [--purge] /
|
both binaries so they import from the same
|
||||||
request-spawn / set-parent (--parent / --root);
|
tree (no per-bin module duplication).
|
||||||
periodic vacuum tasks
|
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/server.rs host admin socket (HostRequest → dispatch)
|
||||||
src/client.rs admin-socket client
|
src/client.rs admin-socket client
|
||||||
src/manager_server.rs manager-privileged socket (ManagerRequest)
|
src/manager_server.rs manager-privileged socket (ManagerRequest)
|
||||||
|
|
|
||||||
133
hive-c0re/src/bin/hivectl.rs
Normal file
133
hive-c0re/src/bin/hivectl.rs
Normal 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, ®ister_token)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("matrix create-user {name}"))?;
|
||||||
|
println!("matrix: provisioned user '{name}' (idempotent)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -277,7 +277,7 @@ struct PortConflict {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
pub(crate) struct TombstoneView {
|
pub struct TombstoneView {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Bytes used by the state dir tree. Cheap-ish to compute; let the
|
/// Bytes used by the state dir tree. Cheap-ish to compute; let the
|
||||||
/// operator know how much they're holding onto.
|
/// operator know how much they're holding onto.
|
||||||
|
|
@ -462,7 +462,7 @@ fn build_port_conflicts(containers: &[ContainerView]) -> Vec<PortConflict> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
pub(crate) struct MetaInputView {
|
pub struct MetaInputView {
|
||||||
/// Input key in meta's `flake.nix` — `hyperhive`, `agent-<n>`, etc.
|
/// Input key in meta's `flake.nix` — `hyperhive`, `agent-<n>`, etc.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Full locked sha. Not displayed verbatim; the dashboard
|
/// 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
|
/// allow-list + `is_file` check happens via the same
|
||||||
/// `resolve_state_path` helper the read endpoint uses, so the
|
/// `resolve_state_path` helper the read endpoint uses, so the
|
||||||
/// security rules can't drift.
|
/// 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] = [
|
const PREFIXES: [&str; 4] = [
|
||||||
"/agents/",
|
"/agents/",
|
||||||
"/shared/",
|
"/shared/",
|
||||||
|
|
|
||||||
44
hive-c0re/src/lib.rs
Normal file
44
hive-c0re/src/lib.rs
Normal 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;
|
||||||
|
|
@ -5,38 +5,17 @@ use anyhow::{Context as _, Result, bail};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use hive_sh4re::{HostRequest, HostResponse};
|
use hive_sh4re::{HostRequest, HostResponse};
|
||||||
|
|
||||||
mod actions;
|
// Every module hangs off the `hive_c0re` library (see `src/lib.rs`).
|
||||||
mod agent_server;
|
// The daemon and the `hivectl` sibling binary share the same module
|
||||||
mod approvals;
|
// tree — no per-binary duplication. Enumerated rather than wildcard
|
||||||
mod auto_update;
|
// so clippy stays happy + the lib surface this bin consumes is
|
||||||
mod broker;
|
// explicit (any new daemon entry point reads off the next add).
|
||||||
mod client;
|
use hive_c0re::coordinator::Coordinator;
|
||||||
mod container_view;
|
use hive_c0re::{
|
||||||
mod coordinator;
|
auto_update, broker, client, crash_watch, dashboard, dashboard_events, events_vacuum, forge,
|
||||||
mod crash_watch;
|
manager_server, matrix, migrate, rebuild_queue, reminder_scheduler, scheduled_prompts_worker,
|
||||||
mod dashboard;
|
server, stats_vacuum,
|
||||||
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;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")]
|
#[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")]
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,11 @@ in
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = hyperhivePackage pkgs.stdenv.hostPlatform.system;
|
default = hyperhivePackage pkgs.stdenv.hostPlatform.system;
|
||||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";
|
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 {
|
frontend = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue