From 6044cd4d79caa9aecd242fdcabfd7391e6fb2976 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 1 Jun 2026 12:26:28 +0200 Subject: [PATCH] feat(#4): seed org-shared/docs repo, grant every agent read access --- hive-c0re/src/forge.rs | 77 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index fd8c69ad..4e1e23f7 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -1,6 +1,8 @@ //! 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`. +//! 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. +//! No-op when `hive-forge` isn't running. Full design: `docs/forge.md`. use std::path::{Path, PathBuf}; @@ -38,9 +40,18 @@ const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-a /// applied repos stay hive-c0re-owned on disk; this org is just a /// mirror target core pushes to. const CONFIG_ORG: &str = "agent-configs"; +/// Forgejo org hosting the operator-curated shared docs/skills repo +/// that every agent gets read-only access to. Agents use it as a +/// common reference without the operator having to bake content into +/// the system prompt or rely on `/shared`. Only the manager + operator +/// (i.e. `core` user) can push. +const SHARED_ORG: &str = "org-shared"; +/// The shared docs repo inside `SHARED_ORG`. Cloneable by every agent +/// at `{FORGE_HTTP}/org-shared/docs.git`. +const SHARED_DOCS_REPO: &str = "docs"; /// 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]; +const SEEDED_ORGS: &[&str] = &[CONFIG_ORG, SHARED_ORG]; /// 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"; @@ -529,6 +540,54 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> { ensure_org_repo(CONFIG_ORG, name, &token).await } +/// Ensure the `org-shared/docs` repo exists. Called once at startup +/// after `ensure_org(SHARED_ORG)`. Idempotent — `ensure_org_repo` +/// treats 409 as success. +pub async fn ensure_shared_docs_repo(core_token: &str) -> Result<()> { + ensure_org_repo(SHARED_ORG, SHARED_DOCS_REPO, core_token).await +} + +/// Grant agent `name` read-only collaborator access to `org-shared/docs`. +/// Idempotent: HTTP 204 (already a collaborator) is treated as success. +/// Mirrors `meta_read_access` so agents can clone the shared docs repo +/// without authentication hassle. +pub async fn shared_docs_access(name: &str, core_token: &str) -> Result<()> { + let url = format!( + "{FORGE_HTTP}/api/v1/repos/{SHARED_ORG}/{SHARED_DOCS_REPO}/collaborators/{name}" + ); + let body = r#"{"permission":"read"}"#; + let out = Command::new("curl") + .args([ + "-sS", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "-X", + "PUT", + "-H", + "Content-Type: application/json", + "-H", + &format!("Authorization: token {core_token}"), + "-d", + body, + &url, + ]) + .output() + .await + .context("invoke curl PUT org-shared/docs/collaborators")?; + let code = String::from_utf8_lossy(&out.stdout).trim().to_owned(); + match code.as_str() { + "204" => { + tracing::info!(%name, "forge: granted shared-docs read access"); + Ok(()) + } + other => anyhow::bail!( + "PUT {SHARED_ORG}/{SHARED_DOCS_REPO}/collaborators/{name} returned HTTP {other}" + ), + } +} + /// Grant agent `name` read-only collaborator access to `core/meta` on /// the forge so the agent can clone/fetch the meta flake. Idempotent: /// HTTP 204 (already a collaborator) is treated as success. @@ -701,6 +760,13 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) { if let Err(e) = ensure_meta_remote(name).await { tracing::warn!(%name, error = ?e, "forge: ensure_meta_remote failed"); } + // Grant read-only access to org-shared/docs so the agent can clone + // the operator-curated shared skills/runbook repo. Best-effort. + if let Some(token) = core_token + && let Err(e) = shared_docs_access(name, token).await + { + tracing::warn!(%name, error = ?e, "forge: shared_docs_access failed"); + } } /// Sweep every existing container (manager + sub-agents) and ensure @@ -734,6 +800,11 @@ pub async fn ensure_all() { if let Err(e) = ensure_repo("meta", token).await { tracing::warn!(error = ?e, "forge: ensure_repo core/meta failed"); } + // Seed the shared docs repo. org-shared is already in + // SEEDED_ORGS above so the org exists; ensure the repo itself. + if let Err(e) = ensure_shared_docs_repo(token).await { + tracing::warn!(error = ?e, "forge: ensure_shared_docs_repo failed"); + } if let Err(e) = ensure_core_avatar(token).await { tracing::warn!(error = ?e, "forge: ensure_core_avatar failed"); }