160 lines
6.5 KiB
Rust
160 lines
6.5 KiB
Rust
//! Hive-wide knowledge repository management.
|
|
//!
|
|
//! `internal/hive-knowledge` on the forge is cloned to
|
|
//! [`LOCAL_DIR`] and bind-mounted read-only into every agent container
|
|
//! at `/hive-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.
|
|
|
|
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
|
|
|
|
<!-- Add an entry here each time you create a new document:
|
|
- [Title](path/to/file.md) - one-line description
|
|
-->
|
|
";
|
|
|
|
/// 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}")
|
|
}
|
|
}
|
|
|
|
/// Pull the latest changes in the local clone. Called from the webhook
|
|
/// handler on every push to `internal/hive-knowledge` main. Uses
|
|
/// `--ff-only` so a force-push to the knowledge repo never wedges the
|
|
/// local copy silently; an error triggers a re-clone fallback.
|
|
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 hive-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}")
|
|
}
|
|
}
|