From 43230979649e50d5d20f2c0aa9b02c4581161510 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 24 Oct 2026 19:20:37 +0200 Subject: [PATCH] keep the meta-rev record in hive-c0re's state, written by rename on deploy, the commit a rebuild builds an agent from is recorded under /var/lib/hyperhive/meta-rev/ instead of the agent's runtime dir, which is mounted read-write into its container. an agent can no longer pick or redirect the record. the file is written to a temp name and renamed over. --- hive-c0re/src/meta_rev.rs | 77 +++++++++++++++++++++++++++------------ hive-c0re/src/paths.rs | 11 ++++++ 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/hive-c0re/src/meta_rev.rs b/hive-c0re/src/meta_rev.rs index dc90afef..65b47abd 100644 --- a/hive-c0re/src/meta_rev.rs +++ b/hive-c0re/src/meta_rev.rs @@ -94,29 +94,33 @@ pub(crate) async fn resolve(agent: &str) -> Option { } } -/// The record file in `agent`'s runtime dir, which a reboot clears, so a -/// record never outlives the boot it was made in. -const RECORD_FILE: &str = "meta-rev"; - /// Record `rev` as the commit `agent`'s build reads, or clear the record for -/// `None`. +/// `None`. 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 or removed. pub(crate) fn record(agent: &str, rev: Option<&str>) -> Result<()> { - record_in(&crate::paths::agent_runtime_dir(agent), rev) + record_in(&crate::paths::meta_rev_dir(), agent, rev) } /// The commit [`record`] stored for `agent`, if it is a valid one. pub(crate) fn recorded(agent: &str) -> Option { - recorded_in(&crate::paths::agent_runtime_dir(agent)) + recorded_in(&crate::paths::meta_rev_dir(), agent) } -fn record_in(dir: &std::path::Path, rev: Option<&str>) -> Result<()> { - let path = dir.join(RECORD_FILE); +/// 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, rev: Option<&str>) -> Result<()> { + let path = dir.join(agent); match rev { Some(rev) => { - std::fs::write(&path, rev).with_context(|| format!("write {}", path.display())) + std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?; + let tmp = dir.join(format!(".{agent}.tmp")); + std::fs::write(&tmp, rev).with_context(|| format!("write {}", tmp.display()))?; + std::fs::rename(&tmp, &path) + .with_context(|| format!("rename {} to {}", tmp.display(), path.display())) } None => match std::fs::remove_file(&path) { Err(e) if e.kind() != std::io::ErrorKind::NotFound => { @@ -127,8 +131,8 @@ fn record_in(dir: &std::path::Path, rev: Option<&str>) -> Result<()> { } } -fn recorded_in(dir: &std::path::Path) -> Option { - let rev = std::fs::read_to_string(dir.join(RECORD_FILE)).ok()?; +fn recorded_in(dir: &std::path::Path, agent: &str) -> Option { + let rev = std::fs::read_to_string(dir.join(agent)).ok()?; valid_meta_rev(rev.trim()).then(|| rev.trim().to_owned()) } @@ -282,37 +286,64 @@ mod tests { async fn a_hive_whose_nix_cannot_authenticate_builds_from_its_own_flake() { let mut source = forge(Some(ARGUS_HEAD)); source.fetchable = false; - assert_eq!(choose(&source, "argus", false).await.unwrap(), None); + assert_eq!(choose(&source, "argus", None).await.unwrap(), None); } #[tokio::test] async fn no_repo_or_no_commit_is_the_hives_own_flake() { let mut source = forge(Some(ARGUS_HEAD)); source.head = None; - assert_eq!(choose(&source, "argus", false).await.unwrap(), None); + assert_eq!(choose(&source, "argus", None).await.unwrap(), None); } #[tokio::test] async fn a_custom_agent_with_no_config_commit_is_the_hives_own_flake() { - assert_eq!(choose(&forge(None), "argus", false).await.unwrap(), None); + assert_eq!(choose(&forge(None), "argus", None).await.unwrap(), None); } #[test] fn a_record_is_read_back_and_cleared() { let dir = tempfile::tempdir().unwrap(); - assert_eq!(recorded_in(dir.path()), None); - record_in(dir.path(), Some(META)).unwrap(); - assert_eq!(recorded_in(dir.path()), Some(META.to_owned())); - record_in(dir.path(), None).unwrap(); - assert_eq!(recorded_in(dir.path()), None); - record_in(dir.path(), None).unwrap(); + assert_eq!(recorded_in(dir.path(), "argus"), None); + record_in(dir.path(), "argus", Some(META)).unwrap(); + assert_eq!(recorded_in(dir.path(), "argus"), Some(META.to_owned())); + assert_eq!( + recorded_in(dir.path(), "rev"), + None, + "a record is one agent's" + ); + record_in(dir.path(), "argus", Some(STALE)).unwrap(); + assert_eq!(recorded_in(dir.path(), "argus"), Some(STALE.to_owned())); + record_in(dir.path(), "argus", None).unwrap(); + assert_eq!(recorded_in(dir.path(), "argus"), None); + record_in(dir.path(), "argus", None).unwrap(); + } + + #[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", Some(META)).unwrap(); + let names: Vec<_> = std::fs::read_dir(&records) + .unwrap() + .map(|e| e.unwrap().file_name()) + .collect(); + assert_eq!(names, ["argus"]); } #[test] fn a_record_that_is_not_a_commit_id_is_no_record() { let dir = tempfile::tempdir().unwrap(); - std::fs::write(dir.path().join(RECORD_FILE), "main\n").unwrap(); - assert_eq!(recorded_in(dir.path()), None); + std::fs::write(dir.path().join("argus"), "main\n").unwrap(); + assert_eq!(recorded_in(dir.path(), "argus"), None); + } + + /// 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] diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index d7cbddff..574be7ac 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -195,6 +195,17 @@ pub fn meta_git_index_lock() -> PathBuf { meta_root().join(".git/index.lock") } +/// `meta-rev/` — one file per agent naming the `internal/meta` commit its +/// rebuild builds from (`crate::meta_rev`). +/// +/// ⚠️ Deliberately **not** in the agent's runtime dir: that is bind-mounted +/// read-write into the container as `/run/hive`, and the commit decides which +/// flake the agent's own toplevel is built from. +#[must_use] +pub fn meta_rev_dir() -> PathBuf { + state_root().join("meta-rev") +} + /// `shared/` — the cross-agent `/shared` scratch space. A `&str`, so it /// stays a const; [`shared_root`] wraps it. // nix: bind-mounted into every agent container as `/shared` (the harness nix modules) — must match.