fix(image): re-check manifest on every boot so rebuilds aren't stale

microsandbox's default PullPolicy::IfMissing keys its layer cache by
*reference*. When we rebuild and re-push under the same :latest tag
(the entire `agent-vm setup` flow), the reference is unchanged, so
microsandbox happily boots the previous manifest and never sees the
new image.

Set PullPolicy::Always on both the launcher path (run.rs) and the
setup verify path (setup.rs). Always means "re-check the manifest at
the registry; reuse cached layers whose digests still match" — so the
cost when nothing changed is one HTTP round-trip + a few bytes.

Verified end-to-end: dropped a /etc/agent-vm-marker file into the
Dockerfile, pushed, observed sandbox sees the marker; rebuilt without
the marker, observed the next sandbox no longer sees it. Both
transitions required PullPolicy::Always — IfMissing kept showing the
old version in both directions.
This commit is contained in:
Evgeny Boger 2026-05-17 18:59:53 +03:00
parent 127f6b32a9
commit 55d52385bb
2 changed files with 14 additions and 2 deletions

View file

@ -14,7 +14,7 @@ use std::{
use anyhow::{Context, Result, bail};
use clap::Args as ClapArgs;
use microsandbox::Sandbox;
use microsandbox::{Sandbox, sandbox::PullPolicy};
use crate::session::ProjectSession;
@ -132,9 +132,16 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
"/workspace".to_string()
};
let mut patch_builder_steps = mkdir_chain(Path::new(&project_guest_path));
// PullPolicy::Always: microsandbox's default IfMissing keys its layer
// cache by reference, so re-running `agent-vm setup` (which rebuilds
// and re-pushes to the same :latest tag) would otherwise boot from the
// stale cached manifest. Always re-checks the manifest at the
// registry; cached layers whose digests still match are reused, so
// the cost is one round-trip + a few bytes when nothing changed.
let mut builder = Sandbox::builder(&session.sandbox_name)
.image(image.as_str())
.registry(|r| r.insecure())
.pull_policy(PullPolicy::Always)
.cpus(cpus)
.memory(memory_mib)
.workdir(project_guest_path.clone())

View file

@ -12,7 +12,7 @@ use std::{
use anyhow::{Context, Result, bail};
use clap::Args as ClapArgs;
use microsandbox::Sandbox;
use microsandbox::{Sandbox, sandbox::PullPolicy};
#[derive(ClapArgs)]
pub struct Args {
@ -67,9 +67,14 @@ fn build_script_path() -> Result<PathBuf> {
async fn verify_image(image: &str) -> Result<()> {
println!("==> Verifying {image}");
println!("==> Booting throwaway sandbox (this is the first VM cold-start; ~3s on a warm host)");
// Pull policy: Always — we just rebuilt + pushed under the same tag,
// so the cached manifest is by definition stale. Without this the
// verify step boots the *previous* image and quietly attests that an
// old version still works.
let sandbox = Sandbox::builder("agent-vm-setup-verify")
.image(image)
.registry(|r| r.insecure())
.pull_policy(PullPolicy::Always)
.cpus(1)
.memory(512)
.replace()