add meta read access and remote for agents on forge

This commit is contained in:
damocles 2026-05-22 12:43:50 +02:00 committed by Mara
parent 4bc5237bea
commit bbe4cdb872
3 changed files with 109 additions and 7 deletions

View file

@ -56,14 +56,23 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
tracing::warn!(agent = %approval.agent, error = ?e, "forge: push_config after apply failed"); tracing::warn!(agent = %approval.agent, error = ?e, "forge: push_config after apply failed");
} }
if is_first_spawn && result.is_ok() { if is_first_spawn && result.is_ok() {
// First-spawn bookkeeping: create the per-agent forge user // First-spawn bookkeeping: create the per-agent forge user,
// and mirror the applied repo into agent-configs/<n>. // mirror the applied repo into agent-configs/<n>, and grant
// read access to core/meta.
if let Err(e) = crate::forge::ensure_user_for(&approval.agent).await { if let Err(e) = crate::forge::ensure_user_for(&approval.agent).await {
tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_user after first spawn failed"); tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_user after first spawn failed");
} }
if let Err(e) = crate::forge::ensure_config_repo(&approval.agent).await { if let Err(e) = crate::forge::ensure_config_repo(&approval.agent).await {
tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_config_repo after first spawn failed"); tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_config_repo after first spawn failed");
} }
if let Some(core_token) = crate::forge::core_token() {
if let Err(e) = crate::forge::meta_read_access(&approval.agent, &core_token).await {
tracing::warn!(agent = %approval.agent, error = ?e, "forge: meta_read_access after first spawn failed");
}
}
if let Err(e) = crate::forge::ensure_meta_remote(&approval.agent).await {
tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_meta_remote after first spawn failed");
}
// New container row appeared — rescan so the dashboard // New container row appeared — rescan so the dashboard
// reflects the post-spawn state without a manual refetch. // reflects the post-spawn state without a manual refetch.
coord.rescan_containers_and_emit().await; coord.rescan_containers_and_emit().await;
@ -81,6 +90,12 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
Ok(()) Ok(())
} }
.await; .await;
// Wire the meta remote now that the proposed repo exists.
if result.is_ok() {
if let Err(e) = crate::forge::ensure_meta_remote(&approval.agent).await {
tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_meta_remote after init_config failed");
}
}
finish_approval(&coord, &approval, result, None, false) finish_approval(&coord, &approval, result, None, false)
} }
ApprovalKind::UpdateMetaInputs => { ApprovalKind::UpdateMetaInputs => {
@ -130,6 +145,14 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
if let Err(e) = crate::forge::push_config(&agent_bg).await { if let Err(e) = crate::forge::push_config(&agent_bg).await {
tracing::warn!(agent = %agent_bg, error = ?e, "forge: push_config after spawn failed"); tracing::warn!(agent = %agent_bg, error = ?e, "forge: push_config after spawn failed");
} }
if let Some(core_token) = crate::forge::core_token() {
if let Err(e) = crate::forge::meta_read_access(&agent_bg, &core_token).await {
tracing::warn!(agent = %agent_bg, error = ?e, "forge: meta_read_access after spawn failed");
}
}
if let Err(e) = crate::forge::ensure_meta_remote(&agent_bg).await {
tracing::warn!(agent = %agent_bg, error = ?e, "forge: ensure_meta_remote after spawn failed");
}
} }
if let Err(e) = finish_approval(&coord_bg, &approval_bg, result, None, false) { if let Err(e) = finish_approval(&coord_bg, &approval_bg, result, None, false) {
tracing::warn!(agent = %agent_bg, error = ?e, "spawn approval failed"); tracing::warn!(agent = %agent_bg, error = ?e, "spawn approval failed");

View file

@ -8,10 +8,10 @@
//! //!
//! It also mirrors each agent's hive-c0re-owned *applied* config repo //! It also mirrors each agent's hive-c0re-owned *applied* config repo
//! into the private `agent-configs` org (`push_config`), so every //! into the private `agent-configs` org (`push_config`), so every
//! deploy / approval tag core plants is visible on the forge. Core is //! deploy / approval tag core plants is visible on the forge. Each
//! the only principal with access — agents are not org members and //! agent is a read-only collaborator on `core/meta` (the meta flake)
//! the repos are private, so an agent can't reach a config repo //! so they can fetch their deployment context; the `agent-configs`
//! (not even its own) through the forge. //! repos remain core-only.
//! //!
//! No-op when `hive-forge` isn't enabled (detected via //! No-op when `hive-forge` isn't enabled (detected via
//! `nixos-container list`), so operators who don't run the bundled //! `nixos-container list`), so operators who don't run the bundled
@ -381,6 +381,75 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> {
ensure_org_repo(CONFIG_ORG, name, &token).await ensure_org_repo(CONFIG_ORG, name, &token).await
} }
/// 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.
pub async fn meta_read_access(name: &str, core_token: &str) -> Result<()> {
let url = format!("{FORGE_HTTP}/api/v1/repos/core/meta/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 core/meta/collaborators")?;
let code = String::from_utf8_lossy(&out.stdout).trim().to_owned();
match code.as_str() {
"204" => {
tracing::info!(%name, "forge: granted meta read access");
Ok(())
}
other => anyhow::bail!(
"PUT core/meta/collaborators/{name} returned HTTP {other}"
),
}
}
/// Add `http://localhost:3000/core/meta.git` as the `meta` remote in
/// the agent's proposed config repo so the agent (and the manager) can
/// fetch the meta flake from the forge. Idempotent: no-op when the
/// remote already points at the right URL, or when the proposed repo
/// does not exist yet. No-op when the forge is not running.
pub async fn ensure_meta_remote(name: &str) -> Result<()> {
if !is_present().await {
return Ok(());
}
let proposed_dir = Coordinator::agent_proposed_dir(name);
if !proposed_dir.join(".git").exists() {
return Ok(());
}
let want = format!("{FORGE_HTTP}/core/meta.git");
let existing = crate::lifecycle::git_command()
.current_dir(&proposed_dir)
.args(["remote", "get-url", "meta"])
.output()
.await
.context("git remote get-url meta")?;
if existing.status.success() {
let current = String::from_utf8_lossy(&existing.stdout).trim().to_owned();
if current == want {
return Ok(());
}
crate::lifecycle::git(&proposed_dir, &["remote", "set-url", "meta", &want]).await
} else {
crate::lifecycle::git(&proposed_dir, &["remote", "add", "meta", &want]).await
}
}
/// Mirror agent `name`'s applied config repo — `main` plus every tag /// Mirror agent `name`'s applied config repo — `main` plus every tag
/// (`proposal` / `approved` / `building` / `deployed` / `failed` / /// (`proposal` / `approved` / `building` / `deployed` / `failed` /
/// `denied`) — to `agent-configs/<name>` on the local forge. /// `denied`) — to `agent-configs/<name>` on the local forge.
@ -531,5 +600,15 @@ pub async fn ensure_all() {
if let Err(e) = push_config(&name).await { if let Err(e) = push_config(&name).await {
tracing::warn!(%name, error = ?e, "forge: push_config failed"); tracing::warn!(%name, error = ?e, "forge: push_config failed");
} }
// Grant read-only access to core/meta and wire the `meta` remote
// into the proposed repo so agents can fetch their deployment context.
if let Some(token) = core_token.as_deref() {
if let Err(e) = meta_read_access(&name, token).await {
tracing::warn!(%name, error = ?e, "forge: ensure_meta_read_access failed");
}
}
if let Err(e) = ensure_meta_remote(&name).await {
tracing::warn!(%name, error = ?e, "forge: ensure_meta_remote failed");
}
} }
} }

View file

@ -579,7 +579,7 @@ pub fn git_command() -> Command {
Command::new(exe) Command::new(exe)
} }
async fn git(dir: &Path, args: &[&str]) -> Result<()> { pub async fn git(dir: &Path, args: &[&str]) -> Result<()> {
let out = git_command() let out = git_command()
.current_dir(dir) .current_dir(dir)
.args(args) .args(args)