feat(#2502): re-lock deploy + rebuild from local applied, not forge

The persistent agent-<n> input url in the meta flake is now the forge
config repo, but the deploy + manual-rebuild re-lock must not follow it:
prepare_deploy and lock_update_for_rebuild override the input back to the
local applied mirror (git+file://<applied_dir>, main head) via
--override-input, which writes the applied rev into flake.lock.

This keeps the forge as the declared, reviewable source while:
- surviving forge unreachability (rebuilds fire on crash-restart and meta
  bumps too, not just config PRs -- coupling every rebuild to forge would
  be a regression),
- deploying exactly the reviewed head applied/<n>/main was fast-forwarded
  to (no TOCTOU on a newer forge head merging mid-deploy),
- reusing verify_commit's local-override pattern so verify and deploy eval
  the same source.

New applied_override_url helper + unit test.
This commit is contained in:
atlas 2026-07-16 20:16:08 +02:00 committed by mara
commit 27a099bb5b

View file

@ -238,7 +238,29 @@ pub async fn prepare_deploy(name: &str) -> Result<()> {
let _guard = META_LOCK.lock().await;
let dir = crate::paths::meta_root();
let input = format!("agent-{name}");
nix_logged(&dir, &["flake", "update", &input], name, "prepare-deploy").await?;
// Re-lock the agent input against the LOCAL applied mirror, not the
// persistent forge URL declared in the meta flake (see the `## Meta flake`
// note in docs/approvals.md): the deploy must build the exact reviewed
// config that `verify_commit` gated and `applied/<n>/main` was
// fast-forwarded to, and it must keep working when the forge is
// unreachable (rebuilds fire on crash-restart / meta bumps too, not just
// config PRs). `--override-input` writes the applied rev into the lock;
// the forge URL stays the declared, reviewable source of truth.
let applied = applied_override_url(&crate::paths::applied_dir(name));
nix_logged(
&dir,
&[
"flake",
"update",
&input,
"--override-input",
&input,
&applied,
],
name,
"prepare-deploy",
)
.await?;
// Stage the new lock — git+file://'s dirty-tree fetcher reads
// index entries, so the upcoming nixos-container update sees the
// bumped rev without a commit yet.
@ -281,7 +303,22 @@ pub async fn lock_update_for_rebuild(name: &str) -> Result<()> {
let _guard = META_LOCK.lock().await;
let dir = crate::paths::meta_root();
let input = format!("agent-{name}");
nix(&dir, &["flake", "update", &input]).await?;
// Re-lock from the local applied mirror, not the persistent forge URL —
// same rationale as `prepare_deploy`: build exactly `applied/<n>/main` and
// stay reproducible when the forge is unreachable.
let applied = applied_override_url(&crate::paths::applied_dir(name));
nix(
&dir,
&[
"flake",
"update",
&input,
"--override-input",
&input,
&applied,
],
)
.await?;
if !paths_dirty(&dir, &["flake.lock"]).await? {
return Ok(());
}
@ -301,6 +338,17 @@ fn agent_input_override(applied_dir: &Path, sha: &str) -> String {
format!("git+file://{}?rev={sha}", applied_dir.display())
}
/// `--override-input` URL re-locking an agent's config input against its
/// LOCAL applied mirror (`git+file://<applied_dir>`, current `main` head)
/// instead of the persistent forge URL declared in the meta flake. The
/// deploy + rebuild paths re-lock from here: the applied tree was already
/// fast-forwarded to the reviewed head, so this builds exactly that config
/// and stays reproducible when the forge is unreachable. No `?rev` — `main`
/// head is the reviewed head at deploy time. Pure so it's unit-testable.
fn applied_override_url(applied_dir: &Path) -> String {
format!("git+file://{}", applied_dir.display())
}
/// Non-mutating "would this commit apply?" verify for the PR-based config
/// flow. Evaluates the agent's nixos configuration with its meta
/// input overridden to the exact `sha`, WITHOUT moving `applied/main` or
@ -1463,6 +1511,20 @@ mod tests {
);
}
#[test]
fn applied_override_url_targets_local_mirror_main_head() {
// Deploy + rebuild re-lock against the local applied mirror's `main`
// head (no `?rev`), never the persistent forge URL — so a rebuild
// survives forge unreachability and builds the fast-forwarded config.
let p = Path::new("/var/lib/hyperhive/agents/iris/applied");
let url = applied_override_url(p);
assert_eq!(url, "git+file:///var/lib/hyperhive/agents/iris/applied");
assert!(
!url.contains("?rev="),
"must lock main head, not a pinned rev: {url}"
);
}
#[test]
fn render_flake_uses_explicit_nixpkgs_url_when_provided() {
let out = render_flake(