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
|
||||
|
||||
```
|
||||
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)
|
||||
|
|
|
|||
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)]
|
||||
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
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 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")]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue