diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index cf635f73..06ce86e9 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -6,7 +6,6 @@ This document contains the help content for the `hivectl` command-line program. * [`hivectl`↴](#hivectl) * [`hivectl forge`↴](#hivectl-forge) -* [`hivectl forge create-user`↴](#hivectl-forge-create-user) * [`hivectl forge reconcile-config`↴](#hivectl-forge-reconcile-config) * [`hivectl matrix`↴](#hivectl-matrix) * [`hivectl matrix create-user`↴](#hivectl-matrix-create-user) @@ -63,13 +62,13 @@ This document contains the help content for the `hivectl` command-line program. ## `hivectl` -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. +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 matrix container, plus future recovery / debugging verbs. **Usage:** `hivectl [OPTIONS] ` ###### **Subcommands:** -* `forge` — Forgejo user provisioning +* `forge` — Reconcile an agent's config between this hive and the forge * `matrix` — matrix-tuwunel user provisioning * `github` — GitHub account provisioning * `gateway` — Gateway htpasswd user management @@ -95,38 +94,18 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that ## `hivectl forge` -Forgejo user provisioning. +Reconcile an agent's config between this hive and the forge. -Manual entry point to the same idempotent provisioning c0re runs at boot — for recovery, ad-hoc reprovisioning, or fixing one agent without bouncing the daemon. +A human's first SSO login to the forge makes their forge account. **Usage:** `hivectl forge ` ###### **Subcommands:** -* `create-user` — Create or refresh a non-agent Forgejo account + token for `` * `reconcile-config` — Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/` main -## `hivectl forge create-user` - -Create or refresh a non-agent Forgejo account + token for ``. - -Prints the token to stdout. Set a password to enable forge web-UI login (otherwise it uses a random throwaway). Refused for an existing agent: swarm-controller mints an agent's token (`swarmctl agent mint-forge-token `). - -**Usage:** `hivectl forge create-user [OPTIONS] ` - -###### **Arguments:** - -* `` — Forgejo username of a human/other account — `mara`, `damocles`, etc - -###### **Options:** - -* `--password ` — Set the account password to this string instead of a random throwaway. Use this for operator accounts that need to log into the forge web UI. Mutually exclusive with `--password-stdin`. WARNING: the password is visible in shell history + process listings; prefer `--password-stdin` for anything sensitive -* `--password-stdin` — Read the password from stdin (single line, trailing newline stripped) instead of an inline flag. Mutually exclusive with `--password` - - - ## `hivectl forge reconcile-config` Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/` main. diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index e79e1d65..292aebe8 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -21,7 +21,7 @@ pub use repos::{ ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fast_forward_applied_main, fetch_config_main_into_applied, meta_read_access, push_config, push_meta, shared_docs_access, }; -pub use users::{core_token, provision_user_token}; +pub use users::core_token; use std::sync::OnceLock; use std::time::{Duration, Instant}; diff --git a/hive-c0re/src/forge/users.rs b/hive-c0re/src/forge/users.rs index fc36a113..b5a4af57 100644 --- a/hive-c0re/src/forge/users.rs +++ b/hive-c0re/src/forge/users.rs @@ -12,7 +12,7 @@ use forgejo_api::structs::{EditUserOption, UpdateUserAvatarOption}; use forgejo_api::{ApiErrorKind, ForgejoError}; use reqwest::StatusCode; -use super::{CONFIG_ORG, api, forge_admin, is_present}; +use super::{CONFIG_ORG, api, forge_admin}; const TOKEN_NAME_PREFIX: &str = "hyperhive"; // Where the host-side `core` admin token lives. Used by hive-c0re itself @@ -58,18 +58,12 @@ fn config_org_avatar_png_path() -> std::path::PathBuf { this process was started outside that unit", )) } -/// Per-agent token scopes (broad-but-not-admin) for tokens hive-c0re -/// mints itself on the **internal** forge. See `docs/integrations/forge.md::Token -/// scopes` for the per-scope rationale. Not `pub(super)` — external -/// forges (`dashboard/extra_forges.rs`) take an operator-pasted token -/// verbatim, so their scope is whatever the operator's remote account -/// happened to grant; we never mint there and don't need to know it. -const TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc"; /// Bootstrap `core` token scopes — adds `read:admin,write:admin` on -/// top of `TOKEN_SCOPES` so the host daemon can drive -/// `/api/v1/admin/*`. Site-admin membership alone isn't enough: the -/// token's own scope gate runs before the user-permission check. +/// top of the agent scopes (swarm-controller's `AGENT_TOKEN_SCOPES`) +/// so the host daemon can drive `/api/v1/admin/*`. Site-admin +/// membership alone isn't enough: the token's own scope gate runs +/// before the user-permission check. /// See `docs/integrations/forge.md::Token scopes`. const CORE_TOKEN_SCOPES: &str = "read:admin,write:admin,read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc"; @@ -137,18 +131,20 @@ fn is_forbidden(e: &ForgejoError) -> bool { /// Ensure a forgejo user named `name` exists. Idempotent: forgejo /// returns a "user already exists" error which we treat as success. /// `admin` adds `--admin` (site admin) — used for the bootstrap -/// `core` user that drives the API. `password` picks the initial -/// account password: `None` uses `--random-password` (the existing -/// agent provisioning shape — the password is never read, agents auth -/// by token); `Some(pw)` uses `--password ` so the operator path -/// in `hivectl` can set a real password for matrix-style web-UI login. -async fn ensure_user_exists(name: &str, admin: bool, password: Option<&str>) -> Result<()> { +/// `core` user that drives the API. The password is random and never +/// read: the account authenticates by token. +async fn ensure_user_exists(name: &str, admin: bool) -> Result<()> { let email = agent_email(name); - let mut args = vec!["user", "create", "--username", name, "--email", &email]; - match password { - Some(pw) => args.extend(["--password", pw, "--must-change-password=false"]), - None => args.extend(["--random-password", "--must-change-password=false"]), - } + let mut args = vec![ + "user", + "create", + "--username", + name, + "--email", + &email, + "--random-password", + "--must-change-password=false", + ]; if admin { args.push("--admin"); } @@ -174,30 +170,6 @@ async fn ensure_user_exists(name: &str, admin: bool, password: Option<&str>) -> } } -/// Set the forgejo password for an existing user. Used by the operator -/// path in `hivectl forge create-user --password` so re-running on an -/// already-created account still updates the password (covers the -/// "I forgot the password I set last week" case + the "argus retried -/// the verb to verify the fix" case — `forgejo admin user create` -/// silently skips a password change once the account exists). Idempotent -/// from the operator's point of view: same password input → same final -/// account state. -async fn change_user_password(name: &str, password: &str) -> Result<()> { - let args = [ - "user", - "change-password", - "--username", - name, - "--password", - password, - ]; - forge_admin(&args) - .await - .with_context(|| format!("forgejo admin user change-password {name}"))?; - tracing::info!(%name, "forge: changed user password"); - Ok(()) -} - /// Idempotently align the Forgejo account email to `agent_email(name)`. /// Existing agents were created with `{name}@hive.local`; this corrects /// that so git commits (which use `{name}@hyperhive`) link to profiles. @@ -313,8 +285,7 @@ pub(super) async fn ensure_repo_creation_disabled(name: &str) { /// a monotonic clock so re-issuing doesn't collide with an existing /// token of the same name in the DB. `scopes` is the scope string /// passed to `forgejo admin user generate-access-token --scopes`; -/// use `TOKEN_SCOPES` for `hivectl forge create-user` accounts and -/// `CORE_TOKEN_SCOPES` for the bootstrap `core` user. +/// the bootstrap `core` user, its one caller, passes `CORE_TOKEN_SCOPES`. async fn mint_token(name: &str, scopes: &str) -> Result { let token_name = format!( "{TOKEN_NAME_PREFIX}-{}", @@ -366,37 +337,6 @@ async fn mint_and_persist_core_token(path: &Path) -> Result<()> { Ok(()) } -/// Provision a forgejo user for `name` and return the freshly-minted -/// token, which is **not** persisted to disk — the caller is responsible -/// for storing it. Used by `hivectl forge create-user` for human -/// (non-agent) accounts. Agent tokens are minted by swarm-controller -/// (`swarm-controller/src/forge/agent_token.rs`), not here. -/// -/// `password` picks the account password. `None` keeps the existing -/// random-throwaway shape (caller doesn't need web UI access — token -/// alone is enough). `Some(pw)` sets `pw` as the password, including -/// running `forgejo admin user change-password` if the account already -/// exists, so the operator can log into the forge web UI afterwards. -/// Idempotent: re-running with the same `Some(pw)` lands on the same -/// final state. -pub async fn provision_user_token(name: &str, password: Option<&str>) -> Result { - if !is_present().await { - anyhow::bail!( - "hive-forge container not running — wait for hive-c0re to start it before provisioning forge users" - ); - } - ensure_user_exists(name, false, password).await?; - if let Some(pw) = password { - // `user create` silently no-ops on an existing account, so - // we run change-password unconditionally when the caller - // asked for a specific password — keeps the verb idempotent - // for "set or reset" use. - change_user_password(name, pw).await?; - } - ensure_user_email(name).await; - mint_token(name, TOKEN_SCOPES).await -} - /// Set `core`'s Forgejo avatar to the hyperhive logo once, then /// remember it so subsequent startups don't re-upload. Best-effort /// — any non-2xx is logged at the caller; the project runs fine @@ -555,7 +495,7 @@ pub(super) async fn ensure_core_user_and_token() -> Result { } } } - ensure_user_exists("core", true, None).await?; + ensure_user_exists("core", true).await?; mint_and_persist_core_token(path).await?; let raw = std::fs::read_to_string(path) .with_context(|| format!("read {CORE_TOKEN_PATH} after mint"))?; diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 224ea86a..59f222ab 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -868,9 +868,9 @@ async fn finish_user_provisioning(name: &str, access_token: &str) -> Result<()> /// can pass [`random_password`] to keep the existing throwaway /// behaviour. /// -/// **Not idempotent** (unlike [`crate::forge::provision_user_token`]): the -/// matrix `/register` endpoint returns `M_USER_IN_USE` (HTTP 400) on a -/// second call for the same localpart, appservice-authorised or not. +/// **Not idempotent**: the matrix `/register` endpoint returns +/// `M_USER_IN_USE` (HTTP 400) on a second call for the same localpart, +/// appservice-authorised or not. /// Callers re-running this for a known-existing matrix user should expect /// a hard error from this fn and route to a password-reset path instead. pub async fn provision_user_token( diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index f2c4b389..8b1ae675 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -233,9 +233,6 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { HostRequest::MatrixInvite { user, room } => { handle_matrix_invite(user, room.as_deref()).await? } - HostRequest::ForgeCreateUser { name, password } => { - handle_forge_create_user(name, password.as_deref()).await? - } HostRequest::ReconcileConfigStatus { agent, verbose } => { crate::forge::reconcile_config_status(agent.as_str(), *verbose).await? } @@ -591,43 +588,6 @@ async fn handle_matrix_create_user( Ok(HostResponse::messages(out)) } -async fn handle_forge_create_user( - name: &hive_types::Ident, - password: Option<&str>, -) -> Result { - if !crate::forge::is_present().await { - anyhow::bail!( - "hive-forge container not running — wait for hive-c0re to start it before provisioning forge users" - ); - } - if agent_exists(name)? { - // An agent's forge user and token are swarm-controller's: it mints - // the token into the swarm secret store and the agent fetches it - // from there. A hive-minted token would be one the swarm neither - // tracks nor rotates. - anyhow::bail!( - "forge create-user: '{name}' is an agent; its forge token is minted by \ - swarm-controller — run `swarmctl agent mint-forge-token {name}` on the \ - controller host" - ); - } - let token = crate::forge::provision_user_token(name.as_str(), password) - .await - .with_context(|| format!("forge create-user {name}"))?; - let mut out = vec![ - format!("forge: provisioned user '{name}' (not an agent — token not persisted)"), - format!("token: {token}"), - ]; - if password.is_some() { - out.push("password: set as supplied — use it to log into the forge web UI".to_owned()); - } else { - out.push( - "password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(), - ); - } - Ok(HostResponse::messages(out)) -} - async fn handle_set_agent_github_token(agent: &str, token: &str) -> Result { crate::priv_client::write_agent_github_token(agent, token) .await diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs index d7263bfb..40cfa32e 100644 --- a/hive-host-sock/src/lib.rs +++ b/hive-host-sock/src/lib.rs @@ -315,17 +315,6 @@ pub enum HostRequest { #[serde(default)] room: Option, }, - /// Create or refresh a forge account + API token for `name`. Daemon-side - /// equivalent of `hivectl forge create-user`: for a non-agent - /// (operator/human) it mints a user and returns the token in - /// [`HostResponse::messages`]. An existing agent is refused: its token is - /// swarm-controller's to mint. `password` is resolved client-side - /// (inline flag or stdin) and only meaningful for non-agent accounts. - ForgeCreateUser { - name: Ident, - #[serde(default)] - password: Option, - }, /// Report the divergence between agent `agent`'s local applied config /// checkout and its forge `agent-configs/` `main`. The daemon /// fetches forge `main` read-only and returns a human-readable report diff --git a/hivectl/src/cli.rs b/hivectl/src/cli.rs index 35bc83d0..42c2b6fd 100644 --- a/hivectl/src/cli.rs +++ b/hivectl/src/cli.rs @@ -11,7 +11,7 @@ use std::path::PathBuf; 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 \ +provisioning on the bundled matrix container, plus future \ recovery / debugging verbs.\ " )] @@ -28,11 +28,10 @@ pub struct Cli { #[derive(Subcommand)] pub enum Cmd { - /// Forgejo user provisioning. + /// Reconcile an agent's config between this hive and the forge. /// - /// Manual entry point to the same idempotent provisioning c0re runs at - /// boot — for recovery, ad-hoc reprovisioning, or fixing one agent - /// without bouncing the daemon. + /// A human's first SSO login to the forge makes their forge + /// account. Forge { #[command(subcommand)] cmd: ForgeCmd, @@ -248,30 +247,6 @@ impl ScopeArgs { #[derive(Subcommand)] pub enum ForgeCmd { - /// Create or refresh a non-agent Forgejo account + token for ``. - /// - /// Prints the token to stdout. Set a password to enable forge web-UI - /// login (otherwise it uses a random throwaway). Refused for an - /// existing agent: swarm-controller mints an agent's token - /// (`swarmctl agent mint-forge-token `). - CreateUser { - /// Forgejo username of a human/other account — `mara`, - /// `damocles`, etc. - name: String, - /// Set the account password to this string instead of a random - /// throwaway. Use this for operator accounts that need to log - /// into the forge web UI. Mutually exclusive with - /// `--password-stdin`. WARNING: the password is visible in - /// shell history + process listings; prefer `--password-stdin` - /// for anything sensitive. - #[arg(long)] - password: Option, - /// Read the password from stdin (single line, trailing newline - /// stripped) instead of an inline flag. Mutually exclusive with - /// `--password`. - #[arg(long, conflicts_with = "password")] - password_stdin: bool, - }, /// Show + reconcile the divergence between an agent's local applied /// config checkout and its forge `agent-configs/` main. /// diff --git a/hivectl/src/forge.rs b/hivectl/src/forge.rs index 12ca8964..15fdb33e 100644 --- a/hivectl/src/forge.rs +++ b/hivectl/src/forge.rs @@ -1,6 +1,5 @@ -//! `hivectl forge create-user ` — provision a Forgejo account via the -//! daemon (which owns the forge admin token); -//! hivectl just resolves the password client-side and relays the request. +//! `hivectl forge reconcile-config ` — show and reconcile an +//! agent's config divergence via the daemon. use std::io::{self, Write}; use std::path::Path; @@ -9,29 +8,7 @@ use anyhow::Result; use hive_host_sock::{HostRequest, ReconcileDirection}; use crate::cli::ReconcileFrom; -use crate::util::{daemon_request, resolve_password}; - -pub(crate) async fn forge_create_user( - socket: &Path, - name: &str, - password: Option<&str>, - password_stdin: bool, -) -> Result<()> { - // Resolve the password client-side (inline flag or stdin read); the - // daemon never touches this process's stdin. The is-present check, the - // agent-vs-operator branch, and token persistence now live in the - // daemon handler. - let password = resolve_password(password, password_stdin)?; - daemon_request( - socket, - hive_host_sock::HostRequest::ForgeCreateUser { - name: crate::util::parse_ident(name)?, - password, - }, - "forge", - ) - .await -} +use crate::util::daemon_request; /// `hivectl forge reconcile-config [--from ] [--verbose]`. /// Always shows the divergence first (daemon computes it read-only), then diff --git a/hivectl/src/main.rs b/hivectl/src/main.rs index 3182deb7..8bb066dc 100644 --- a/hivectl/src/main.rs +++ b/hivectl/src/main.rs @@ -45,7 +45,7 @@ mod github; mod watch; use github::github_set_token; mod forge; -use forge::{forge_create_user, forge_reconcile_config}; +use forge::forge_reconcile_config; mod agents; use agents::{agents_list, run_agent}; mod power; @@ -66,11 +66,6 @@ async fn main() -> Result<()> { let socket = cli.socket; match cli.cmd { Cmd::Forge { cmd } => match cmd { - ForgeCmd::CreateUser { - name, - password, - password_stdin, - } => forge_create_user(&socket, &name, password.as_deref(), password_stdin).await, ForgeCmd::ReconcileConfig { agent, from, diff --git a/swarm-controller/src/forge/agent_token.rs b/swarm-controller/src/forge/agent_token.rs index 19a322d8..caca0cac 100644 --- a/swarm-controller/src/forge/agent_token.rs +++ b/swarm-controller/src/forge/agent_token.rs @@ -30,12 +30,10 @@ use super::Client; /// of its re-mints, and a later sweep of those must never match this one. pub const AGENT_TOKEN_NAME: &str = "swarm-agent"; -/// The scopes an agent token carries. Byte-identical to `hive-c0re`'s -/// `forge::users::TOKEN_SCOPES`, so the move from hive to swarm changes -/// nothing an agent can do. Narrowing it is a separate decision. -/// -/// ⚠️ Duplicated across the crate boundary until the last `hive-c0re` caller -/// of that constant goes. A test here pins the literal. +/// The scopes an agent token carries. Byte-identical to the `TOKEN_SCOPES` +/// `hive-c0re` minted with, so the move from hive to swarm changes nothing an +/// agent can do. Narrowing it is a separate decision. A test here pins the +/// literal. pub const AGENT_TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc"; /// How often [`spawn`] re-checks every agent's token.