//! Hive-wide knowledge repository management. //! //! `internal/knowledge` on the forge is cloned to //! [`LOCAL_DIR`] and bind-mounted read-only into every agent container //! at `/knowledge`. Agents read documents from it directly and //! contribute by forking the repo and opening PRs — they never write //! to the bind-mounted path inside the container. //! //! hive-c0re maintains the local clone. A Forgejo webhook notifies it //! on push to main so agents always see an up-to-date snapshot. The //! webhook is auto-created by [`ensure_webhook`] at startup. A //! periodic pull in `main.rs` provides a fallback cadence. use anyhow::{Context, Result}; pub const ORG: &str = "internal"; pub const REPO: &str = "knowledge"; /// Host-side path for the local clone. Also referenced from /// `lifecycle.rs` (bind-mount source) and the dashboard webhook handler. pub const LOCAL_DIR: &str = "/var/lib/hyperhive/knowledge"; /// In-container mount point for the knowledge repo. Bind-mounted /// read-only from [`LOCAL_DIR`] into every agent container. pub const CONTAINER_MOUNT: &str = "/knowledge"; /// Default README pushed to a freshly created `internal/knowledge` repo. /// Short explanation + empty ToC with an HTML comment instructing contributors /// to add entries when they create new files. const README_CONTENT: &str = "\ # knowledge Hive-wide reference documents: conventions, runbooks, and anything that \ every agent should know. ## How to contribute 1. Fork this repo into your own namespace on the forge. 2. Create a branch, add or update a document. 3. Open a pull request — the operator reviews and merges. 4. Every agent container updates automatically on merge. Do **not** push directly to `main` — agents have read-only access. ## Contents "; /// Clone `internal/knowledge` to [`LOCAL_DIR`] if it is not already a git /// repository. `core_token` authenticates the HTTPS clone so private repos /// work. Idempotent — skips if `LOCAL_DIR/.git` exists. /// /// When the upstream repo is empty (freshly created), seeds it with a /// README.md before returning so the local clone is always non-empty and /// agents see a useful starting document. /// /// Called once at hive-c0re startup after `forge::ensure_all`. pub async fn ensure_local_clone(core_token: &str) -> Result<()> { let git_dir = std::path::Path::new(LOCAL_DIR).join(".git"); if git_dir.exists() { tracing::debug!("knowledge: local clone already present at {LOCAL_DIR}"); return Ok(()); } std::fs::create_dir_all(LOCAL_DIR).context("create knowledge local dir")?; // Embed credentials in the URL — safe for localhost-only forge. let url = format!("http://core:{core_token}@localhost:3000/{ORG}/{REPO}.git"); let out = tokio::process::Command::new("git") .args(["clone", &url, LOCAL_DIR]) .output() .await .context("git clone knowledge repo")?; if !out.status.success() { let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); anyhow::bail!("git clone {ORG}/{REPO} failed: {stderr}"); } tracing::info!("knowledge: cloned {ORG}/{REPO} to {LOCAL_DIR}"); // If the repo is brand new (no commits), seed it with a README. let head_out = tokio::process::Command::new("git") .args(["-C", LOCAL_DIR, "rev-parse", "HEAD"]) .output() .await .context("git rev-parse HEAD")?; if !head_out.status.success() { seed_readme(core_token).await?; } Ok(()) } /// Write the initial README.md, commit, and push to `internal/knowledge`. /// Called only when the upstream repo is empty. async fn seed_readme(core_token: &str) -> Result<()> { let readme = std::path::Path::new(LOCAL_DIR).join("README.md"); std::fs::write(&readme, README_CONTENT).context("write README.md")?; // Set a minimal git identity for the seed commit. for (k, v) in [("user.email", "core@hive"), ("user.name", "hive-c0re")] { let out = tokio::process::Command::new("git") .args(["-C", LOCAL_DIR, "config", k, v]) .output() .await .with_context(|| format!("git config {k}"))?; if !out.status.success() { let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); anyhow::bail!("git config {k} failed: {stderr}"); } } for args in [ vec!["add", "README.md"], vec!["commit", "-m", "init: seed README"], ] { let out = tokio::process::Command::new("git") .args(["-C", LOCAL_DIR].iter().chain(args.iter())) .output() .await .with_context(|| format!("git {:?}", args))?; if !out.status.success() { let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); anyhow::bail!("git {:?} failed: {stderr}", args); } } let url = format!("http://core:{core_token}@localhost:3000/{ORG}/{REPO}.git"); let out = tokio::process::Command::new("git") .args(["-C", LOCAL_DIR, "push", &url, "HEAD:main"]) .output() .await .context("git push knowledge README")?; if out.status.success() { tracing::info!("knowledge: seeded README.md and pushed to {ORG}/{REPO}"); Ok(()) } else { let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); anyhow::bail!("git push {ORG}/{REPO} failed: {stderr}") } } /// Ensure a Forgejo push webhook for `internal/knowledge` exists and /// points at hive-c0re's `/webhook/knowledge` endpoint. Idempotent — /// lists existing hooks first and skips creation when one is already /// targeting the correct URL. `dashboard_port` is the TCP port /// hive-c0re's dashboard listens on (default 7000); the webhook URL /// is `http://127.0.0.1:/webhook/knowledge`. /// /// Called at startup alongside [`ensure_local_clone`]. No-op when the /// core token is absent (forge not yet provisioned). pub async fn ensure_webhook(core_token: &str, dashboard_port: u16) -> Result<()> { const FORGE_HTTP: &str = "http://localhost:3000"; let target_url = format!("http://127.0.0.1:{dashboard_port}/webhook/knowledge"); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() .context("build reqwest client for webhook setup")?; // List existing hooks — skip creation if ours is already there. let list_url = format!("{FORGE_HTTP}/api/v1/repos/{ORG}/{REPO}/hooks"); let resp = client .get(&list_url) .header("Authorization", format!("token {core_token}")) .send() .await .with_context(|| format!("GET {list_url}"))?; if resp.status().is_success() { let hooks: Vec = resp.json().await.unwrap_or_default(); let already_exists = hooks.iter().any(|h| { h.get("config") .and_then(|c| c.get("url")) .and_then(|u| u.as_str()) == Some(&target_url) }); if already_exists { tracing::debug!(%target_url, "knowledge: push webhook already configured"); return Ok(()); } } // Create the webhook. let create_url = format!("{FORGE_HTTP}/api/v1/repos/{ORG}/{REPO}/hooks"); let body = serde_json::json!({ "type": "forgejo", "config": { "url": target_url, "content_type": "json" }, "events": ["push"], "active": true }); let resp = client .post(&create_url) .header("Authorization", format!("token {core_token}")) .json(&body) .send() .await .with_context(|| format!("POST {create_url}"))?; let status = resp.status(); if status.is_success() { tracing::info!(%target_url, "knowledge: push webhook created"); Ok(()) } else { let body = resp.text().await.unwrap_or_default(); anyhow::bail!("create webhook for {ORG}/{REPO} failed ({status}): {body}") } } /// Pull the latest changes in the local clone. Called from the webhook /// handler on every push to `internal/knowledge` main, and periodically /// from `main.rs` as a fallback. Uses `--ff-only` so a force-push to /// the knowledge repo never wedges the local copy silently. pub async fn pull() -> Result<()> { // Sanity: if the clone is missing (e.g. storage was wiped), refuse // to pull and let the caller decide whether to re-clone. let git_dir = std::path::Path::new(LOCAL_DIR).join(".git"); if !git_dir.exists() { anyhow::bail!("knowledge: {LOCAL_DIR}/.git not found — clone first"); } let out = tokio::process::Command::new("git") .args(["-C", LOCAL_DIR, "pull", "--ff-only"]) .output() .await .context("git pull knowledge")?; if out.status.success() { let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned(); tracing::info!(result = %stdout, "knowledge: pull succeeded"); Ok(()) } else { let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); anyhow::bail!("git pull {ORG}/{REPO} failed: {stderr}") } }