From 1de8a0193643c629822da42099c71437262f86ed Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 4 Oct 2026 20:12:48 +0200 Subject: [PATCH] hive-c0re: drop the spawn-time forge user-email, repo-creation and config-repo passes sync_agent no longer runs ensure_user_email, ensure_repo_creation_disabled or ensure_config_repo. swarm-controller does all three before any deploy: CreateForgeUser sets the account email and max_repo_creation = 0 (and the agent_token backfill re-runs CreateForgeUser), CreateRepo creates and protects agent-configs/, AddRepoMember grants the agent write on it, and InitAgentConfigRepo seeds it. What goes with them: the email-aligned and repo-creation-disabled marker paths and the legacy email-marker relocate, sparse_edit_user_option, is_forbidden and its tests, and ensure_org_repo. A config repo deleted by hand is no longer recreated on the next spawn; the operator recreates the agent at swarm level. Comments in swarm-controller that described the removed hive passes now state only the current behaviour. Closes #4848 --- docs/integrations/forge.md | 2 +- hive-c0re/src/forge/mod.rs | 46 +++---- hive-c0re/src/forge/repos.rs | 37 +----- hive-c0re/src/forge/users.rs | 226 ++-------------------------------- hive-c0re/src/paths.rs | 28 ----- swarm-controller/src/forge.rs | 56 +++------ 6 files changed, 44 insertions(+), 351 deletions(-) diff --git a/docs/integrations/forge.md b/docs/integrations/forge.md index b395cf7a..b858d1f2 100644 --- a/docs/integrations/forge.md +++ b/docs/integrations/forge.md @@ -38,7 +38,7 @@ before the user-permission check, so `/api/v1/admin/*` returns `403 Forbidden` for any token without the admin scope bits, even when the bearer is a site admin. -**Migration note**: if `PATCH /api/v1/admin/users/{name}` returns 403 +**Migration note**: if a hive-c0re call to `/api/v1/admin/*` returns 403 on an existing deploy, the core token predates the admin-scope addition. Delete `/var/lib/hyperhive/forge-core-token` and restart hive-c0re to re-mint with the new scopes. diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index f017b758..34f10b0a 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -1,7 +1,7 @@ -//! Optional Forgejo wiring — per-agent account alignment, -//! config-repo mirroring, meta read-access grants, and each agent's -//! read-only grant on `internal/docs` (the swarm-controller creates that -//! repo, with the other swarm-wide orgs, teams and repos). +//! Optional Forgejo wiring — config-repo mirroring, meta read-access +//! grants, and each agent's read-only grant on `internal/docs` (the +//! swarm-controller creates that repo, with the other swarm-wide orgs, +//! teams and repos). //! No-op when `hive-forge` isn't running. Full design: `docs/integrations/forge.md`. mod ci_runner; @@ -13,9 +13,8 @@ mod users; pub use pr_comment::{config_repo, merged_pr_for_commit, post_pr_comment}; pub use reconcile::{fetch_forge_main, reconcile_config_apply, reconcile_config_status}; pub use repos::{ - clone_config_into_proposed, ensure_config_repo, ensure_meta_remote, ensure_repo, - fast_forward_applied_main, fetch_config_main_into_applied, meta_read_access, push_config, - push_meta, shared_docs_access, + clone_config_into_proposed, ensure_meta_remote, ensure_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; @@ -26,10 +25,7 @@ use anyhow::{Context, Result}; use forgejo_api::{Auth, Forgejo}; use url::Url; -use users::{ - ensure_core_avatar, ensure_core_user_and_token, ensure_repo_creation_disabled, - ensure_user_email, -}; +use users::{ensure_core_avatar, ensure_core_user_and_token}; const FORGE_CONTAINER: &str = "hive-forge"; @@ -185,8 +181,7 @@ pub(crate) fn api(token: &str) -> Result { Forgejo::new(Auth::Token(token), url).context("build forgejo api client") } -/// Per-agent forge sync: align the agent's forge account (email, -/// repo-creation lockdown), mirror its config repo, grant read access +/// Per-agent forge sync: mirror the agent's 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. /// @@ -201,27 +196,14 @@ 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. + // The agent's forge account, token and `agent-configs/` repo are + // swarm-controller's; this hive creates none of them and only pushes + // to the repo. let mut ok = true; - // 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). - ensure_user_email(name).await; - // Block direct agent-initiated repo creation: agents create - // repos through hive-c0re, never with their own token. Idempotent + - // marker-guarded; also covers agents provisioned before this landed. - ensure_repo_creation_disabled(name).await; // Mirror the agent's applied config repo into agent-configs. - // ensure_config_repo is idempotent; push_config catches any - // drift since the last run — e.g. the startup migration just - // relocated `deployed/0`, or a deploy landed while the forge - // was down. - if let Err(e) = ensure_config_repo(name).await { - tracing::warn!(%name, error = ?e, "forge: ensure_config_repo failed"); - ok = false; - } + // push_config catches any drift since the last run — e.g. the + // startup migration just relocated `deployed/0`, or a deploy landed + // while the forge was down. if let Err(e) = push_config(name).await { tracing::warn!(%name, error = ?e, "forge: push_config failed"); ok = false; diff --git a/hive-c0re/src/forge/repos.rs b/hive-c0re/src/forge/repos.rs index 93771621..1dad0e6a 100644 --- a/hive-c0re/src/forge/repos.rs +++ b/hive-c0re/src/forge/repos.rs @@ -1,5 +1,5 @@ -//! Repo + org plumbing on the local Forgejo: org / repo creation, -//! the meta + shared-docs + knowledge repos, per-agent config-repo +//! Repo + org plumbing on the local Forgejo: the meta + shared-docs + +//! knowledge repos, per-agent config-repo //! mirroring (`push_config` / `push_meta`), collaborator grants, //! pull-mirrors, and branch-protection rules. The typed API-client //! constructor + org-name constants live in the module root (`super`). @@ -86,15 +86,6 @@ pub async fn ensure_repo(name: &str, token: &str) -> Result<()> { created_or_exists(res, &format!("core/{name}")) } -/// Create `name` inside org `org` (used for `agent-configs/`). -/// Idempotent. -async fn ensure_org_repo(org: &str, name: &str, token: &str) -> Result<()> { - let res = api(token)? - .create_org_repo(org, repo_option(name, true)) - .await; - created_or_exists(res, &format!("{org}/{name}")) -} - /// Dashboard-warning guard for the meta non-fast-forward condition. /// Held while `core/meta` remote is ahead of our local mirror; cleared /// automatically when the next push succeeds. @@ -155,30 +146,6 @@ pub async fn push_meta(dir: &Path) -> Result<()> { Ok(()) } -/// Ensure the `agent-configs/` repo exists so the first -/// `push_config` doesn't 404, and add the agent as a **write** collaborator -/// (it can push feature branches + open config PRs). Branch protection on -/// `main` is swarm-controller's, not set here. No-op when the forge isn't -/// running or the core token isn't minted yet. Safe to call on every spawn -/// and on every startup (all steps idempotent). -pub async fn ensure_config_repo(name: &str) -> Result<()> { - if !is_present().await { - return Ok(()); - } - let Some(token) = core_token() else { - return Ok(()); - }; - ensure_org_repo(CONFIG_ORG, name, &token).await?; - add_collaborator( - CONFIG_ORG, - name, - name, - AddCollaboratorOptionPermission::Write, - &token, - ) - .await -} - /// Grant agent `name` read-only collaborator access to `internal/docs`. /// Idempotent: re-adding an existing collaborator succeeds (Forgejo /// answers 204 either way). Mirrors `meta_read_access` so agents can diff --git a/hive-c0re/src/forge/users.rs b/hive-c0re/src/forge/users.rs index 0ae8eb47..4b313aa1 100644 --- a/hive-c0re/src/forge/users.rs +++ b/hive-c0re/src/forge/users.rs @@ -1,14 +1,12 @@ -//! Per-agent Forgejo user + access-token provisioning, account -//! policy (email alignment, repo-creation lockdown), avatar uploads, -//! and the bootstrap `core` admin user + token lifecycle. The typed -//! API-client constructor + `forgejo admin` helpers live in the -//! module root (`super`). +//! The bootstrap `core` admin user: its account, access-token lifecycle +//! and avatar upload. The typed API-client constructor + `forgejo admin` +//! helpers live in the module root (`super`). use std::path::Path; use anyhow::{Context, Result}; use base64::Engine; -use forgejo_api::structs::{EditUserOption, UpdateUserAvatarOption}; +use forgejo_api::structs::UpdateUserAvatarOption; use forgejo_api::{ApiErrorKind, ForgejoError}; use reqwest::StatusCode; @@ -19,10 +17,9 @@ const TOKEN_NAME_PREFIX: &str = "hyperhive"; // to push the meta repo + drive admin API calls. Root-only. Literal lives // in `crate::paths`; aliased here under the long-standing name. use crate::paths::FORGE_CORE_TOKEN as CORE_TOKEN_PATH; -// Forge provisioning markers (`forge/core-avatar-set`, -// `forge/email-aligned-`) live in `crate::paths` — one-shot guards: -// the upload/align runs once, the marker is written, subsequent startups -// skip. Delete one to force its step to re-run. +// The `forge/core-avatar-set` marker lives in `crate::paths` — a one-shot +// guard: the upload runs once, the marker is written, subsequent startups +// skip. Delete it to force a re-upload. // The avatar PNG path is resolved by `core_avatar_png_path` below — // only-used-here, so it lives in this module rather than the shared // `hive_sh4re::assets` helpers (which now hold only `prompt_template`, the @@ -67,49 +64,6 @@ fn agent_email(name: &str) -> String { format!("{name}@hyperhive.local") } -/// `EditUserOption` with every field unset except the ones Forgejo's -/// validator effectively requires: `login_name` and `source_id = 0` -/// (local auth, the default for users hive-c0re creates). Omitting -/// `login_name` made Forgejo reset `use_custom_avatar` on every admin -/// edit — the same reason the old raw JSON bodies always carried both -/// fields. Callers set only the field(s) they mean to change on top. -fn sparse_edit_user_option(name: &str) -> EditUserOption { - EditUserOption { - active: None, - admin: None, - allow_create_organization: None, - allow_git_hook: None, - allow_import_local: None, - description: None, - email: None, - full_name: None, - hide_email: None, - location: None, - login_name: Some(name.to_owned()), - max_repo_creation: None, - must_change_password: None, - password: None, - prohibit_login: None, - pronouns: None, - restricted: None, - source_id: Some(0), - visibility: None, - website: None, - } -} - -/// Whether a Forgejo API error is a definitive 403. The typed client -/// maps a 403 response to `ApiErrorKind::Forbidden` when the endpoint -/// spec lists it, or to `UnexpectedStatusCode(403)` otherwise — check -/// both defensively. -fn is_forbidden(e: &ForgejoError) -> bool { - match e { - ForgejoError::ApiError(api) => matches!(api.error_kind(), ApiErrorKind::Forbidden), - ForgejoError::UnexpectedStatusCode(s) => *s == StatusCode::FORBIDDEN, - _ => false, - } -} - /// 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 @@ -152,116 +106,6 @@ async fn ensure_user_exists(name: &str, admin: bool) -> Result<()> { } } -/// 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. -/// Best-effort: failures are warned, not propagated. -/// -/// Marker-guarded: writes `EMAIL_ALIGNED_MARKER_PREFIX{name}` on first -/// success and skips the PATCH on all subsequent calls. This prevents -/// Forgejo's admin-user-edit endpoint from resetting `use_custom_avatar` -/// on every `sync_agent` tick. Delete the marker to force re-alignment. -/// -/// Uses the admin REST API (`admin_edit_user`, i.e. `PATCH -/// /api/v1/admin/users/{name}`) rather than `forgejo admin user edit` -/// because the CLI dropped the `edit` subcommand somewhere between -/// forgejo 8 and current. The edit body carries `login_name` + -/// `source_id = 0` via [`sparse_edit_user_option`]. -pub(super) async fn ensure_user_email(name: &str) { - let marker = crate::paths::forge_email_aligned_marker(name); - if marker.exists() { - return; - } - let Some(token) = core_token() else { - tracing::debug!(%name, "forge: skipping ensure_user_email — no core token yet"); - return; - }; - let email = agent_email(name); - let mut edit = sparse_edit_user_option(name); - edit.email = Some(email.clone()); - let client = match api(&token) { - Ok(c) => c, - Err(e) => { - tracing::warn!(%name, error = %e, "forge: PATCH user email: client build failed"); - return; - } - }; - match client.admin_edit_user(name, edit).await { - Ok(_) => { - if let Some(parent) = marker.parent() { - std::fs::create_dir_all(parent).ok(); - } - std::fs::write(&marker, "").ok(); - tracing::info!(%name, %email, "forge: user email aligned"); - } - Err(e) if is_forbidden(&e) => { - // Core token missing admin scope — see - // `docs/integrations/forge.md::Token scopes` migration note. - tracing::warn!( - %name, %email, error = %e, - "forge: PATCH user email forbidden — core token likely missing admin scope. \ - Delete {CORE_TOKEN_PATH} and restart hive-c0re to re-mint with the new scopes." - ); - } - Err(e) => tracing::warn!(%name, %email, error = %e, "forge: PATCH user email failed"), - } -} - -/// Disable direct repo creation for agent `name` by setting -/// `max_repo_creation = 0` on its Forgejo account. A write-scoped token -/// can otherwise create + own repos and self-merge, bypassing the -/// operator-only-merge policy. -/// -/// `max_repo_creation = 0` means `CanCreateRepo()` is false for any -/// count (Forgejo: `MaxRepoCreation >= 0 && NumRepos >= MaxRepoCreation`), -/// so creation is refused while push / PR / clone stay intact. **Existing -/// repos are untouched** — this only blocks *new* direct creation. -/// -/// Marker-guarded like [`ensure_user_email`]: the edit runs once per -/// agent (delete the marker to re-apply). The edit body carries -/// `login_name` + `source_id` for the same reason `ensure_user_email` -/// does — omitting `login_name` makes Forgejo's `EditUserOption` -/// validator reset `use_custom_avatar`. Best-effort: failures warn, -/// don't propagate. -pub(super) async fn ensure_repo_creation_disabled(name: &str) { - let marker = crate::paths::forge_repo_creation_disabled_marker(name); - if marker.exists() { - return; - } - let Some(token) = core_token() else { - tracing::debug!(%name, "forge: skipping ensure_repo_creation_disabled — no core token yet"); - return; - }; - let mut edit = sparse_edit_user_option(name); - edit.max_repo_creation = Some(0); - let client = match api(&token) { - Ok(c) => c, - Err(e) => { - tracing::warn!(%name, error = %e, "forge: PATCH max_repo_creation: client build failed"); - return; - } - }; - match client.admin_edit_user(name, edit).await { - Ok(_) => { - if let Some(parent) = marker.parent() { - std::fs::create_dir_all(parent).ok(); - } - std::fs::write(&marker, "").ok(); - tracing::info!(%name, "forge: disabled direct repo creation (max_repo_creation=0)"); - } - Err(e) if is_forbidden(&e) => { - tracing::warn!( - %name, error = %e, - "forge: PATCH max_repo_creation forbidden — core token likely missing admin scope. \ - Delete {CORE_TOKEN_PATH} and restart hive-c0re to re-mint with the new scopes." - ); - } - Err(e) => { - tracing::warn!(%name, error = %e, "forge: PATCH max_repo_creation failed"); - } - } -} - /// Mint a fresh access token for `name`. Token name is suffixed with /// 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 @@ -461,7 +305,7 @@ pub fn core_token() -> Option { #[cfg(test)] mod tests { - use super::{CoreTokenCheck, classify_core_token_error, is_forbidden}; + use super::{CoreTokenCheck, classify_core_token_error}; use forgejo_api::{ApiError, ApiErrorKind, ForgejoError}; use reqwest::StatusCode; @@ -521,58 +365,4 @@ mod tests { CoreTokenCheck::Indeterminate ); } - - /// The typed client only produces `ApiErrorKind::Forbidden` when the - /// endpoint's spec lists 403; every other endpoint surfaces the same - /// response as a bare `UnexpectedStatusCode`. Both shapes must be - /// recognised, because the arms guarded by this predicate are what tell - /// the operator that `ensure_repo_creation_disabled` — the lockdown - /// stopping an agent from creating and self-merging in its own repo — - /// did not apply, and which credential to re-mint to make it apply. - /// Drop the second arm as "redundant" and that lockdown fails with a - /// generic warning that names no remedy. - #[test] - fn a_403_is_recognised_in_both_shapes_the_client_can_produce() { - assert!(is_forbidden(&api_err(ApiErrorKind::Forbidden))); - assert!(is_forbidden(&ForgejoError::UnexpectedStatusCode( - StatusCode::FORBIDDEN - ))); - } - - /// The other direction, and the more expensive one to get wrong: this - /// predicate gates advice to **delete the core token and restart - /// hive-c0re**. A widened match would hand that advice out for a 502 - /// from a restarting forge or a client-side error that never reached - /// the network — destroying a working credential in response to a blip. - /// 401 is called out separately because it is the near miss: it *is* a - /// credential problem, but its remedy is [`classify_core_token_error`]'s - /// automatic re-mint, so classifying it as forbidden buries the cause - /// under a scope warning that does not apply. - #[test] - fn a_non_403_failure_is_not_reported_as_a_missing_admin_scope() { - for kind in [ - ApiErrorKind::Unauthorized, - ApiErrorKind::NotFound { errors: None }, - ApiErrorKind::ValidationFailed, - ApiErrorKind::Generic, - ] { - let e = api_err(kind); - assert!(!is_forbidden(&e), "{e} must not read as forbidden"); - } - for s in [ - StatusCode::UNAUTHORIZED, - StatusCode::NOT_FOUND, - StatusCode::BAD_GATEWAY, - StatusCode::INTERNAL_SERVER_ERROR, - ] { - assert!( - !is_forbidden(&ForgejoError::UnexpectedStatusCode(s)), - "status {s} must not read as forbidden" - ); - } - // A failure with no HTTP status at all — the request never got an - // answer, so nothing has been said about the token's scopes. - assert!(!is_forbidden(&ForgejoError::KeyNotAscii)); - assert!(!is_forbidden(&ForgejoError::HostRequired)); - } } diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index b6fa8a2e..c1806bef 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -71,22 +71,6 @@ pub fn forge_core_avatar_marker() -> PathBuf { forge_dir().join("core-avatar-set") } -/// `forge/email-aligned-` — marker: ``'s forge email aligned. -#[must_use] -pub fn forge_email_aligned_marker(name: &str) -> PathBuf { - forge_dir().join(format!("email-aligned-{name}")) -} - -/// `forge/repo-creation-disabled-` — marker: ``'s forge user -/// has had `max_repo_creation = 0` applied (blocks direct agent-initiated -/// repo creation). One-shot guard so the PATCH runs once per -/// agent (including agents provisioned before the change); delete to -/// re-apply. -#[must_use] -pub fn forge_repo_creation_disabled_marker(name: &str) -> PathBuf { - forge_dir().join(format!("repo-creation-disabled-{name}")) -} - /// `agent-identity/` — one subdir per agent holding the store identity this /// hive collected for it. Staging only: the files exist so `systemd-nspawn` /// has something to `--load-credential` from, and nothing on the host ever @@ -297,18 +281,6 @@ pub fn relocate_legacy_state() { for (old_rel, new) in &moves { move_if_legacy(&root.join(old_rel), new); } - // `forge-email-aligned-` markers: glob the flat root. - if let Ok(rd) = std::fs::read_dir(&root) { - for ent in rd.flatten() { - if let Some(name) = ent - .file_name() - .to_str() - .and_then(|s| s.strip_prefix("forge-email-aligned-")) - { - move_if_legacy(&ent.path(), &forge_email_aligned_marker(name)); - } - } - } } /// Rename `old` → `new` when `old` exists and `new` doesn't, creating diff --git a/swarm-controller/src/forge.rs b/swarm-controller/src/forge.rs index 3f1aec1a..d00dd7ff 100644 --- a/swarm-controller/src/forge.rs +++ b/swarm-controller/src/forge.rs @@ -397,8 +397,8 @@ impl Client { /// Then locks the account out of creating repos /// ([`Self::disable_repo_creation`]), whether it was just created or /// already existed. Nothing else does that for a swarm-created agent, so - /// a failure fails the node. Does not align the account's email the way - /// `hive-c0re`'s `ensure_user_email` does — cosmetic, not a guard. + /// a failure fails the node. Leaves an existing account's email as it + /// finds it. pub async fn ensure_agent_user(&self, agent: &str) -> Result<()> { self.create_agent_user(agent).await?; self.disable_repo_creation(agent).await @@ -421,13 +421,8 @@ impl Client { /// account once and checks the one thing that *is* comparable: the /// email. A match against either [`agent_email`] (what /// `create_agent_user` itself sets) or [`legacy_agent_email`] (what - /// `hive-c0re::forge::users::ensure_user_exists` used before its own - /// `ensure_user_email` alignment pass runs) counts as this agent's - /// account — the legacy value has to be accepted too, not just the - /// aligned one, because `ensure_user_email` only runs on the forge-host - /// hive and only once it has a core token and has ticked at least once, - /// so a real, existing legacy agent can still carry the old email when - /// this node reads it mid-rollout. A mismatch against both means the + /// older hive-created agent accounts carry) counts as this agent's + /// account. A mismatch against both means the /// name collided with an account this node didn't create, so it fails /// the node rather than sending the PATCH (or silently skipping) to an /// account nobody verified is agent-controlled. @@ -1139,30 +1134,23 @@ fn initial_flake_nix() -> &'static str { /// [`Client::disable_repo_creation`] before it sends the account's PATCH. Same /// format string as `hive-c0re::forge::users::agent_email` — not called /// across the crate boundary (see this module's doc comment), but both -/// creators (and `hive-c0re`'s `ensure_user_email`, which aligns older -/// accounts to it) converge on this exact value, so it doubles as the +/// creators use this exact value, so it doubles as the /// marker that distinguishes an agent's own account from an unrelated one /// that happens to share its username. fn agent_email(agent: &str) -> String { format!("{agent}@hyperhive.local") } -/// The pre-alignment email `hive-c0re::forge::users::ensure_user_exists` -/// gave an agent account before that crate's own `ensure_user_email` -/// existed — see that function's doc comment ("Existing agents were -/// created with `{name}@hive.local`; this corrects that"). Accepted by -/// [`Client::disable_repo_creation`]'s guard alongside [`agent_email`] -/// because `ensure_user_email`'s alignment PATCH only runs on the -/// forge-host hive, and only after it has a core token and has ticked at -/// least once, so a real legacy agent can still carry this value when this -/// node reads it. +/// The email older hive-created agent accounts carry, and keep: nothing +/// rewrites it to [`agent_email`]. Accepted by +/// [`Client::disable_repo_creation`]'s guard alongside [`agent_email`] so +/// such an account is still recognised as the agent's own. fn legacy_agent_email(agent: &str) -> String { format!("{agent}@hive.local") } /// The `admin_edit_user` body that sets `max_repo_creation = 0` on `agent` -/// and nothing else. Same shape as `hive-c0re::forge::users`' -/// `sparse_edit_user_option`: `login_name` + `source_id = 0` (local auth, +/// and nothing else. `login_name` + `source_id = 0` (local auth, /// which is what [`Client::create_agent_user`] creates) ride along because /// Forgejo resets `use_custom_avatar` on an admin edit that omits /// `login_name`; every other field is left unset so nothing else is touched. @@ -1548,9 +1536,9 @@ mod tests { } /// A refused lockdown fails the node with Forgejo's own message, rather - /// than warning and reporting the account as provisioned — that is - /// `hive-c0re`'s `ensure_repo_creation_disabled` shape, which leaves an - /// agent able to create and self-merge with nothing marked failed. + /// than warning and reporting the account as provisioned, which would + /// leave an agent able to create and self-merge with nothing marked + /// failed. #[tokio::test] async fn a_refused_lockdown_fails_the_node() { let (client, _seen) = stub_forge( @@ -1598,10 +1586,9 @@ mod tests { ); } - /// A legacy account `hive-c0re::forge::users::ensure_user_exists` (or - /// its `ensure_user_email` alignment pass) created carries the same - /// `{agent}@hyperhive.local` marker, plus the `login_name`/`source_id` - /// fields its own `sparse_edit_user_option` sets — this node must still + /// A legacy hive-created account can carry the same + /// `{agent}@hyperhive.local` marker, plus `login_name`/`source_id` from + /// a hive admin edit — this node must still /// lock such an account down, not just ones it created itself. #[tokio::test] async fn a_legacy_hive_created_agent_account_still_gets_locked_down() { @@ -1621,14 +1608,9 @@ mod tests { assert_eq!(body["max_repo_creation"], 0); } - /// argus's round-2 🟡: `hive-c0re`'s `ensure_user_email` alignment pass - /// (the thing that rewrites a legacy `{name}@hive.local` account to - /// `{name}@hyperhive.local`) only runs on the forge-host hive, and only - /// once it has a core token and has ticked — so a real, pre-existing - /// agent can still be sitting on the pre-alignment `@hive.local` email - /// when this node reads it. The guard must accept that value too, not - /// just the aligned one, or a real agent would fail with "not this - /// agent's" during the rollout window. + /// A real, pre-existing agent account can carry the legacy + /// `@hive.local` email, and nothing rewrites it. The guard must accept + /// that value too, or such an agent would fail with "not this agent's". #[tokio::test] async fn a_pre_alignment_legacy_email_still_gets_locked_down() { let (client, seen) = stub_forge(