Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions bin/fm-crosscheck-azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,160 @@ def verify_scope_and_foundation(config: dict[str, Any]) -> Any:
return runner


# The image build declaration (docs/azure-crosscheck/model-image.json) writes
# one attestation tag per reviewer harness onto the managed image it
# distributes, taking every digest from the pinned closure
# (docs/azure-crosscheck/model-image-closure.json). Until this guard landed
# nothing read those tags. PR #246 exists because of what that costs: every Pi
# reviewer that reached a live model VM died on `pi: command not found`, one
# paid VM per attempt, because admission never compared the harness it was
# about to dispatch against what the configured image actually carries.
#
# `pi` binds two tags. Pi ships a `#!/usr/bin/env node` entrypoint and declares
# `engines.node >= 22.19.0`, so an image carrying `pi` without the pinned Node
# runtime fails the reviewer at launch for the same reason and at the same
# cost as an image carrying no `pi` at all.
MODEL_IMAGE_CLOSURE = ROOT / "docs" / "azure-crosscheck" / "model-image-closure.json"
GALLERY_IMAGE_VERSION_API_VERSION = "2023-07-03"
MANAGED_IMAGE_API_VERSION = "2024-03-01"
HARNESS_IMAGE_ATTESTATION: dict[str, tuple[tuple[str, str], ...]] = {
"pi": (
("pi-tarball-sha256", "piTarballSha256"),
("node-tarball-sha256", "nodeTarballSha256"),
),
"codex": (("codex-cli-sha256", "codexCliSha256"),),
}


def image_api_version(resource_id: str) -> str:
lowered = resource_id.lower()
if "/galleries/" in lowered and "/versions/" in lowered:
return GALLERY_IMAGE_VERSION_API_VERSION
return MANAGED_IMAGE_API_VERSION


def read_image_tags(
config: dict[str, Any], resource_id: str, label: str
) -> tuple[dict[str, str], str | None]:
"""Read one image resource's tags and its source image, failing closed.

An unreadable resource and an unreadable tag object are both refusals:
this guard exists to stand between a wrong image and a paid VM, so it may
never admit on ambiguity. An ARM resource with no `tags` at all is not
ambiguous - it is an image that attests nothing - so that reads as an
empty tag set and the caller refuses it as absence.
"""

url = (
"https://management.azure.com"
+ resource_id
+ "?api-version="
+ image_api_version(resource_id)
)
resource, rc, detail = az(
config, ["rest", "--method", "get", "--url", url], check=False
)
if rc != 0 or not isinstance(resource, dict):
diagnostic = detail.strip()[-400:] if isinstance(detail, str) else ""
raise AzureCrosscheckError(
"Azure Crosscheck model image is unreadable, so its harness "
f"attestation is unproven: {label} {resource_id}: "
+ (diagnostic or "no diagnostic")
)
tags = resource.get("tags")
if tags is None:
tags = {}
if not isinstance(tags, dict) or not all(
isinstance(key, str) and isinstance(value, str) for key, value in tags.items()
):
raise AzureCrosscheckError(
"Azure Crosscheck model image exposes no readable tags, so its "
f"harness attestation is unproven: {label} {resource_id}"
)
properties = resource.get("properties")
storage = properties.get("storageProfile") if isinstance(properties, dict) else None
source = storage.get("source") if isinstance(storage, dict) else None
source_id = source.get("id") if isinstance(source, dict) else None
if not isinstance(source_id, str) or not source_id.startswith("/subscriptions/"):
source_id = None
return tags, source_id


