Watch
0
0
Fork
You've already forked hyperhive
0

drop the hive's push of the legacy core/meta repo

Nothing reads core/meta: builds come from internal/meta. Remove push_meta, ensure_meta_remote, meta_read_access, ensure_repo, the per-agent forge sync that only served them, and the git commit path in meta.rs; remove_permission_files now just deletes the seed files.
This commit is contained in:
atlas 2026-10-25 16:35:46 +01:00 • committed by mara
commit b6b568e97e
3 changed files with 81 additions and 518 deletions

View file

@ -1,6 +1,5 @@
//! Optional Forgejo wiring — the legacy `core/meta` repo (the hive's old
//! permission-seed repo, which `meta::remove_permission_files` still commits
//! to) and its read-access grants. Those writes, as `core`, run only beside a local `hive-forge`
//! Optional Forgejo wiring — the `core` admin user and token and its avatar.
//! Those writes, as `core`, run only beside a local `hive-forge`
//! container; reads of the forge go through [`reader_token`] on every hive.
//! The swarm-wide orgs, teams and repos, and each agent's read grant on
//! `internal/docs`, are swarm-controller's. Full design:
@ -15,10 +14,7 @@ mod users;
pub(crate) use internal_meta::internal_meta_source;
pub use reader::reader_token;
pub use reconcile::{fetch_forge_main, reconcile_config_apply, reconcile_config_status};
pub use repos::{
clone_config_into_proposed, ensure_meta_remote, ensure_repo, fetch_config_main_into_applied,
meta_read_access, push_meta,
};
pub use repos::{clone_config_into_proposed, fetch_config_main_into_applied};
pub use users::core_token;
use std::future::IntoFuture;
@ -74,13 +70,13 @@ pub(crate) fn validate_env() -> Result<()> {
Ok(())
}
/// Git remote for `repo` (e.g. `"core/meta"`) — **credential-free**.
/// Git remote for `repo` (e.g. `"hyperhive/hyperhive"`) — **credential-free**.
///
/// The token does not go here. A URL is a process argument, and `argv` is
/// world-readable through `/proc/<pid>/cmdline` for as long as the git child
/// lives, so a credentialed remote publishes the core admin token to every
/// local user on the host. Credentials travel in the environment instead, via
/// [`core_auth_header`] and [`crate::lifecycle::git_command_authed`] —
/// [`basic_auth_header`] and [`crate::lifecycle::git_command_authed`] —
/// `/proc/<pid>/environ` is owner-only.
pub(crate) fn forge_git_url(repo: &str) -> String {
git_url_with_base(forge_http_base(), repo)
@ -92,11 +88,6 @@ fn git_url_with_base(base: &str, repo: &str) -> String {
format!("{base}/{repo}.git")
}
/// The `http.extraHeader` value authenticating as the forge core user.
pub(crate) fn core_auth_header(token: &str) -> String {
basic_auth_header("core", token)
}
/// The `http.extraHeader` value carrying `token`. Forgejo takes the account
/// from the token, so `login` only labels the request.
///
@ -120,16 +111,6 @@ fn basic_auth_header(login: &str, token: &str) -> String {
/// still can't reach *another* agent's config. hive-c0re only reads from it.
pub(crate) const CONFIG_ORG: &str = "agent-configs";
/// Leak `s` to get a `&'static str` warning `kind` for the small, bounded
/// set of boot warnings in [`ensure_all`] keyed by a runtime name (at
/// most a handful per process). [`crate::warnings::set_boot_warning`]
/// requires a `'static` kind so distinct orgs/repos don't clobber each
/// other's warning entry; leaking a few short strings once per boot is
/// cheap and bounded, unlike a per-request or per-loop-iteration leak.
fn static_kind(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
/// Probe whether `hive-forge` exists as a nixos-container. Cheap —
/// `nixos-container list` is just a directory scan in /etc. Routed
/// through hive-priv: `nixos-container` needs root, and hive-c0re runs
@ -216,42 +197,7 @@ where
.map_err(|_| anyhow::anyhow!("{op}: no answer from the forge within {API_TIMEOUT:?}"))
}
/// Per-agent forge sync: grant read access to the legacy `core/meta`, and wire
/// the `meta` remote in its proposed repo. All operations are idempotent;
/// failures are logged as warnings but don't abort the caller.
///
/// `core_token` is `core_token()` — passed in so callers that already
/// fetched it don't re-read the file. Pass `None` to skip the
/// `meta_read_access` step (safe: the access grant is best-effort).
///
/// Called by both `ensure_all()` (startup sweep) and `rebuild_agent`
/// (per-rebuild) so the two paths stay equivalent.
/// Returns `true` if all steps succeeded, `false` if any step failed. The
/// caller can use the return value to aggregate per-agent failures into a
/// dashboard warning (see [`ensure_all`]); the rebuild path ignores it and
/// relies on the journal `warn!` lines alone (a rebuild is its own retry).
pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool {
// The agent's forge account, token and `agent-configs/<name>` repo are
// swarm-controller's; this hive creates none of them and pushes nothing
// to the repo.
let mut ok = true;
// Grant read-only access to the legacy core/meta and wire the `meta`
// remote into the proposed repo. Agents build from `internal/meta`, not
// from this repo.
if let Some(token) = core_token
&& let Err(e) = meta_read_access(name, token).await
{
tracing::warn!(%name, error = ?e, "forge: ensure_meta_read_access failed");
ok = false;
}
if let Err(e) = ensure_meta_remote(name).await {
tracing::warn!(%name, error = ?e, "forge: ensure_meta_remote failed");
ok = false;
}
ok
}
/// The `core_token.is_some()` half of [`ensure_all`]: the legacy meta repo, the
/// The `core_token.is_some()` half of [`ensure_all`]: the
/// core avatar, and removing this hive's own config-PR hook —
/// every step that needs an authenticated forge client. The swarm-wide
/// objects (orgs, the `operators` team, mirrors, `internal/docs`,
@ -260,17 +206,6 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool {
/// `ensure_all` under clippy's function-length limit; not meant to be called
/// from anywhere else.
async fn ensure_all_orgs_and_repos(token: &str) {
// The legacy meta repo lives at core/meta. The only push to it is
// `meta::remove_permission_files`' commit; make sure the repo exists
// before that push hits a 404.
if let Err(e) = ensure_repo("meta", token).await {
tracing::warn!(error = ?e, "forge: ensure_repo core/meta failed");
crate::warnings::set_boot_warning(
"forge_ensure_meta_repo",
"crit",
format!("forge: core/meta repo provisioning failed: {e}"),
);
}
if let Err(e) = ensure_core_avatar(token).await {
tracing::warn!(error = ?e, "forge: ensure_core_avatar failed");
crate::warnings::set_boot_warning(
@ -382,11 +317,10 @@ async fn wait_until_ready() -> bool {
}
/// Sweep the forge at startup: seed the `core` admin user (hive-c0re's own
/// identity for pushing the legacy meta repo + driving the API) and its token, the
/// `core/meta` repo and the local knowledge clone, then run [`sync_agent`]
/// over every existing container (manager + sub-agents). Agent accounts,
/// identity for driving the API, and the one nix's git reads as) and its token,
/// then its avatar. Agent accounts,
/// tokens and `agent-configs/<name>` repos are the swarm-controller's, as is
/// the `agent-configs` org; this sweep pushes nothing to them.
/// the `agent-configs` org; this sweep touches none of them.
/// Called once at hive-c0re startup. Per-step failures are logged
/// but don't abort the sweep.
pub async fn ensure_all() {
@ -413,37 +347,6 @@ pub async fn ensure_all() {
if let Some(token) = core_token.as_deref() {
ensure_all_orgs_and_repos(token).await;
}
let Ok(containers) = crate::lifecycle::list().await else {
tracing::warn!("forge: nixos-container list failed; skipping user sweep");
crate::warnings::set_boot_warning(
"forge_container_list",
"crit",
"forge: nixos-container list failed; per-agent forge sync skipped this boot",
);
return;
};
let mut sync_failed: Vec<String> = Vec::new();
for c in containers {
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
continue;
};
if !sync_agent(name, core_token.as_deref()).await {
sync_failed.push(name.to_owned());
}
}
if !sync_failed.is_empty() {
// Use static_kind to mint a `&'static str` key from the failed-agent
// list (bounded leak: one per hive-c0re boot, not per request).
let key = static_kind(format!("forge_sync_agent_{}", sync_failed.join("_")));
crate::warnings::set_boot_warning(
key,
"warn",
format!(
"forge: per-agent sync failed for: {} (see journal for per-step detail)",
sync_failed.join(", ")
),
);
}
}
#[cfg(test)]
@ -526,9 +429,9 @@ mod tests {
/// back to `core:<token>`, so the swap is genuinely equivalent auth and
/// not a silent downgrade to anonymous.
#[test]
fn core_auth_header_is_basic_core_token() {
fn basic_auth_header_is_basic_core_token() {
use base64::Engine as _;
let header = super::core_auth_header("s3cret");
let header = super::basic_auth_header("core", "s3cret");
let b64 = header
.strip_prefix("Authorization: Basic ")
.expect("basic auth header");

View file

@ -1,207 +1,10 @@
//! Repo + org plumbing on the local Forgejo: the meta + knowledge repos,
//! meta mirroring (`push_meta`), collaborator grants, pull-mirrors, and
//! branch-protection rules. The typed API-client constructor + org-name
//! constants live in the module root (`super`).
//! Repo plumbing on the local Forgejo: reseeding an agent's `applied` and
//! `proposed` repos from `agent-configs/<name>`. The typed API-client
//! constructor + org-name constants live in the module root (`super`).
use std::path::Path;
use std::sync::Mutex;
use anyhow::{Context, Result};
use forgejo_api::structs::{
AddCollaboratorOption, AddCollaboratorOptionPermission, CreateRepoOption, Repository,
};
use forgejo_api::{ApiErrorKind, ForgejoError};
use reqwest::StatusCode;
use crate::coordinator::Coordinator;
use super::{
CONFIG_ORG, api, bounded, core_auth_header, core_token, forge_git_url, forge_http_base,
is_present, reader_token,
};
/// Creation options for an empty repo defaulting to `main`.
fn repo_option(name: &str, private: bool) -> CreateRepoOption {
CreateRepoOption {
auto_init: Some(false),
default_branch: Some("main".to_owned()),
description: None,
gitignores: None,
issue_labels: None,
license: None,
name: name.to_owned(),
object_format_name: None,
private: Some(private),
readme: None,
template: None,
trust_model: None,
}
}
/// Whether a create-style call failed because the object already
/// exists. Forgejo signals this as HTTP 409 (conflict) or 422
/// (validation). The typed client surfaces those as
/// `ApiErrorKind::Other(409)` / `ValidationFailed` when the endpoint
/// spec lists the status, or as a bare `UnexpectedStatusCode`
/// otherwise — match all shapes defensively.
fn is_already_exists(e: &ForgejoError) -> bool {
match e {
ForgejoError::ApiError(api) => match api.error_kind() {
ApiErrorKind::ValidationFailed => true,
ApiErrorKind::Other(s) => *s == StatusCode::CONFLICT,
_ => false,
},
ForgejoError::UnexpectedStatusCode(s) => {
*s == StatusCode::CONFLICT || *s == StatusCode::UNPROCESSABLE_ENTITY
}
_ => false,
}
}
/// Fold a repo-creation result's "already exists" (409 / 422) into
/// success. `label` is `<owner>/<name>` — purely for log + error
/// context.
fn created_or_exists(res: Result<Repository, ForgejoError>, label: &str) -> Result<()> {
match res {
Ok(_) => {
tracing::info!(%label, "forge: created repo");
Ok(())
}
Err(e) if is_already_exists(&e) => {
tracing::debug!(%label, "forge: repo already exists");
Ok(())
}
Err(e) => Err(e).with_context(|| format!("create repo {label}")),
}
}
/// Create a repo in the token-owner's own namespace. `token` belongs
/// to the user we want the repo owned by (we use `core`'s token for
/// `core/meta`). Idempotent.
pub async fn ensure_repo(name: &str, token: &str) -> Result<()> {
let label = format!("core/{name}");
let res = bounded(
&format!("create repo {label}"),
api(token)?.create_current_user_repo(repo_option(name, true)),
)
.await?;
created_or_exists(res, &label)
}
/// Dashboard-warning guard for the meta non-fast-forward condition.
/// Held while the `core/meta` remote is ahead of our local mirror; cleared
/// automatically when the next push succeeds.
static META_NON_FF_GUARD: Mutex<Option<crate::warnings::WarningGuard>> = Mutex::new(None);
/// Push `dir` (the legacy hive meta repo) to `core/meta` on the local forge.
/// Its one caller is `meta::remove_permission_files`' commit.
/// Best-effort: returns Err which callers log + ignore. No-op when
/// the core token isn't present yet (forge container not provisioned).
pub async fn push_meta(dir: &Path) -> Result<()> {
let Some(token) = core_token() else {
return Ok(());
};
// Token-in-URL push. Forgejo accepts `oauth2:<token>` or just
// any-username:<token>; using `core` matches the owner so the
// remote name is self-describing.
//
// No --force: `meta.rs` uses regular `git commit` (append-only),
// so the push is always fast-forward in normal operation. Per
// operator directive, hive-c0re must not force-push anywhere. If
// the remote is somehow ahead (e.g. split-brain or manual push) we
// raise a dashboard warning and leave the remote intact rather than
// silently erasing its history.
let url = forge_git_url("core/meta");
let out = crate::lifecycle::run_with_deadline(
crate::lifecycle::git_command_authed(&core_auth_header(&token))
.current_dir(dir)
.args(["push", &url, "HEAD:main"]),
crate::lifecycle::FETCH_DEADLINE,
"git push core/meta",
)
.await?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("non-fast-forward") || stderr.contains("fetch first") {
let msg = "forge/core/meta push rejected (non-fast-forward): \
remote is ahead of local; leaving remote history intact";
tracing::warn!("{msg}");
// Raise a persistent hive warning; cleared on next successful push.
if let Ok(mut g) = META_NON_FF_GUARD.lock() {
*g = Some(crate::warnings::set_warning(
"forge-meta-non-ff",
"warn",
msg,
));
}
return Ok(());
}
anyhow::bail!(
"git push core/meta failed ({}): {}",
out.status,
stderr.trim()
);
}
// Successful push: clear any outstanding non-ff warning.
if let Ok(mut g) = META_NON_FF_GUARD.lock() {
*g = None;
}
tracing::info!("forge: pushed meta to core/meta");
Ok(())
}
/// Grant agent `name` read-only collaborator access to `core/meta` on
/// the forge so the agent can clone/fetch that legacy repo. Idempotent:
/// re-adding an existing collaborator succeeds (Forgejo answers 204
/// either way).
pub async fn meta_read_access(name: &str, core_token: &str) -> Result<()> {
add_collaborator(
"core",
"meta",
name,
AddCollaboratorOptionPermission::Read,
core_token,
)
.await?;
tracing::info!(%name, "forge: granted meta read access");
Ok(())
}
/// Add the forge `core/meta.git` URL as the `meta` remote in the
/// agent's proposed config repo so the agent (and the manager) can
/// fetch that legacy repo 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(());
}
// A malformed name has no proposed config repo (repos are only created
// under a validated Ident), so there's nothing to wire — no-op.
let Ok(agent) = hive_types::Ident::parse(name) else {
return Ok(());
};
let proposed_dir = Coordinator::agent_proposed_dir(&agent);
if !proposed_dir.join(".git").exists() {
return Ok(());
}
let want = format!("{}/core/meta.git", forge_http_base());
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
}
}
use super::{CONFIG_ORG, forge_git_url, reader_token};
/// Reseed a revived agent's `applied` repo — missing its `.git` entirely,
/// e.g. after `destroy --purge` — by fetching `agent-configs/<name>`'s
@ -334,38 +137,3 @@ pub async fn clone_config_into_proposed(dir: &Path, name: &str) -> bool {
}
}
}
/// Add `user` as a collaborator on `owner/repo` at `permission`.
/// Idempotent: adding an existing collaborator just updates its
/// permission (Forgejo answers 204 either way; a 201 from older
/// versions is tolerated defensively).
async fn add_collaborator(
owner: &str,
repo: &str,
user: &str,
permission: AddCollaboratorOptionPermission,
token: &str,
) -> Result<()> {
let op = format!("add collaborator {user} to {owner}/{repo}");
let res = bounded(
&op,
api(token)?.repo_add_collaborator(
owner,
repo,
user,
AddCollaboratorOption {
permission: Some(permission),
},
),
)
.await?;
match res {
Ok(()) => {}
Err(ForgejoError::UnexpectedStatusCode(s)) if s == StatusCode::CREATED => {}
Err(e) => {
return Err(e).context(op);
}
}
tracing::debug!(%owner, %repo, %user, ?permission, "forge: collaborator set");
Ok(())
}

View file

@ -14,7 +14,7 @@ use super::{api, bounded, forge_admin};
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. Root-only. Literal lives
// to drive admin API calls and as the credential nix's git reads. Root-only. Literal lives
// in `crate::paths`; aliased here under the long-standing name.
use crate::paths::FORGE_CORE_TOKEN as CORE_TOKEN_PATH;
// The `forge/core-avatar-set` marker lives in `crate::paths` — a one-shot