Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-c0re/src/meta_rev.rs
atlas b79896c237 hive reads the forge for nix as its own account, not core
hive-c0re copies the hive's store read token to /run/hyperhive/forge-hive-token
for nix's git credential helper, and no longer mints, reads or falls back to
the core admin token. drops the core user sweep, avatar upload, webhook
cleanup and hive-priv's RunForgeAdmin.
2026-10-25 18:06:48 +01:00

579 lines
21 KiB
Rust

//! The `internal/meta` commit a hive builds an agent from.
//!
//! `internal/meta` is the swarm's meta flake, committed by swarm-controller;
//! a hive has no meta flake of its own. A rebuild's `MetaSync` node resolves
//! the head ([`resolve`]) and records it ([`record`]); `Prebuild` and `Swap`
//! read the record ([`recorded`]), so both build the same flake. A create
//! resolves and uses the commit directly. When the head does not pin the
//! agent's current config, the build fails rather than falling back: the
//! swarm re-pins before it asks a hive to deploy.
//!
//! The record also names the pins a successful build deploys. [`mark_deployed`]
//! copies them to the agent's markers, which the boot reconcile
//! ([`crate::auto_update`]) and the dashboard's update chip read.
use anyhow::{Context, Result, bail};
use hive_priv_sock::{meta_flake_url, valid_meta_rev};
use serde::{Deserialize, Serialize};
use swarm_queue_client::agent_archetype::ArchetypeRef;
/// What the forge says about `internal/meta` and an agent's config repo.
pub(crate) trait MetaSource {
/// Whether nix's own `git` can authenticate to the forge: it reads as this
/// hive's account, from the credential file the source publishes.
fn nix_can_fetch(&self) -> bool;
/// The default branch's head commit of `internal/meta`; `None` when the
/// repo is absent or has no commit.
async fn meta_head(&self) -> Result<Option<String>>;
/// `internal/meta`'s `flake.nix` at `rev`.
async fn flake_at(&self, rev: &str) -> Result<String>;
/// The head of `agent-configs/<agent>`'s `main`; `None` when it has none.
async fn config_head(&self, agent: &str) -> Result<Option<String>>;
}
/// An `internal/meta` commit and the pins it holds for one agent.
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct MetaBuild {
/// The `internal/meta` commit.
pub rev: String,
/// The hyperhive commit its flake pins.
pub hyperhive: String,
/// The agent's config commit its flake pins; none for an archetype agent.
pub config: Option<String>,
}
/// The `internal/meta` commit to build `agent` from.
///
/// `internal/meta` is private, so a hive whose nix cannot authenticate
/// ([`MetaSource::nix_can_fetch`]) cannot build. The head is taken only when
/// its `flake.nix` builds the agent the way it is wanted now: from
/// `archetype` when that is set, otherwise from the config commit the forge's
/// `main` is at. A head that is behind (the swarm's update has not run, or
/// failed), lacks the agent or lists it under another archetype would build an
/// older module than the one being deployed.
///
/// # Errors
/// No usable head, a head that does not pin the agent's current config, or a
/// forge read that failed.
pub(crate) async fn choose(
source: &impl MetaSource,
agent: &str,
archetype: Option<&ArchetypeRef>,
) -> Result<MetaBuild> {
if !source.nix_can_fetch() {
bail!("{agent}: this hive has no forge credential for nix to fetch internal/meta with");
}
let Some(rev) = source.meta_head().await? else {
bail!("{agent}: the swarm has not published internal/meta yet");
};
if !valid_meta_rev(&rev) {
bail!("{agent}: internal/meta's head {rev:?} is not a commit id");
}
let flake = source.flake_at(&rev).await?;
let config = if archetype.is_some() {
None
} else {
source.config_head(agent).await?
};
if !hive_meta_flake::builds_agent_at(&flake, agent, archetype, config.as_deref()) {
bail!("{agent}: the swarm meta flake does not pin its config yet (internal/meta at {rev})");
}
let Some(hyperhive) = hive_meta_flake::pinned_rev(&flake, "hyperhive") else {
bail!("{agent}: internal/meta at {rev} pins no hyperhive commit");
};
Ok(MetaBuild {
rev,
hyperhive: hyperhive.to_owned(),
config,
})
}
/// The hyperhive commit `internal/meta`'s head pins; `None` when the repo or
/// its commit is absent.
///
/// # Errors
/// A forge read that failed, or a head that pins no hyperhive commit.
pub(crate) async fn hyperhive_pin(source: &impl MetaSource) -> Result<Option<String>> {
let Some(rev) = source.meta_head().await? else {
return Ok(None);
};
let flake = source.flake_at(&rev).await?;
let pin = hive_meta_flake::pinned_rev(&flake, "hyperhive")
.with_context(|| format!("internal/meta at {rev} pins no hyperhive commit"))?;
Ok(Some(pin.to_owned()))
}
/// The hyperhive commit `internal/meta` pins now, as this hive reads it: what
/// an agent's marker is compared with to tell it is stale. `None`, logged,
/// when the hive cannot tell (no forge token, a failed read, no commit yet),
/// which callers treat as nothing to rebuild for.
pub(crate) async fn current_hyperhive_pin() -> Option<String> {
let Some(source) = crate::forge::internal_meta_source().await else {
tracing::info!("internal/meta: no forge read token; cannot tell which hyperhive it pins");
return None;
};
match hyperhive_pin(&source).await {
Ok(pin) => pin,
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "internal/meta: cannot read its hyperhive pin");
None
}
}
}
/// [`choose`] against the forge this hive reads.
///
/// # Errors
/// As [`choose`], or this hive holds no forge token or cannot read the
/// agent's archetype.
pub(crate) async fn resolve(agent: &str) -> Result<MetaBuild> {
let archetype = crate::archetypes::of(agent)
.with_context(|| format!("{agent}: cannot read the agent's archetype"))?;
let Some(source) = crate::forge::internal_meta_source().await else {
bail!("{agent}: this hive holds no forge token to read internal/meta with");
};
let build = choose(&source, agent, archetype.as_ref()).await?;
tracing::info!(%agent, rev = %build.rev, "internal/meta: building from the swarm's meta flake");
Ok(build)
}
/// Record `build` as what `agent`'s rebuild builds from. The record is in
/// hive-c0re's own state ([`crate::paths::meta_rev_dir`]), where the agent
/// cannot write.
///
/// # Errors
/// The record file could not be written.
pub(crate) fn record(agent: &str, build: &MetaBuild) -> Result<()> {
record_in(&crate::paths::meta_rev_dir(), agent, build)
}
/// What [`record`] stored for `agent`, if it is a valid record.
pub(crate) fn recorded(agent: &str) -> Option<MetaBuild> {
recorded_in(&crate::paths::meta_rev_dir(), agent)
}
/// Written whole to a temp file beside the record and renamed over it, so a
/// reader sees the old record or the new one and a crash leaves no partial
/// file.
fn record_in(dir: &std::path::Path, agent: &str, build: &MetaBuild) -> Result<()> {
std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
let path = dir.join(agent);
let tmp = dir.join(format!(".{agent}.tmp"));
std::fs::write(&tmp, serde_json::to_vec(build)?)
.with_context(|| format!("write {}", tmp.display()))?;
std::fs::rename(&tmp, &path)
.with_context(|| format!("rename {} to {}", tmp.display(), path.display()))
}
fn recorded_in(dir: &std::path::Path, agent: &str) -> Option<MetaBuild> {
let build: MetaBuild = serde_json::from_slice(&std::fs::read(dir.join(agent)).ok()?).ok()?;
let valid = valid_meta_rev(&build.rev)
&& valid_meta_rev(&build.hyperhive)
&& build.config.as_deref().is_none_or(valid_meta_rev);
valid.then_some(build)
}
/// The record of `agent`'s build, or the error naming why a `Prebuild` or
/// `Swap` has nothing to build from.
///
/// # Errors
/// No valid record: its `MetaSync` did not run, or ran in an older hive-c0re.
pub(crate) fn recorded_or_err(agent: &str) -> Result<MetaBuild> {
recorded(agent)
.with_context(|| format!("{agent}: no internal/meta commit was recorded for this build"))
}
/// Mark `agent` as running what `build` pins: the hyperhive commit for the
/// boot reconcile ([`crate::paths::applied_rev_marker`]), the config commit
/// for the dashboard's deployed chip ([`crate::paths::applied_config_marker`]).
///
/// # Errors
/// A marker could not be written or removed.
pub(crate) fn mark_deployed(agent: &str, build: &MetaBuild) -> Result<()> {
mark_deployed_in(
&crate::paths::applied_rev_marker(agent),
&crate::paths::applied_config_marker(agent),
build,
)
}
fn mark_deployed_in(
hyperhive: &std::path::Path,
config: &std::path::Path,
build: &MetaBuild,
) -> Result<()> {
std::fs::write(hyperhive, &build.hyperhive)
.with_context(|| format!("write {}", hyperhive.display()))?;
match &build.config {
Some(rev) => {
std::fs::write(config, rev).with_context(|| format!("write {}", config.display()))
}
None => match std::fs::remove_file(config) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
Err(e).with_context(|| format!("remove {}", config.display()))
}
_ => Ok(()),
},
}
}
/// Whether an agent's `stored` marker says it runs the hyperhive commit `pin`
/// names. With no `pin`, because the hive can't read `internal/meta`, nothing
/// is stale: a rebuild would have no flake to build from either.
pub(crate) fn marker_is_fresh(pin: Option<&str>, stored: Option<&str>) -> bool {
pin.is_none_or(|pin| stored == Some(pin))
}
/// The `<flake>#<agent>` ref [`crate::lifecycle::prebuild_toplevel`] builds
/// for `rev`: the same flake hive-priv builds, from the same forge URL.
pub(crate) fn flake_ref(agent: &str, rev: &str) -> String {
flake_ref_at(crate::forge::forge_http_base(), agent, rev)
}
fn flake_ref_at(forge_base: &str, agent: &str, rev: &str) -> String {
format!("{}#{agent}", meta_flake_url(forge_base, rev))
}
#[cfg(test)]
mod tests {
use super::*;
const HYPERHIVE: &str = "1111111111111111111111111111111111111111";
const META: &str = "3333333333333333333333333333333333333333";
const ARGUS_HEAD: &str = "2222222222222222222222222222222222222222";
const STALE: &str = "4444444444444444444444444444444444444444";
/// A forge holding `internal/meta` at [`META`] with the given flake, and
/// `agent-configs/argus` at `config_head`.
struct Forge {
head: Option<&'static str>,
flake: String,
config_head: Option<&'static str>,
fetchable: bool,
}
impl MetaSource for Forge {
fn nix_can_fetch(&self) -> bool {
self.fetchable
}
async fn meta_head(&self) -> Result<Option<String>> {
Ok(self.head.map(str::to_owned))
}
async fn flake_at(&self, rev: &str) -> Result<String> {
assert_eq!(Some(rev), self.head, "the flake is read at the head");
Ok(self.flake.clone())
}
async fn config_head(&self, _: &str) -> Result<Option<String>> {
Ok(self.config_head.map(str::to_owned))
}
}
/// `internal/meta`'s flake as swarm-controller renders it: argus pinned at
/// [`ARGUS_HEAD`], and a reviewer from an archetype.
fn flake() -> String {
use swarm_wire::agent_archetype::ArchetypeRef;
let url = |repo: &str, rev: &str| {
hive_meta_flake::pinned_git_url(&format!("https://f.t/{repo}.git"), Some(rev)).unwrap()
};
let spec = |name: &str, archetype, pin| hive_meta_flake::AgentSpec {
name: name.to_owned(),
port: hive_meta_flake::agent_web_port(name),
archetype,
pin,
};
hive_meta_flake::render_flake(
&url("hyperhive/hyperhive", HYPERHIVE),
&[
spec(
"argus",
None,
Some(hive_meta_flake::AgentPin {
url: url("agent-configs/argus", ARGUS_HEAD),
canonical_inputs: vec![],
}),
),
spec(
"rev",
Some(ArchetypeRef {
source: "hyperhive".to_owned(),
name: "reviewer".to_owned(),
}),
None,
),
],
&[],
false,
)
.unwrap()
}
fn forge(config_head: Option<&'static str>) -> Forge {
Forge {
head: Some(META),
flake: flake(),
config_head,
fetchable: true,
}
}
fn reviewer() -> ArchetypeRef {
ArchetypeRef {
source: "hyperhive".to_owned(),
name: "reviewer".to_owned(),
}
}
/// The error a build with no usable pin ends in: it names the agent and
/// says the swarm meta flake does not pin its config yet.
fn assert_unpinned(err: &anyhow::Error, agent: &str) {
let msg = format!("{err:#}");
assert!(
msg.starts_with(&format!("{agent}: "))
&& msg.contains("swarm meta flake does not pin its config yet"),
"{msg}"
);
}
#[tokio::test]
async fn the_head_is_taken_when_it_pins_the_agents_current_config() {
assert_eq!(
choose(&forge(Some(ARGUS_HEAD)), "argus", None)
.await
.unwrap(),
MetaBuild {
rev: META.to_owned(),
hyperhive: HYPERHIVE.to_owned(),
config: Some(ARGUS_HEAD.to_owned()),
}
);
}
#[tokio::test]
async fn a_head_behind_the_agents_config_is_an_error() {
let err = choose(&forge(Some(STALE)), "argus", None)
.await
.unwrap_err();
assert_unpinned(&err, "argus");
}
#[tokio::test]
async fn a_head_without_the_agent_is_an_error() {
let mut source = forge(Some(ARGUS_HEAD));
source.flake = source.flake.replace("argus", "someone");
let err = choose(&source, "argus", None).await.unwrap_err();
assert_unpinned(&err, "argus");
}
#[tokio::test]
async fn an_archetype_agent_is_taken_when_the_head_lists_that_archetype() {
struct Archetypes(Forge);
impl MetaSource for Archetypes {
fn nix_can_fetch(&self) -> bool {
self.0.nix_can_fetch()
}
async fn meta_head(&self) -> Result<Option<String>> {
self.0.meta_head().await
}
async fn flake_at(&self, rev: &str) -> Result<String> {
self.0.flake_at(rev).await
}
async fn config_head(&self, _: &str) -> Result<Option<String>> {
panic!("an archetype agent has no config repo to read")
}
}
let source = Archetypes(forge(None));
assert_eq!(
choose(&source, "rev", Some(&reviewer())).await.unwrap(),
MetaBuild {
rev: META.to_owned(),
hyperhive: HYPERHIVE.to_owned(),
config: None,
},
"an archetype agent pins no config"
);
let other = ArchetypeRef {
name: "planner".to_owned(),
..reviewer()
};
assert_unpinned(
&choose(&source, "rev", Some(&other)).await.unwrap_err(),
"rev",
);
assert_unpinned(
&choose(&source, "ghost", Some(&reviewer()))
.await
.unwrap_err(),
"ghost",
);
}
/// The head lists argus as a custom agent and rev as an archetype agent;
/// an agent switched either way is not what the head builds.
#[tokio::test]
async fn an_agent_switched_between_custom_and_archetype_is_an_error() {
let source = forge(Some(ARGUS_HEAD));
assert_unpinned(
&choose(&source, "argus", Some(&reviewer()))
.await
.unwrap_err(),
"argus",
);
assert_unpinned(&choose(&source, "rev", None).await.unwrap_err(), "rev");
}
/// A hive without the credential nix's git reads cannot build from the
/// repo even though its own token reads it; the first test is the control.
#[tokio::test]
async fn a_hive_whose_nix_cannot_authenticate_cannot_build() {
let mut source = forge(Some(ARGUS_HEAD));
source.fetchable = false;
let err = choose(&source, "argus", None).await.unwrap_err();
assert!(
format!("{err:#}").contains("no forge credential"),
"{err:#}"
);
}
#[tokio::test]
async fn no_repo_or_no_commit_is_an_error_naming_the_agent() {
let mut source = forge(Some(ARGUS_HEAD));
source.head = None;
let err = choose(&source, "argus", None).await.unwrap_err();
assert!(
format!("{err:#}").starts_with("argus: the swarm has not published internal/meta"),
"{err:#}"
);
}
#[tokio::test]
async fn a_custom_agent_with_no_config_commit_is_an_error() {
let err = choose(&forge(None), "argus", None).await.unwrap_err();
assert_unpinned(&err, "argus");
}
#[tokio::test]
async fn the_hyperhive_pin_is_the_one_in_the_head() {
assert_eq!(
hyperhive_pin(&forge(None)).await.unwrap().as_deref(),
Some(HYPERHIVE)
);
let mut source = forge(None);
source.head = None;
assert_eq!(hyperhive_pin(&source).await.unwrap(), None);
let mut source = forge(None);
source.flake = source.flake.replace("hyperhive.url", "other.url");
assert!(hyperhive_pin(&source).await.is_err(), "a head with no pin");
}
fn build() -> MetaBuild {
MetaBuild {
rev: META.to_owned(),
hyperhive: HYPERHIVE.to_owned(),
config: Some(ARGUS_HEAD.to_owned()),
}
}
#[test]
fn a_record_is_read_back_and_replaced() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(recorded_in(dir.path(), "argus"), None);
record_in(dir.path(), "argus", &build()).unwrap();
assert_eq!(recorded_in(dir.path(), "argus"), Some(build()));
assert_eq!(
recorded_in(dir.path(), "rev"),
None,
"a record is one agent's"
);
let next = MetaBuild {
rev: STALE.to_owned(),
config: None,
..build()
};
record_in(dir.path(), "argus", &next).unwrap();
assert_eq!(recorded_in(dir.path(), "argus"), Some(next));
}
#[test]
fn a_record_leaves_no_temp_file_and_creates_its_dir() {
let dir = tempfile::tempdir().unwrap();
let records = dir.path().join("meta-rev");
record_in(&records, "argus", &build()).unwrap();
let names: Vec<_> = std::fs::read_dir(&records)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(names, ["argus"]);
}
/// Anything but a record naming commit ids is no record: a plain commit id
/// (the older format), a branch name, a bad pin.
#[test]
fn a_record_that_does_not_name_commit_ids_is_no_record() {
let dir = tempfile::tempdir().unwrap();
for bad in [
META.to_owned(),
"main\n".to_owned(),
serde_json::to_string(&MetaBuild {
rev: "main".to_owned(),
..build()
})
.unwrap(),
serde_json::to_string(&MetaBuild {
hyperhive: "main".to_owned(),
..build()
})
.unwrap(),
serde_json::to_string(&MetaBuild {
config: Some("main".to_owned()),
..build()
})
.unwrap(),
] {
std::fs::write(dir.path().join("argus"), &bad).unwrap();
assert_eq!(recorded_in(dir.path(), "argus"), None, "{bad}");
}
}
/// The record is hive-c0re's own state, never the agent-writable runtime
/// dir bind-mounted into the container.
#[test]
fn the_record_is_outside_the_agents_runtime_dir() {
assert!(!crate::paths::meta_rev_dir().starts_with(crate::paths::runtime_root()));
assert!(crate::paths::meta_rev_dir().starts_with(crate::paths::state_root()));
}
#[test]
fn marking_deployed_writes_both_pins_and_clears_the_config_of_an_archetype_agent() {
let dir = tempfile::tempdir().unwrap();
let hyperhive = dir.path().join("hyperhive");
let config = dir.path().join("config");
mark_deployed_in(&hyperhive, &config, &build()).unwrap();
assert_eq!(std::fs::read_to_string(&hyperhive).unwrap(), HYPERHIVE);
assert_eq!(std::fs::read_to_string(&config).unwrap(), ARGUS_HEAD);
let archetype = MetaBuild {
config: None,
..build()
};
mark_deployed_in(&hyperhive, &config, &archetype).unwrap();
assert!(!config.exists(), "an archetype agent pins no config");
mark_deployed_in(&hyperhive, &config, &archetype).unwrap();
}
#[test]
fn a_marker_is_fresh_only_at_the_pin() {
assert!(marker_is_fresh(Some(HYPERHIVE), Some(HYPERHIVE)));
assert!(!marker_is_fresh(Some(HYPERHIVE), Some(STALE)), "behind");
assert!(!marker_is_fresh(Some(HYPERHIVE), None), "never built");
assert!(marker_is_fresh(None, Some(STALE)), "no pin to compare with");
assert!(marker_is_fresh(None, None));
}
#[test]
fn the_prebuild_ref_is_the_flake_hive_priv_builds() {
assert_eq!(
flake_ref_at("http://forge.t.local", "argus", META),
format!("git+http://forge.t.local/internal/meta.git?rev={META}#argus")
);
}
}