diff --git a/CLAUDE.md b/CLAUDE.md index c235369d..10991b55 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -220,7 +220,7 @@ hive-forge/ Forgejo CLI wrapper (`hive-forge` binary) hive-matrix-mcp/ per-agent matrix-sdk integration (#548 phase 3). src/main.rs `hive-matrix-daemon` binary entry — long-running matrix-sdk Client + sync per agent; serves the - MCP bridge over /run/hive-matrix/socket; emits + MCP bridge over /run/hive-matrix.sock; emits hyperhive wake on incoming room events. src/bin/mcp.rs `hive-matrix-mcp` stdio bridge claude spawns per turn — connects to the daemon socket, forwards diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 92d4e396..9647b06c 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -70,32 +70,11 @@ enum ForgeCmd { /// 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). - /// - /// Without `--password` / `--password-stdin` a random throwaway is - /// used (fine for agents — they auth by token via tea / hive-forge). - /// Set a password to log into the forge web UI afterwards (#663). - /// `--password` is idempotent: re-running with the same value sets - /// the same password (covers password resets on already-created - /// accounts since `forgejo admin user create` silently no-ops once - /// the user exists). CreateUser { /// Forgejo username. For agents: the container/agent name /// (`` in `h-`; manager uses the literal `manager`). /// For humans: any forgejo username — `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, }, } @@ -149,11 +128,7 @@ async fn main() -> Result<()> { let cli = Cli::parse(); match cli.cmd { Cmd::Forge { cmd } => match cmd { - ForgeCmd::CreateUser { - name, - password, - password_stdin, - } => forge_create_user(&name, password.as_deref(), password_stdin).await, + ForgeCmd::CreateUser { name } => forge_create_user(&name).await, }, Cmd::Matrix { cmd } => match cmd { MatrixCmd::CreateUser { @@ -174,23 +149,13 @@ fn is_agent(name: &str) -> bool { Coordinator::agent_state_root(name).exists() } -async fn forge_create_user( - name: &str, - password: Option<&str>, - password_stdin: bool, -) -> Result<()> { +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" ); } - let user_password = resolve_password(password, password_stdin)?; if is_agent(name) { - if user_password.is_some() { - bail!( - "forge create-user: --password / --password-stdin is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via API token" - ); - } hive_c0re::forge::ensure_user_for(name) .await .with_context(|| format!("forge create-user {name}"))?; @@ -198,18 +163,11 @@ async fn forge_create_user( println!("forge: provisioned agent user '{name}'"); println!("token persisted at: {}", path.display()); } else { - let token = hive_c0re::forge::provision_user_token(name, user_password.as_deref()) + 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}"); - if password.is_some() || password_stdin { - println!("password: set as supplied — use it to log into the forge web UI"); - } else { - println!( - "password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)" - ); - } } Ok(()) } @@ -263,7 +221,7 @@ async fn matrix_create_user( // doesn't accept a password — agents auth by access_token, // never by password. Refuse rather than silently dropping it. bail!( - "matrix create-user: --password / --password-stdin is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token" + "matrix create-user: --password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token" ); } hive_c0re::matrix::ensure_user_for(&client, name, ®ister_token) diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 4fe27d5d..9959fcb8 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -195,19 +195,12 @@ async fn forge_http( /// 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 -/// (#663). -async fn ensure_user_exists(name: &str, admin: bool, password: Option<&str>) -> Result<()> { +/// `core` user that drives the API. +async fn ensure_user_exists(name: &str, admin: bool) -> Result<()> { + let mut args = vec!["user", "create", "--username", name, "--email"]; 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"]), - } + args.push(&email); + args.extend(["--random-password", "--must-change-password=false"]); if admin { args.push("--admin"); } @@ -233,23 +226,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. @@ -346,7 +322,7 @@ pub async fn ensure_user_for(name: &str) -> Result<()> { if !is_present().await { return Ok(()); } - ensure_user_exists(name, false, None).await?; + ensure_user_exists(name, false).await?; ensure_user_email(name).await; mint_and_persist_token(name, &token_path(name), TOKEN_SCOPES).await } @@ -357,28 +333,13 @@ pub async fn ensure_user_for(name: &str) -> Result<()> { /// forge create-user` for human (non-agent) accounts so we don't create /// stray `/var/lib/hyperhive/agents//` directories for users that /// aren't agents (#662). -/// -/// `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 -/// (#663). 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 { +pub async fn provision_user_token(name: &str) -> Result { if !is_present().await { anyhow::bail!( "hive-forge container not running — start it (services.hyperhive.forge.enable = true) 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_exists(name, false).await?; ensure_user_email(name).await; mint_token(name, TOKEN_SCOPES).await } @@ -460,7 +421,7 @@ async fn ensure_core_user_and_token() -> Result { return Ok(trimmed); } } - ensure_user_exists("core", true, None).await?; + ensure_user_exists("core", true).await?; mint_and_persist_token("core", path, CORE_TOKEN_SCOPES).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 c695e17c..0c51c123 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -289,12 +289,6 @@ pub async fn ensure_user_for( /// web clients afterwards (#663); for headless agent re-provisioning /// the caller can pass [`random_password`] to keep the existing /// throwaway behaviour. -/// -/// **Not idempotent** (unlike [`forge::provision_user_token`]): the -/// matrix UIAA `/register` endpoint returns `M_USER_IN_USE` (HTTP 400) -/// on second call for the same localpart. 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( client: &reqwest::Client, name: &str,