def pinned_image_closure() -> dict[str, Any]:
try:
value = json.loads(MODEL_IMAGE_CLOSURE.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise AzureCrosscheckError(
"Azure Crosscheck pinned image closure is unreadable, so the model "
f"image attestation cannot be compared: {exc}"
) from exc
if not isinstance(value, dict):
raise AzureCrosscheckError(
"Azure Crosscheck pinned image closure is unreadable, so the model "
"image attestation cannot be compared: it is not an object"
)
return value


def require_model_image_attests_harness(
config: dict[str, Any], harness: str
) -> dict[str, str]:
"""Refuse a model image that does not attest the harness about to run.

This is a preflight refusal and nothing more: when it passes, the lane
does exactly what it did before. It proves the configured image was built
from a declaration carrying that harness's pinned artifact, which is not
the same claim as the harness executing successfully inside the guest.
"""

expected_tags = HARNESS_IMAGE_ATTESTATION.get(harness)
if not expected_tags:
raise AzureCrosscheckError(
"Azure Crosscheck has no image attestation for reviewer harness "
f"{harness!r}"
)
closure = pinned_image_closure()
image_id = config["model_image_id"]
tags, source_id = read_image_tags(config, image_id, "configured image")
source_tags: dict[str, str] | None = None
attested: dict[str, str] = {}
for tag, closure_key in expected_tags:
value = tags.get(tag)
if value is None and source_id is not None:
# The build writes its artifactTags onto the managed image it
# distributes; promoting that image into a gallery image version
# is a separate operator step that need not carry them, so the
# source is followed exactly once before absence is declared.
if source_tags is None:
source_tags, _ = read_image_tags(
config, source_id, "source managed image"
)
value = source_tags.get(tag)
if value is None:
raise AzureCrosscheckError(
"Azure Crosscheck model image does not attest reviewer harness "
f"{harness!r}: attestation tag {tag!r} is absent from {image_id}"
+ (f" and from its source {source_id}" if source_id else "")
+ "; refusing before any model VM"
)
entry = closure.get(closure_key)
pinned = entry.get("value") if isinstance(entry, dict) else None
if not isinstance(pinned, str) or not pinned:
raise AzureCrosscheckError(
"Azure Crosscheck pinned image closure is unreadable, so the "
"model image attestation cannot be compared: "
f"{closure_key!r} is missing"
)
if value != pinned:
raise AzureCrosscheckError(
"Azure Crosscheck model image attestation "
f"{tag!r} disagrees with pinned closure {closure_key!r}: image "
f"{value} is not closure {pinned}; refusing before any model VM"
)
attested[tag] = value
return attested


# R6 (docs/azure-requirements.md): these pins must equal the constants in
# bin/fm-crosscheck.py; tests/fm-crosscheck-azure.test.sh enforces the
# equality. The GLM lane binds exactly one Foundry resource + deployment and
Expand Down Expand Up @@ -1685,6 +1839,16 @@ def _run_azure_review_in_lane(
# The lane locks are the queue authority; this live-VM read is only a
# safety cap against leaked or foreign reviewer compute.
raise core.CrosscheckToolError("Azure review admission reached its local model concurrency safety cap")
# Nothing billable exists yet: no capacity reservation, no staged object,
# no model VM. This is the last point at which a model image that does not
# attest the harness this review dispatches can be refused for free, so
# the attestation tags the build writes are read here. The refusal is a
# tool failure rather than a hard error because the same image can attest
# a different harness, which is exactly what reviewer rotation is for.
try:
require_model_image_attests_harness(azure, config["harness"])
except AzureCrosscheckError as exc:
raise core.CrosscheckToolError(str(exc)) from exc
config["account_selector"] = {
"codex": "CODEX_HOME",
"pi": "PI_CODING_AGENT_DIR",
Expand Down
10 changes: 9 additions & 1 deletion docs/azure-crosscheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ The interim claude reviewer lane is retired end to end: no `api.anthropic.com` h
Honest limit, corrected 2026-08-20: this Azure-compartment GLM lane does not run today because it is switched off, not because the image lacks `pi`. `$FM_HOME/config/crosscheck-azure.json` exists and carries `"enabled": false`, set by an operator on 2026-08-20; that flag, not an image rebake, is what stands between this lane and a run. The executable GLM lane today is the local Pi reviewer.
The earlier reading of this limit said the built image carries no `pi` binary and needed a rebake. That was measured on 2026-08-16 against gallery version `1.0.1786915905`, whose source managed image `img-fm7c799d-ccm-1.0.0` was built on 2026-08-13 from the pre-Pi declaration and carries no `pi-tarball-sha256` tag (M29 in the owner's mutation ledger, `firstmate-azure-full-completion-mutation-ledger.md`, which lives outside this repository rather than in it). It was already stale when it was written here: `model_image_id` has named `1.0.1787092687` since 2026-08-18T22:45Z.
That current version was published 2026-08-18T22:38:08Z from managed image `img-fm7c799d-ccm-1.0.1787091895`, which carries `pi-tarball-sha256` `a69a1859...` and `node-tarball-sha256` `d60acfe0...`, matching `docs/azure-crosscheck/model-image-closure.json` for `pi-coding-agent` 0.84.1 and Node v22.23.2. Only a build from the Pi-carrying declaration writes those tags, its Image Builder run succeeded, and that declaration asserts `/usr/local/bin/pi --version` against the tracked version twice under `set -eu`, before and after the credential purge, so a build that reached distribution cannot have omitted `pi`. What remains unproven is a Pi review actually completing on this image, which is a separate claim from the binary being present.
Both readings were guesses about an image that admission never inspected. It does now: the harness attestation guard described under Operator setup reads `pi-tarball-sha256` and `node-tarball-sha256` off the configured image before any model VM exists, so the next time this question is asked the lane answers it from the image rather than from a document, and a wrong `model_image_id` is refused for free instead of discovered on a paid VM.
The 25K TPM quota cap (DataZoneStandard capacity 25) bounds review throughput until quota is raised.

The model process has no Azure CLI credential, managed identity, SSH agent, Docker socket, repository checkout, control-home mount, MCP configuration, or shell/read tool.
Expand Down Expand Up @@ -193,7 +194,14 @@ Plan legs are read-only; `image-build` and `policy-apply` are billable/security-
Record the exact built image resource ID before any live review.
`image-build` distributes a managed image; the reviewer SKUs in [`azure-crosscheck/compartment.json`](azure-crosscheck/compartment.json) need the `DiskControllerTypes` feature a managed image cannot carry, so an operator promotes that managed image into a Compute Gallery image version and it is the gallery version's resource ID that `model_image_id` names.
That promotion is the one step of this contract the bounded command does not own, so a rebuilt image reaches reviews only after it is promoted and `model_image_id` is repointed.
Admission does not check that the configured image actually carries the harness it admits: the build tags the image with its `pi-tarball-sha256`, `codex-cli-sha256`, and `claude-cli-sha256`, and nothing reads those tags. Until it does, pointing `model_image_id` at an image built before a harness was added admits that harness and fails it inside a paid VM.
Admission refuses a model image that does not attest the reviewer harness it is about to dispatch.
The build writes `pi-tarball-sha256`, `node-tarball-sha256`, `codex-cli-sha256`, and `claude-cli-sha256` onto the managed image it distributes, from the pinned closure; `require_model_image_attests_harness` in [`bin/fm-crosscheck-azure.py`](../bin/fm-crosscheck-azure.py) now reads them, so those previously unread tags are load-bearing.
Closing that read closes the gap PR #246 recorded: pointing `model_image_id` at an image built before a harness was added used to admit that harness and fail it inside a paid VM, one VM per attempt, on `pi: command not found`.
The check runs after the lane is held and after the foundation preflight, but before the capacity reservation, before any staged object, and before the model VM, so a refusal costs nothing.
It reads the configured image's own tags with one read-only ARM GET, and follows the version's source managed image exactly once when a required tag is absent there, because gallery promotion is a separate operator step that need not carry `artifactTags`.
A tag that is absent refuses, a tag that disagrees with the tracked closure digest refuses and names which digest disagreed, and an unreadable image, unreadable source, or unreadable tag object refuses rather than admitting: the guard never admits on ambiguity.
`pi` binds two tags, its tarball and its Node runtime, because Pi ships a `#!/usr/bin/env node` entrypoint and an image carrying `pi` without the pinned Node fails the reviewer at launch for the same reason and at the same cost.
Honest limit: this proves the configured image attests a harness, not that the harness runs. It reads what the build recorded about the image; a review completing on that image remains a separate claim, and a harness not in the attestation table (the retired `claude` lane) is refused rather than admitted.
One more Pi-lane cost is open: the model guest launches `pi` without `--offline`, and the compartment's egress allowlist is Azure DNS plus one provider endpoint, so Pi's startup update and telemetry calls are dropped rather than refused and each waits out its own timeout on a paid VM before the review begins.

The pinned closure is tracked at [`azure-crosscheck/model-image-closure.json`](azure-crosscheck/model-image-closure.json).
Expand Down
Loading
Loading