hivectl: print token to stdout for non-agent users (#662)

This commit is contained in:
damocles 2026-05-30 21:39:42 +02:00 committed by Mara
commit 18253bf2f5
3 changed files with 121 additions and 40 deletions

View file

@ -17,6 +17,7 @@
use anyhow::{Context as _, Result, bail};
use clap::{Parser, Subcommand};
use hive_c0re::coordinator::Coordinator;
#[derive(Parser)]
#[command(
@ -58,13 +59,21 @@ enum Cmd {
#[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`.
///
/// When `<name>` matches an existing agent (i.e. it has a state
/// dir under `/var/lib/hyperhive/agents/`), persists the token to
/// `<state>/forge-token` (idempotent: re-mints + rewrites every
/// call so the on-disk scope matches the current
/// `forge::TOKEN_SCOPES`).
///
/// When `<name>` is **not** an agent (a human or any other
/// non-container account), creates the forgejo user and prints the
/// freshly-minted token to stdout — no `/var/lib/hyperhive/agents/`
/// directory is created for the user (#662).
CreateUser {
/// Container/agent name (the `<name>` in `h-<name>`; manager
/// agent uses the literal `manager`).
/// Forgejo username. For agents: the container/agent name
/// (`<n>` in `h-<n>`; manager uses the literal `manager`).
/// For humans: any forgejo username — `mara`, `damocles`, etc.
name: String,
},
}
@ -72,11 +81,20 @@ enum ForgeCmd {
#[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`.
///
/// When `<name>` matches an existing agent (i.e. it has a state
/// dir under `/var/lib/hyperhive/agents/`), persists the token to
/// `<state>/matrix-token`. Skips registration when the file is
/// already populated; delete it to force re-registration.
///
/// When `<name>` is **not** an agent (a human or any other
/// non-container account), registers the matrix user and prints
/// the freshly-minted access token to stdout — no
/// `/var/lib/hyperhive/agents/` directory is created for the user
/// (#662).
CreateUser {
/// Container/agent name.
/// Matrix localpart. For agents: the container/agent name.
/// For humans: any matrix localpart — `mara`, `damocles`, etc.
name: String,
},
}
@ -100,16 +118,35 @@ async fn main() -> Result<()> {
}
}
/// True when `name` matches an existing hyperhive agent — i.e. it has a
/// persistent state dir under `/var/lib/hyperhive/agents/`. We use the
/// state dir (not the live container list) so kept-state tombstones
/// still resolve as agents — re-provisioning a destroyed-but-kept agent
/// should still drop its token in the existing state tree.
fn is_agent(name: &str) -> bool {
Coordinator::agent_state_root(name).exists()
}
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)");
if is_agent(name) {
hive_c0re::forge::ensure_user_for(name)
.await
.with_context(|| format!("forge create-user {name}"))?;
let path = Coordinator::agent_notes_dir(name).join("forge-token");
println!("forge: provisioned agent user '{name}'");
println!("token persisted at: {}", path.display());
} else {
let token = hive_c0re::forge::provision_user_token(name)
.await
.with_context(|| format!("forge create-user {name}"))?;
println!("forge: provisioned user '{name}' (not an agent — token not persisted)");
println!("token: {token}");
}
Ok(())
}
@ -125,9 +162,19 @@ async fn matrix_create_user(name: &str) -> Result<()> {
.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)");
if is_agent(name) {
hive_c0re::matrix::ensure_user_for(&client, name, &register_token)
.await
.with_context(|| format!("matrix create-user {name}"))?;
let path = Coordinator::agent_notes_dir(name).join("matrix-token");
println!("matrix: provisioned agent user '{name}'");
println!("token persisted at: {}", path.display());
} else {
let token = hive_c0re::matrix::provision_user_token(&client, name, &register_token)
.await
.with_context(|| format!("matrix create-user {name}"))?;
println!("matrix: provisioned user '{name}' (not an agent — token not persisted)");
println!("token: {token}");
}
Ok(())
}