diff --git a/docs/forge.md b/docs/forge.md index ee15c5b1..c086fc05 100644 --- a/docs/forge.md +++ b/docs/forge.md @@ -8,6 +8,37 @@ handling live in [`docs/gateway.md`](gateway.md); this file owns the per-agent integration story and the notification pump that wakes each agent on relevant activity. +## Token scopes + +Two scope sets live in `hive-c0re::forge`: + +**`TOKEN_SCOPES`** (per-agent tokens): + +| Scope | Why | +|-------|-----| +| `write:repository` | Create, clone, push, delete repos; merge PRs. | +| `write:issue` | Open / comment / review issues **and** pull requests (Forgejo namespaces PR conversation under issues). | +| `write:user` | Edit own profile, create repos under own user. | +| `write:organization` | Create + manage orgs (lets agents share a forge namespace). | +| `read:user` | Token-owner endpoint used for self-identification at harness startup. | +| `write:misc` | Hooks, attachments, the rest of the long tail. | +| `read:notification` | Poll `GET /notifications` for unread events. | +| `write:notification` | Mark notifications read via `PATCH /notifications/threads/{id}`. | + +**`CORE_TOKEN_SCOPES`** (hive-c0re's own `core` user): everything in +`TOKEN_SCOPES` plus `read:admin` and `write:admin`. Site-admin +membership alone isn't sufficient — Forgejo's token scope gate runs +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 +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. + +--- + ## Per-agent forge accounts Each agent gets its own Forgejo user + access token, provisioned at diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 01cf713d..fd8c69ad 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -1,21 +1,6 @@ -//! Optional Forgejo wiring. When the `hive-forge` nixos-container is -//! present and running, hive-c0re ensures every agent (and the -//! manager) has a corresponding forgejo user with an API token -//! written to `/forge-token` — visible inside the -//! container as `/state/forge-token`. Idempotent: skips creation -//! when the user already exists, skips token issuance when the file -//! is already there. -//! -//! It also mirrors each agent's hive-c0re-owned *applied* config repo -//! into the private `agent-configs` org (`push_config`), so every -//! deploy / approval tag core plants is visible on the forge. Each -//! agent is a read-only collaborator on `core/meta` (the meta flake) -//! so they can fetch their deployment context; the `agent-configs` -//! repos remain core-only. -//! -//! No-op when `hive-forge` isn't enabled (detected via -//! `nixos-container list`), so operators who don't run the bundled -//! forge pay nothing. +//! Optional Forgejo wiring — per-agent user + token provisioning, +//! config-repo mirroring, meta read-access grants. No-op when +//! `hive-forge` isn't running. Full design: `docs/forge.md`. use std::path::{Path, PathBuf}; @@ -56,32 +41,15 @@ const CONFIG_ORG: &str = "agent-configs"; /// Forgejo orgs hive-c0re ensures on startup. The meta repo lives at /// `core/meta` (the `core` user's own namespace — no org needed). const SEEDED_ORGS: &[&str] = &[CONFIG_ORG]; -/// Forgejo scopes the agent's token gets. Broad-but-not-admin: every -/// repo / PR / issue thing an agent needs day-to-day, no admin -/// surface. -/// - `write:repository` — create, clone, push, delete repos in the -/// user's own namespace; merge PRs. -/// - `write:issue` — open / comment / review issues *and* pull -/// requests (forgejo namespaces PR conversation under issues). -/// - `write:user` — edit own profile, create repos under own user. -/// - `write:organization` — create + manage orgs (lets agents share -/// a forge namespace). -/// - `read:user` — token-owner endpoint clients call to introspect. -/// - `write:misc` — hooks, attachments, the rest of the long tail. -/// - `read:notification` — required by `forge_notify` to poll -/// `GET /notifications` for unread PR/review events. -/// - `write:notification` — required by `forge_notify` to mark -/// notifications as read via `PATCH /notifications/threads/{id}`. +/// Per-agent token scopes (broad-but-not-admin). See +/// `docs/forge.md::Token scopes` for the per-scope rationale. const TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc"; -/// Scopes for the bootstrap `core` token used by hive-c0re itself. -/// Adds `read:admin,write:admin` on top of `TOKEN_SCOPES` so the host -/// daemon can drive `/api/v1/admin/*` endpoints (PATCH user email on -/// agent provision, future webhook + org admin work). Site-admin -/// membership alone isn't enough — the token's own scope gate runs -/// before the user-permission check, so `403 Forbidden` comes back -/// for any `/admin/users/*` call from a non-admin-scoped token -/// even if the bearer is an admin user. +/// 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. +/// See `docs/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"; /// Token file inside the agent's bind-mounted state dir (visible as @@ -271,10 +239,8 @@ async fn ensure_user_email(name: &str) { tracing::debug!(%name, %email, "forge: user email aligned"); } Ok(status) if status == reqwest::StatusCode::FORBIDDEN => { - // Almost certainly an existing-deployment migration case: - // an older core token may have been minted without admin - // scope, so /admin/users/* now returns 403 even though - // `core` is a site admin. Tell the operator how to fix it. + // Core token missing admin scope — see + // `docs/forge.md::Token scopes` migration note. tracing::warn!( %name, %email, %status, "forge: PATCH user email forbidden — core token likely missing admin scope. \