chore(runtime): promote v0.8.4 from canary to the default image
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped

Every mission on gw-04 was already running v0.8.4 — pinned by
CLAWMATES_RUNTIME_IMAGE in .env. The canary is retired: the default tag
`clawmates-runtime:sync` now IS that image, the override is commented out,
and the built-in default is the single source of truth again.

Promoting it exposed why the pin was load-bearing in the first place. The
default tag resolved to zeroclaw 0.8.3 — two releases behind what was
actually running — and the REGISTRY copy of the same tag was a different
image again, 849MB against 2.31GB, without the Rust toolchain. A host that
pulled `sync` rather than retagging it would have lost the on-green test
gate with every probe still reporting success.

A moving tag pointing somewhere old resolves perfectly, starts perfectly,
and runs old code. Nothing anywhere said which image a mission got, so two
things now do:

- mission_runtime logs the image it resolved and whether that came from
  the env override or the built-in default, once at startup.
- runtime_preflight probes `zeroclaw --version` alongside the other tools
  and prints every tool's VERSION, not just that it is present. A presence
  check passes happily on an image two releases behind, which is exactly
  what happened here and was found by running the binary by hand.

Rollback is a retag: `clawmates-runtime:pre-v084-default` on gw-04 holds
the previous default, and .env.pre-v084-default holds the previous pin.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-11 13:57:08 -07:00
co-authored by Claude Opus 5
parent d810fc0a86
commit cdc45bd082
2 changed files with 38 additions and 7 deletions
+21 -5
View File
@@ -36,9 +36,20 @@ use futures::StreamExt;
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
/// Docker image the per-mission runtime uses. Matches the current /// Docker image the per-mission runtime uses, overridable per-deploy via
/// singleton `clawmates-runtime` image; can be overridden per-deploy /// `CLAWMATES_RUNTIME_IMAGE` (for a canary; the default is the shipped one).
/// via `CLAWMATES_RUNTIME_IMAGE`. ///
/// A MOVING tag, and that is the hazard: it names whatever the host last
/// tagged. gw-04's `sync` was two zeroclaw releases behind the canary every
/// mission was actually running, and there was no way to see it — a moving tag
/// that points somewhere old resolves perfectly, starts perfectly, and runs old
/// code. The registry copy of the same tag was a DIFFERENT image again, without
/// the Rust toolchain, so a host that pulled rather than retagged would have
/// silently lost the on-green test gate.
///
/// Two things make that visible now: this module logs the image it resolved and
/// `runtime_preflight` reports the versions inside the container, not just that
/// the binaries exist.
const DEFAULT_IMAGE: &str = "clawmates-runtime:sync"; const DEFAULT_IMAGE: &str = "clawmates-runtime:sync";
/// Well-known ZeroClaw gateway port. /// Well-known ZeroClaw gateway port.
@@ -568,8 +579,13 @@ impl MissionRuntimeProvisioner {
} else { } else {
Docker::connect_with_local_defaults().ok()? Docker::connect_with_local_defaults().ok()?
}; };
let image = let (image, source) = match std::env::var("CLAWMATES_RUNTIME_IMAGE") {
std::env::var("CLAWMATES_RUNTIME_IMAGE").unwrap_or_else(|_| DEFAULT_IMAGE.to_string()); Ok(v) if !v.trim().is_empty() => (v, "CLAWMATES_RUNTIME_IMAGE"),
_ => (DEFAULT_IMAGE.to_string(), "the built-in default"),
};
// Said once at startup, because every mission on this host runs in it
// and nothing else names it out loud.
eprintln!("mission_runtime: per-mission runtime image = {image} (from {source})");
Some(MissionRuntimeProvisioner { docker, image }) Some(MissionRuntimeProvisioner { docker, image })
} }
+17 -2
View File
@@ -37,6 +37,11 @@ struct Dependency {
} }
const DEPENDENCIES: &[Dependency] = &[ const DEPENDENCIES: &[Dependency] = &[
Dependency {
argv: &["zeroclaw", "--version"],
needed_for: "driving every container-tier turn; the version is also how \
a runtime image that silently rolled back is noticed",
},
Dependency { Dependency {
argv: &["cargo", "--version"], argv: &["cargo", "--version"],
needed_for: "the on_green_tests gate for Rust repos; without it every \ needed_for: "the on_green_tests gate for Rust repos; without it every \
@@ -193,10 +198,20 @@ pub fn report_at_boot() {
let missing: Vec<&ToolStatus> = tools.iter().filter(|t| !t.present).collect(); let missing: Vec<&ToolStatus> = tools.iter().filter(|t| !t.present).collect();
if missing.is_empty() { if missing.is_empty() {
let names: Vec<&str> = tools.iter().map(|t| t.program.as_str()).collect(); let names: Vec<&str> = tools.iter().map(|t| t.program.as_str()).collect();
// The VERSIONS, not just the names. A tag that quietly
// points at an older build passes a presence check
// perfectly: gw-04's default tag was two zeroclaw releases
// behind while every probe said "present", and the only way
// anyone found out was running the binary by hand.
let detail: Vec<String> = tools
.iter()
.map(|t| format!("{}={}", t.program, t.detail))
.collect();
eprintln!( eprintln!(
"runtime_preflight: `{container}` has all {} expected tools ({})", "runtime_preflight: `{container}` has all {} expected tools ({}) — {}",
tools.len(), tools.len(),
names.join(", ") names.join(", "),
detail.join("; ")
); );
return; return;
} }