diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 23fa8de5..e2140da0 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -527,7 +527,7 @@ pub(crate) async fn resolve_approval_dag( let mut terminal_tag = None; match approval.kind { ApprovalKind::Spawn => { - // Post-spawn forge bookkeeping (user, config repo mirror, meta + // Post-spawn forge bookkeeping (config repo mirror, meta // access) — warn-only, then the resolution events + a rescan so // the dashboard reflects the post-spawn state either way. if result.is_ok() { @@ -605,13 +605,10 @@ fn fetch_approval_for_worker( } /// Forge bookkeeping run once after the very first container spawn: -/// create the per-agent forge user, mirror the applied repo, and grant -/// read access to core/meta. Also rescans containers so the dashboard -/// reflects the post-spawn state. +/// mirror the applied repo and grant read access to core/meta. The +/// agent's forge user and token are swarm-controller's, not this +/// hive's. Also rescans containers so the dashboard reflects the post-spawn state. async fn forge_after_first_spawn(coord: &Arc, agent: &str) { - if let Err(e) = crate::forge::ensure_user_for(agent).await { - tracing::warn!(%agent, error = ?e, "forge: ensure_user after first spawn failed"); - } if let Err(e) = crate::forge::ensure_config_repo(agent).await { tracing::warn!(%agent, error = ?e, "forge: ensure_config_repo after first spawn failed"); } diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index 89ca413d..e79e1d65 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -1,4 +1,4 @@ -//! Optional Forgejo wiring — per-agent user + token provisioning, +//! Optional Forgejo wiring — per-agent account alignment, //! config-repo mirroring, meta read-access grants. Also seeds //! `internal/docs` — a private repo every agent gets read-only //! collaborator access to for operator-curated shared content. @@ -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, ensure_user_for, provision_user_token}; +pub use users::{core_token, provision_user_token}; use std::sync::OnceLock; use std::time::{Duration, Instant}; @@ -212,9 +212,9 @@ pub(crate) fn api(token: &str) -> Result { Forgejo::new(Auth::Token(token), url).context("build forgejo api client") } -/// Per-agent forge sync: ensure the agent has a forgejo user + token, -/// a mirrored config repo, read access to `core/meta`, and the `meta` -/// remote in its proposed repo. All operations are idempotent; failures +/// Per-agent forge sync: align the agent's forge account (email, +/// repo-creation lockdown), mirror its config repo, grant read access +/// to `core/meta`, and wire the `meta` remote in its proposed repo. All operations are idempotent; failures /// are logged as warnings but don't abort the caller. /// /// `core_token` is `core_token()` — passed in so callers that already @@ -228,11 +228,10 @@ pub(crate) fn api(token: &str) -> Result { /// dashboard warning (see [`ensure_all`]); the rebuild path ignores it and /// relies on the journal `warn!` lines alone (a rebuild is its own retry). pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool { + // The agent's forge user and token are swarm-controller's now + // (`MintAgentForgeToken`); this hive no longer creates or re-mints + // either. let mut ok = true; - if let Err(e) = ensure_user_for(name).await { - tracing::warn!(%name, error = ?e, "forge: ensure_user failed"); - ok = false; - } // Align email to match the git user.email set by meta::render_flake // so commits link to the agent's Forgejo profile. Best-effort; // also patches up agents created before this fix (old @hive.local). diff --git a/hive-c0re/src/forge/users.rs b/hive-c0re/src/forge/users.rs index 2bc6be80..fc36a113 100644 --- a/hive-c0re/src/forge/users.rs +++ b/hive-c0re/src/forge/users.rs @@ -313,8 +313,8 @@ 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 agents, `CORE_TOKEN_SCOPES` for the -/// bootstrap `core` user. +/// use `TOKEN_SCOPES` for `hivectl forge create-user` accounts and +/// `CORE_TOKEN_SCOPES` for the bootstrap `core` user. async fn mint_token(name: &str, scopes: &str) -> Result { let token_name = format!( "{TOKEN_NAME_PREFIX}-{}", @@ -349,16 +349,6 @@ async fn mint_token(name: &str, scopes: &str) -> Result { Ok(token) } -/// Mint a fresh Forgejo access token for an agent and write it to the -/// agent's state dir via hive-priv. hive-c0re runs unprivileged and -/// cannot write to agent-owned (0755) state directories directly. -async fn mint_and_persist_agent_token(name: &str) -> Result<()> { - let token = mint_token(name, TOKEN_SCOPES).await?; - crate::priv_client::write_agent_forge_token(name, &token) - .await - .with_context(|| format!("write forge-token for {name} via hive-priv")) -} - /// Mint a fresh Forgejo access token for the `core` admin user and /// write it directly to `path`. Unlike agent tokens this path is owned /// by hive-c0re itself (under `/var/lib/hyperhive/`), so a direct @@ -376,24 +366,11 @@ async fn mint_and_persist_core_token(path: &Path) -> Result<()> { Ok(()) } -/// Ensure `name` has a forgejo user + token file. Always re-mints the -/// token so the on-disk file always reflects the current `TOKEN_SCOPES`. -/// Safe to call on every spawn and on every hive-c0re startup. -pub async fn ensure_user_for(name: &str) -> Result<()> { - if !is_present().await { - return Ok(()); - } - ensure_user_exists(name, false, None).await?; - ensure_user_email(name).await; - mint_and_persist_agent_token(name).await -} - /// Provision a forgejo user for `name` and return the freshly-minted -/// token. Unlike [`ensure_user_for`], the token is **not** persisted to -/// disk — the caller is responsible for storing it. Used by `hivectl -/// 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. +/// 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 diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index 978c3ad6..4ef99842 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -361,18 +361,6 @@ pub async fn set_agent_paused(agent_name: &str, paused: bool) -> Result<()> { .await?) } -/// Write the Forgejo access token for `agent_name` to -/// `//state/forge-token` via hive-priv -/// (running as root). The file is written 0600 and chowned to the agent -/// user so it is readable from inside the agent container. -pub async fn write_agent_forge_token(agent_name: &str, token: &str) -> Result<()> { - ok(call(&PrivRequest::WriteAgentForgeToken { - agent_name: agent_name.to_owned(), - token: token.to_owned(), - }) - .await?) -} - /// Write a Matrix access token for `agent_name` via hive-priv (running as /// root). `account: None` writes the hive-internal /// `/matrix-token`; `account: Some(name)` writes diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 0d0e3440..f2c4b389 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -600,36 +600,30 @@ async fn handle_forge_create_user( "hive-forge container not running — wait for hive-c0re to start it before provisioning forge users" ); } - let mut out = Vec::new(); if agent_exists(name)? { - if password.is_some() { - // Agents authenticate by API token, never by password — refuse - // rather than silently dropping a supplied one. - anyhow::bail!( - "forge create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via API token" - ); - } - crate::forge::ensure_user_for(name.as_str()) - .await - .with_context(|| format!("forge create-user {name}"))?; - let path = Coordinator::agent_notes_dir(name).join("forge-token"); - out.push(format!("forge: provisioned agent user '{name}'")); - out.push(format!("token persisted at: {}", path.display())); + // 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 { - let token = crate::forge::provision_user_token(name.as_str(), password) - .await - .with_context(|| format!("forge create-user {name}"))?; - out.push(format!( - "forge: provisioned user '{name}' (not an agent — token not persisted)" - )); - out.push(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(), - ); - } + out.push( + "password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(), + ); } Ok(HostResponse::messages(out)) } diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs index 2e9dcae0..d7263bfb 100644 --- a/hive-host-sock/src/lib.rs +++ b/hive-host-sock/src/lib.rs @@ -316,10 +316,10 @@ pub enum HostRequest { room: Option, }, /// Create or refresh a forge account + API token for `name`. Daemon-side - /// equivalent of `hivectl forge create-user`: for an existing agent it - /// provisions the account and persists the token to `/forge-token`; - /// for a non-agent (operator/human) it mints a user and returns the token - /// in [`HostResponse::messages`]. `password` is resolved client-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, diff --git a/hive-priv-sock/src/lib.rs b/hive-priv-sock/src/lib.rs index 60dfdcd2..8fa577c4 100644 --- a/hive-priv-sock/src/lib.rs +++ b/hive-priv-sock/src/lib.rs @@ -468,19 +468,6 @@ pub enum PrivRequest { }, // --- Agent credential writes --- - /// Write `forge-token` into `AGENT_STATE_ROOT//state/forge-token`. - /// - /// hive-priv validates `agent_name`, creates the state dir if absent, - /// writes the file 0600, and chowns it to the state dir's owner so - /// the agent process can read it. Required because hive-c0re runs - /// unprivileged and cannot write to agent-owned state directories. - WriteAgentForgeToken { - /// Logical agent name (validated by `validate_agent_name`). - agent_name: String, - /// Token value. hive-priv appends a trailing newline before writing. - token: String, - }, - /// Write a matrix access token into the agent's state dir. With /// `account: None` it targets the hive-internal `matrix-token`; with /// `account: Some(name)` it targets `matrix-token-` for an extra @@ -488,8 +475,10 @@ pub enum PrivRequest { /// `account` suffix as plain identifiers before building the path, so a /// crafted account name cannot traverse out of the state dir. /// - /// Same write semantics as `WriteAgentForgeToken` — validates names, - /// creates dir, writes 0600, chowns to agent owner. + /// hive-priv validates the names, creates the state dir if absent, + /// writes the file 0600, and chowns it to the state dir's owner so the + /// agent process can read it. Required because hive-c0re runs + /// unprivileged and cannot write to agent-owned state directories. WriteAgentMatrixToken { /// Logical agent name (validated by `validate_agent_name`). agent_name: String, @@ -512,7 +501,7 @@ pub enum PrivRequest { /// The operator-supplied GitHub personal access token (PAT) for the /// agent's GitHub integration (`services.hyperhive.agent.github.enable`). Same write /// semantics as - /// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir + /// `WriteAgentMatrixToken` — validates `agent_name`, creates the state dir /// if absent, writes the file 0600, and chowns it to the agent so the /// `gh` wrapper / git credential helper can read it. No account suffix /// (single GitHub account per agent). @@ -534,7 +523,7 @@ pub enum PrivRequest { /// `label` MUST be validated as a plain identifier (same rule as the /// matrix `account` suffix) before it goes into the filename — a /// crafted label could otherwise traverse out of the state dir. Same - /// write semantics as `WriteAgentForgeToken` — validates `agent_name`, + /// write semantics as `WriteAgentMatrixToken` — validates `agent_name`, /// creates the state dir if absent, writes both files 0600, chowns to /// the agent. WriteAgentExtraForgeAccount { diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 5a1be8d5..66358cb6 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -403,11 +403,6 @@ async fn exec( paused, } => exec_set_agent_paused(agent_name, paused), - PrivRequest::WriteAgentForgeToken { - ref agent_name, - ref token, - } => write_forge_token(agent_name, token), - PrivRequest::WriteAgentMatrixToken { ref agent_name, ref token, @@ -619,12 +614,6 @@ fn exec_set_agent_paused(agent_name: &str, paused: bool) -> Result<(String, Stri set_agent_paused(agent_name, paused) } -/// `WriteAgentForgeToken`. -fn write_forge_token(agent_name: &str, token: &str) -> Result<(String, String)> { - validate_agent_name(agent_name)?; - write_agent_state_file(agent_name, "forge-token", &format!("{token}\n")) -} - /// `WriteAgentGithubToken`. fn write_github_token(agent_name: &str, token: &str) -> Result<(String, String)> { validate_agent_name(agent_name)?; @@ -1441,7 +1430,7 @@ struct ForgeSidecar<'a> { base_url: &'a str, } -/// Shared helper for `WriteAgentForgeToken` and `WriteAgentMatrixToken`. +/// Shared helper for the `WriteAgent*Token` requests. /// Writes `content` to `AGENT_STATE_ROOT//state/`, /// chowns to the agent user (derived from the state dir's existing owner), /// and chmods 0600. Running as root (hive-priv), so this succeeds diff --git a/hivectl/src/cli.rs b/hivectl/src/cli.rs index a4de9784..35bc83d0 100644 --- a/hivectl/src/cli.rs +++ b/hivectl/src/cli.rs @@ -248,15 +248,15 @@ impl ScopeArgs { #[derive(Subcommand)] pub enum ForgeCmd { - /// Create or refresh the Forgejo account + token for ``. + /// Create or refresh a non-agent Forgejo account + token for ``. /// - /// For an existing agent, persists the token to its state dir; for a - /// human/other account, prints the token to stdout. Set a password to - /// enable forge web-UI login (otherwise it uses a random throwaway). + /// 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. For agents: the container/agent name - /// (`` in `h-`; manager uses the literal `manager`). - /// For humans: any forgejo username — `mara`, `damocles`, etc. + /// 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 diff --git a/hivectl/src/forge.rs b/hivectl/src/forge.rs index bebad895..12ca8964 100644 --- a/hivectl/src/forge.rs +++ b/hivectl/src/forge.rs @@ -1,5 +1,5 @@ //! `hivectl forge create-user ` — provision a Forgejo account via the -//! daemon (which owns the forge admin token + account-token persistence); +//! daemon (which owns the forge admin token); //! hivectl just resolves the password client-side and relays the request. use std::io::{self, Write};