diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index bd8fd07e..ec21b0d5 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -178,21 +178,15 @@ impl Coordinator { /// Drop a system message into the given agent's inbox. Wakes the /// turn loop with a "you were just (re)started" hint — operator /// caused the transition, agent picks up where it left off - /// (notes are in the bind-mounted state dir, last turn is in - /// --continue's session). Best-effort; broker errors are logged - /// but don't propagate. + /// (notes are in /state/, last turn is in --continue's session). + /// Best-effort; broker errors are logged but don't propagate. pub fn kick_agent(&self, name: &str, reason: &str) { - // Sub-agents bind their state at /agents//state. The - // manager has both /state (legacy mount) and /agents - // bind-mounted, so /agents//state resolves there too — - // use that uniformly so the wake message has one canonical - // path that works everywhere. let body = format!( "{reason}\n\nYou were just (re)started by the operator. \ - If you were mid-task, check `/agents/{name}/state/` for \ - your notes and pick up where you left off. claude's \ - `--continue` session is intact, so prior context is \ - still in your window." + If you were mid-task, check `/state/` for your notes \ + and pick up where you left off. claude's `--continue` \ + session is intact, so prior context is still in your \ + window." ); if let Err(e) = self.broker.send(&hive_sh4re::Message { from: hive_sh4re::SYSTEM_SENDER.to_owned(), diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 7018f327..228fd6ef 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -18,18 +18,7 @@ use tokio::process::Command; use crate::coordinator::Coordinator; const FORGE_CONTAINER: &str = "hive-forge"; -const FORGE_HTTP: &str = "http://localhost:3000"; const TOKEN_NAME_PREFIX: &str = "hyperhive"; -/// Where the host-side `core` admin token lives. Used by hive-c0re -/// itself to push the meta repo + drive admin API calls (org -/// creation, future webhook setup, etc.). Root-only. -const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token"; -/// Forgejo orgs hive-c0re ensures on startup. The meta repo lives -/// at `core/meta` (the `core` user's own namespace — no org needed); -/// `agents` holds per-agent applied config repos so they're -/// visible/grouped together and access can be granted org-wide -/// (RO membership for the future shared docs/skills repo). -const SEEDED_ORGS: &[&str] = &["agents"]; /// 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. @@ -122,23 +111,18 @@ fn extract_token(output: &str) -> Option { /// Ensure a forgejo user named `name` exists. Idempotent: forgejo /// returns a "user already exists" error which we treat as success. -/// `admin` adds `--admin` (site admin) — used for the bootstrap -/// `core` user that drives the API. -async fn ensure_user_exists(name: &str, admin: bool) -> Result<()> { - let mut args = vec![ +async fn ensure_user_exists(name: &str) -> Result<()> { + let result = forge_admin(&[ "user", "create", "--username", name, "--email", - ]; - let email = format!("{name}@hive.local"); - args.push(&email); - args.extend(["--random-password", "--must-change-password=false"]); - if admin { - args.push("--admin"); - } - let result = forge_admin(&args).await; + &format!("{name}@hive.local"), + "--random-password", + "--must-change-password=false", + ]) + .await; match result { Ok(_) => { tracing::info!(%name, "forge: created user"); @@ -207,183 +191,18 @@ pub async fn ensure_user_for(name: &str) -> Result<()> { if path.exists() { return Ok(()); } - ensure_user_exists(name, false).await?; + ensure_user_exists(name).await?; mint_and_persist_token(name, &path).await } -/// Ensure the bootstrap `core` admin user + a token at -/// `CORE_TOKEN_PATH`. The token is what hive-c0re uses for forgejo -/// API calls (org creation now, meta-repo push later). Returns the -/// token. Idempotent: skips creation when user exists, skips token -/// when the file is present. -async fn ensure_core_user_and_token() -> Result { - let path = std::path::Path::new(CORE_TOKEN_PATH); - if let Ok(existing) = std::fs::read_to_string(path) { - let trimmed = existing.trim().to_owned(); - if !trimmed.is_empty() { - return Ok(trimmed); - } - } - ensure_user_exists("core", true).await?; - mint_and_persist_token("core", path).await?; - let raw = std::fs::read_to_string(path) - .with_context(|| format!("read {CORE_TOKEN_PATH} after mint"))?; - Ok(raw.trim().to_owned()) -} - -/// POST `/api/v1/user/repos` to create a repo in the authenticated -/// user's own namespace. `token` belongs to the user we want the -/// repo owned by (we use `core`'s token for `core/meta`). Idempotent: -/// HTTP 409 ("repository already exists") is treated as success. -pub async fn ensure_repo(name: &str, token: &str) -> Result<()> { - let body = format!(r#"{{"name":"{name}","auto_init":false,"private":true,"default_branch":"main"}}"#); - let url = format!("{FORGE_HTTP}/api/v1/user/repos"); - let out = Command::new("curl") - .args([ - "-sS", - "-o", - "/dev/null", - "-w", - "%{http_code}", - "-X", - "POST", - "-H", - "Content-Type: application/json", - "-H", - &format!("Authorization: token {token}"), - "-d", - &body, - &url, - ]) - .output() - .await - .context("invoke curl POST /api/v1/user/repos")?; - let code = String::from_utf8_lossy(&out.stdout).trim().to_owned(); - match code.as_str() { - "201" => { - tracing::info!(%name, "forge: created repo"); - Ok(()) - } - "409" | "422" => { - tracing::debug!(%name, "forge: repo already exists"); - Ok(()) - } - other => anyhow::bail!( - "POST /api/v1/user/repos name={name} returned HTTP {other}" - ), - } -} - -/// Read the persisted core token, or None when the forge isn't -/// seeded yet. Cheap — just a file read. -pub fn core_token() -> Option { - std::fs::read_to_string(CORE_TOKEN_PATH) - .ok() - .map(|s| s.trim().to_owned()) - .filter(|s| !s.is_empty()) -} - -/// Push `dir` (the meta repo) to `core/meta` on the local forge. -/// Best-effort: returns Err which callers log + ignore. No-op when -/// the core token isn't present (forge not enabled). -pub async fn push_meta(dir: &Path) -> Result<()> { - let Some(token) = core_token() else { - return Ok(()); - }; - // Token-in-URL push. Forgejo accepts `oauth2:` or just - // any-username:; using `core` matches the owner so the - // remote name is self-describing. - let url = format!("http://core:{token}@localhost:3000/core/meta.git"); - let out = Command::new("git") - .current_dir(dir) - .args(["push", "--force", &url, "HEAD:main"]) - .output() - .await - .context("invoke git push core/meta")?; - if !out.status.success() { - anyhow::bail!( - "git push core/meta failed ({}): {}", - out.status, - String::from_utf8_lossy(&out.stderr).trim() - ); - } - tracing::info!("forge: pushed meta to core/meta"); - Ok(()) -} - -/// POST `/api/v1/orgs` to create an org named `name`. Idempotent: -/// HTTP 422 ("user already exists") is treated as success. -async fn ensure_org(name: &str, admin_token: &str) -> Result<()> { - let body = format!(r#"{{"username":"{name}"}}"#); - let url = format!("{FORGE_HTTP}/api/v1/orgs"); - let out = Command::new("curl") - .args([ - "-sS", - "-o", - "/dev/null", - "-w", - "%{http_code}", - "-X", - "POST", - "-H", - "Content-Type: application/json", - "-H", - &format!("Authorization: token {admin_token}"), - "-d", - &body, - &url, - ]) - .output() - .await - .context("invoke curl POST /api/v1/orgs")?; - let code = String::from_utf8_lossy(&out.stdout).trim().to_owned(); - match code.as_str() { - "201" => { - tracing::info!(%name, "forge: created org"); - Ok(()) - } - "422" | "409" => { - tracing::debug!(%name, "forge: org already exists"); - Ok(()) - } - other => anyhow::bail!( - "POST /api/v1/orgs name={name} returned HTTP {other}: {}", - String::from_utf8_lossy(&out.stderr).trim() - ), - } -} - /// Sweep every existing container (manager + sub-agents) and ensure -/// each has a forgejo user + token. Also seeds the `core` admin -/// user (hive-c0re's own identity for pushing the meta repo + driving -/// the API) and the `core` / `agents` orgs the system pushes into. -/// Called once at hive-c0re startup. Per-step failures are logged -/// but don't abort the sweep. +/// each has a forgejo user + token. Called once at hive-c0re +/// startup. Per-agent failures are logged but don't abort the sweep. pub async fn ensure_all() { if !is_present().await { tracing::debug!("forge: hive-forge container absent, skipping user sweep"); return; } - let core_token = match ensure_core_user_and_token().await { - Ok(t) => Some(t), - Err(e) => { - tracing::warn!(error = ?e, "forge: ensure_core_user_and_token failed"); - None - } - }; - if let Some(token) = core_token.as_deref() { - for org in SEEDED_ORGS { - if let Err(e) = ensure_org(org, token).await { - tracing::warn!(%org, error = ?e, "forge: ensure_org failed"); - } - } - // Meta repo lives at core/meta — pushed from git_commit in - // meta.rs on every deploy/lock-update. Make sure it exists - // before the first push hits a 404. - if let Err(e) = ensure_repo("meta", token).await { - tracing::warn!(error = ?e, "forge: ensure_repo core/meta failed"); - } - } let Ok(containers) = crate::lifecycle::list().await else { tracing::warn!("forge: nixos-container list failed; skipping user sweep"); return; diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 6774e669..8f5bb882 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -342,15 +342,7 @@ async fn git_commit(dir: &Path, message: &str) -> Result<()> { message, ], ) - .await?; - // Best-effort mirror to the bundled forge. No-op when the forge - // isn't seeded (no core token on disk); push failures log a warn - // but don't bubble up — a missing mirror shouldn't fail an - // otherwise successful deploy. - if let Err(e) = crate::forge::push_meta(dir).await { - tracing::warn!(error = ?e, "forge: meta push after commit failed (non-fatal)"); - } - Ok(()) + .await } async fn nix(dir: &Path, args: &[&str]) -> Result<()> {