feat(#4): seed org-shared/docs repo, grant every agent read access

This commit is contained in:
damocles 2026-06-01 12:26:28 +02:00 committed by mara
commit 6044cd4d79

View file

@ -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");
}