From 858475549a60c5fcb02d40f329829857f6882fa4 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 16 Jul 2026 10:48:41 +0200 Subject: [PATCH] feat(#2415): hive-priv RegisterCiRunner primitive + c0re client Part A of moving hive-ci runner registration off the boot-critical path. Adds a root-side hive-priv op that writes the runner registration token to the host env-file /run/hive-ci/runner-token (in-place, preserving the inode the container bind-mounts) and restarts the in-container gitea-runner-hive unit. The forge admin token stays in hive-c0re; only the registration token reaches the host env-file the container mounts read-only. The c0re-side caller (ensure_ci_runner_registered) + the nix boot-path change land next on this branch. --- hive-c0re/src/priv_client.rs | 12 ++++++++++ hive-priv-sock/src/lib.rs | 13 +++++++++++ hive-priv/src/main.rs | 43 ++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index 3bd202ca..8dcfb441 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -322,6 +322,18 @@ pub async fn restart_matrix_daemon(agent_name: &str) -> Result<()> { .await?) } +/// Register the hive-ci Forgejo Actions runner: hand the freshly-minted +/// registration token to hive-priv, which writes it to the host-side +/// `/run/hive-ci/runner-token` env-file and restarts the in-container runner. +/// The forge admin token stays in hive-c0re; only the registration token +/// crosses to the (host-path) env-file the container bind-mounts read-only. +pub async fn register_ci_runner(token: &str) -> Result<()> { + ok(call(&PrivRequest::RegisterCiRunner { + token: token.to_owned(), + }) + .await?) +} + /// Restart a hive infrastructure container on the host (thin wrapper over /// [`control_infra_container`] with `action = Restart`). hive-priv /// re-validates `container` against its root-side allowlist; callers must diff --git a/hive-priv-sock/src/lib.rs b/hive-priv-sock/src/lib.rs index 4bd16e5d..9067575e 100644 --- a/hive-priv-sock/src/lib.rs +++ b/hive-priv-sock/src/lib.rs @@ -489,6 +489,19 @@ pub enum PrivRequest { agent_name: String, }, + /// Register the hive-ci Forgejo Actions runner: write the registration + /// token to the host-side `/run/hive-ci/runner-token` env-file (root-owned, + /// bind-mounted read-only into the container) as `TOKEN=`, then + /// `systemctl --machine=hive-ci restart gitea-runner-hive.service` so the + /// runner picks up the credential. hive-c0re holds the forge admin token + /// and mints the registration token; only the registration token is written + /// here, and only to a host path — the admin token never enters the + /// container. The token is validated single-line + non-empty root-side. + RegisterCiRunner { + /// Forge runner registration token minted by hive-c0re. + token: String, + }, + /// Start / stop / restart a hive infrastructure container on the host /// via `systemctl container@.service`. The /// [`InfraContainer`] enum is the allowlist — serde rejects unknown / diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 3ea8f0d8..8728c93d 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -331,6 +331,8 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String, restart_matrix_daemon(agent_name).await } + PrivRequest::RegisterCiRunner { ref token } => register_ci_runner(token).await, + PrivRequest::ControlInfraContainer { container, action } => { control_infra_container(container, action).await } @@ -596,6 +598,47 @@ async fn restart_matrix_daemon(agent_name: &str) -> Result<(String, String)> { )) } +/// `RegisterCiRunner` — write the runner registration token to the host-side +/// `/run/hive-ci/runner-token` env-file, then restart the in-container runner +/// so it re-registers. The forge admin token never enters the container; only +/// the registration token c0re passes here is written, and it lands on a host +/// path bind-mounted read-only into hive-ci. +async fn register_ci_runner(token: &str) -> Result<(String, String)> { + use std::os::unix::fs::PermissionsExt as _; + // Reject anything that could corrupt the `KEY=VALUE` env-file or smuggle a + // second line — a forge registration token is an opaque single-line string. + if token.is_empty() || token.contains(['\n', '\r', '\0']) { + bail!("ci runner registration token empty or contains control characters"); + } + let token_path = "/run/hive-ci/runner-token"; + // In-place truncate+write of the existing inode (mirrors the prefetch's + // `echo > $FILE`), NOT a temp+rename: nspawn pins this file's inode into + // hive-ci at container start, so a rename would leave the running runner + // reading the old content. Format + perms match the tmpfiles seed and the + // prefetch: `TOKEN=`, mode 0600, root-owned. + std::fs::write(token_path, format!("TOKEN={token}\n")) + .with_context(|| format!("write {token_path}"))?; + std::fs::set_permissions(token_path, std::fs::Permissions::from_mode(0o600)) + .with_context(|| format!("chmod {token_path}"))?; + // Restart the in-container runner so it reads the new token and registers. + let out = Command::new("systemctl") + .args(["--machine=hive-ci", "restart", "gitea-runner-hive.service"]) + .output() + .await + .context("systemctl restart gitea-runner-hive.service in hive-ci")?; + if !out.status.success() { + bail!( + "systemctl restart gitea-runner-hive.service in hive-ci exited {}: {}", + out.status, + String::from_utf8_lossy(&out.stderr).trim() + ); + } + Ok(( + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + )) +} + /// `ControlInfraContainer` — start/stop/restart a hive infrastructure /// container via `systemctl container@.service`. The /// [`InfraContainer`] enum is the allowlist: serde already rejected any