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
154 changes: 140 additions & 14 deletions bin/fm-crosscheck-azure-model-guest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -237,27 +237,153 @@ case "$HARNESS" in
python3 - "$BASE/pi-events.jsonl" "$RESULT" <<'PY'
import json
import pathlib
import re
import sys

source = pathlib.Path(sys.argv[1])
destination = pathlib.Path(sys.argv[2])
final = None
ended = False
for line in source.read_text(encoding="utf-8").splitlines():
event = json.loads(line)
if event.get("type") == "turn_end":
message = event.get("message") or {}
if message.get("role") == "assistant" and message.get("stopReason") == "stop":

# BEGIN PI_VERDICT_BODY_CONTRACT
PI_FENCED_BLOCK_RE = re.compile(
r"```[A-Za-z0-9_+.-]*[ \t]*\r?\n(?P<body>.*?)\r?\n?```",
re.DOTALL,
)


def pi_verdict_body(final_text: str) -> str:
"""Return the JSON body of a Pi reviewer's final assistant text.

An UNTERMINATED fence anywhere in the message refuses outright, before the
block count is even consulted. That ordering is the whole safety property.
A truncated verdict fence contributes ZERO complete blocks, so a model that
emitted any complete fence earlier in the same message - a draft, an
example, a quoted snippet - left the count at exactly one, and this
returned THAT EARLIER BLOCK as the verdict while silently discarding the
truncated real one. `stopReason` is `stop` in that shape (the exact live
condition seen on attempt 3), and the parse SUCCEEDS on the wrong block, so
nothing else downstream catches it: a superseded draft gets certified as
the review. That is strictly worse than the failure it replaced, which at
least failed loudly.
"""

stripped = final_text.strip()
# An odd number of fence markers means one was opened and never closed.
# Refusing on the marker count rather than on "no complete block found"
# is what makes a preceding complete fence unable to rescue a truncated
# one; returning the raw text sends it to the parser, which fails.
if stripped.count("```") % 2:
return stripped
blocks = PI_FENCED_BLOCK_RE.findall(stripped)
if len(blocks) != 1:
return stripped
# The block must be the ONLY JSON-bearing content in the message. An even
# fence count is not enough on its own: a COMPLETE example fence followed
# by a truncated BARE verdict also counts one block, and unwrapping there
# would certify the example and discard the real answer. Prose carries no
# braces, so this still tolerates a wrapper while refusing every shape
# where a second candidate verdict exists.
remainder = PI_FENCED_BLOCK_RE.sub("", stripped, count=1)
if "{" in remainder or "}" in remainder:
return stripped
return blocks[0].strip()
# END PI_VERDICT_BODY_CONTRACT


turn_count = 0
attempt_turn_count = 0
agent_ended = False
final_text = None
final_stop_reason = None
final_error = None
for line_number, line in enumerate(source.read_text(encoding="utf-8").splitlines(), start=1):
if not line.strip():
continue
try:
event = json.loads(line)
except (json.JSONDecodeError, ValueError, RecursionError) as exc:
raise SystemExit(
f"model guest: Pi returned malformed JSON events at line {line_number}: {exc}"
)
if not isinstance(event, dict):
raise SystemExit(
f"model guest: Pi returned a non-object event at line {line_number}"
)
event_type = event.get("type")
if event_type == "turn_end":
if agent_ended:
raise SystemExit("model guest: Pi emitted a turn after agent completion")
turn_count += 1
attempt_turn_count += 1
final_text = None
final_stop_reason = None
final_error = None
message = event.get("message")
if isinstance(message, dict) and message.get("role") == "assistant":
stop_reason = message.get("stopReason")
if isinstance(stop_reason, str):
final_stop_reason = stop_reason
error_message = message.get("errorMessage")
if isinstance(error_message, str) and error_message.strip():
final_error = error_message.strip()
content = message.get("content")
if isinstance(content, str):
final = content
final_text = content
elif isinstance(content, list):
final = "".join(part.get("text", "") for part in content if isinstance(part, dict))
elif event.get("type") == "agent_end":
ended = True
if not ended or not final:
raise SystemExit("model guest: Pi stopped without a successful verdict")
value = json.loads(final)
text_parts = [
part["text"]
for part in content
if isinstance(part, dict)
and part.get("type") == "text"
and isinstance(part.get("text"), str)
]
if text_parts:
final_text = "".join(text_parts)
elif event_type == "agent_end":
if agent_ended:
raise SystemExit("model guest: Pi emitted duplicate agent completion")
agent_ended = True
elif event_type == "auto_retry_start":
if not agent_ended:
raise SystemExit(
"model guest: Pi announced a retry while its agent was still running"
)
if attempt_turn_count == 0:
raise SystemExit(
"model guest: Pi announced a retry after an attempt that executed no turn"
)
if final_stop_reason == "stop":
raise SystemExit(
"model guest: Pi announced a retry after a successful assistant turn"
)
agent_ended = False
attempt_turn_count = 0
final_text = None
final_stop_reason = None
final_error = None
if turn_count == 0:
raise SystemExit("model guest: Pi completed without executing a turn")
if not agent_ended:
raise SystemExit("model guest: Pi stopped before agent completion")
if attempt_turn_count == 0:
raise SystemExit("model guest: Pi final attempt completed without executing a turn")
if final_stop_reason != "stop":
raise SystemExit(
"model guest: Pi final assistant turn did not stop successfully: "
f"stopReason={final_stop_reason!r}"
+ (f": {final_error[:500]}" if final_error else "")
)
if final_text is None or not final_text.strip():
raise SystemExit("model guest: Pi completed without a verdict artifact")
body = pi_verdict_body(final_text)
try:
value = json.loads(body)
except (json.JSONDecodeError, ValueError, RecursionError) as exc:
raise SystemExit(
f"model guest: Pi returned a malformed verdict artifact: {exc}; "
f"final assistant text began {body[:240]!r}"
)
if not isinstance(value, dict):
raise SystemExit("model guest: Pi verdict artifact must be an object")
destination.write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
PY
;;
Expand Down
14 changes: 14 additions & 0 deletions bin/fm-crosscheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -3795,6 +3795,7 @@ def pi_reviewer_command() -> list[str]:
# never closes its fence, so it yields zero complete blocks, falls through to
# the bare text, and still fails to parse - and `stopReason` refuses it one
# step earlier regardless. Both remain pinned by tests.
# BEGIN PI_VERDICT_BODY_CONTRACT
PI_FENCED_BLOCK_RE = re.compile(
r"```[A-Za-z0-9_+.-]*[ \t]*\r?\n(?P<body>.*?)\r?\n?```",
re.DOTALL,
Expand Down Expand Up @@ -3837,10 +3838,12 @@ def pi_verdict_body(final_text: str) -> str:
if "{" in remainder or "}" in remainder:
return stripped
return blocks[0].strip()
# END PI_VERDICT_BODY_CONTRACT


def pi_review_result(output: str) -> tuple[dict[str, Any], int]:
turn_count = 0
attempt_turn_count = 0
agent_ended = False
final_text: str | None = None
final_stop_reason: str | None = None
Expand Down Expand Up @@ -3869,6 +3872,7 @@ def pi_review_result(output: str) -> tuple[dict[str, Any], int]:
if agent_ended:
tool_fail("Pi reviewer emitted a turn after agent completion")
turn_count += 1
attempt_turn_count += 1
final_text = None
final_stop_reason = None
final_error = None
Expand Down Expand Up @@ -3908,11 +3912,21 @@ def pi_review_result(output: str) -> tuple[dict[str, Any], int]:
# refused above, so the original defense stands.
if not agent_ended:
tool_fail("Pi reviewer announced a retry while its agent was still running")
if attempt_turn_count == 0:
tool_fail("Pi reviewer announced a retry after an attempt that executed no turn")
if final_stop_reason == "stop":
tool_fail("Pi reviewer announced a retry after a successful assistant turn")
agent_ended = False
attempt_turn_count = 0
final_text = None
final_stop_reason = None
final_error = None
if turn_count == 0:
tool_fail("Pi reviewer completed without executing a turn")
if not agent_ended:
tool_fail("Pi reviewer stopped before agent completion")
if attempt_turn_count == 0:
tool_fail("Pi reviewer final attempt completed without executing a turn")
if final_stop_reason != "stop":
tool_fail(
"Pi reviewer final assistant turn did not stop successfully: "
Expand Down
9 changes: 4 additions & 5 deletions docs/azure-crosscheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ The primary review family is a registered cross-family lane, driven by Pi as tha
For that profile the packaged compartment credential is the api-key `models.json` (not a codex `auth.json`), pinned to exactly `https://api.fireworks.ai/inference/v1` - chat completions only; any other baseUrl, including a Responses API surface, refuses before staging. The archive gate also refuses a model-level `compat` that is not the lane's pinned compat, so a credential cannot weaken the truncated-verdict refusal on its way into a compartment.
pi gives model-level `baseUrl`/`api` fields precedence over the provider level, so the inspection, the archive gate, and the model guest all refuse a model entry carrying either field; the pinned provider level owns both.
`effective_provider_host` is model-aware: a cross-family review derives its own lane's host, today `api.fireworks.ai`, as its single egress host and refuses a conflicting configured `provider_host`, while the codex-family fallback keeps its `chatgpt.com` derivation.
The executing identity is the non-secret Foundry resource/deployment binding (an api key names no upstream account); the api key and anything derived from it never enter identity, ledger, or output.
The executing identity is the non-secret provider-slot, endpoint, and model binding (an api key names no upstream account); the api key and anything derived from it never enter identity, ledger, or output.
The interim claude reviewer lane is retired end to end: no `api.anthropic.com` host derivation, no `.credentials.json` packaging or boot copy, and no claude launch branch in the model guest.
Honest limit, corrected twice. This Azure-compartment lane does not run today, and until 2026-08-21 both reasons given for that were wrong. It is not that the image lacks `pi` (stale), and it is not ONLY that the lane is switched off. `$FM_HOME/config/crosscheck-azure.json` carries `"enabled": false`, set by an operator on 2026-08-20 - but flipping that flag would NOT have restored the codex-family path, because a second, independent, in-code blocker sat behind it: the archive gate derived the executing-account identity a second time and returned the BARE account id while the admitted identity carried a `codex:` / `openai-codex:` prefix, so `archived_identity != reviewer_account_identity` was structurally always true. A live run refused there at 04:43Z, before any billable resource. NO codex-family compartment review has ever run; every one of the resource group's historical `fm-crosscheck-model-*` deployments is on the cross-family path, which passed only because both sides there read one shared value. Fixed 2026-08-21, and the FIRST attempt at that fix was incomplete in a way worth recording: it made the host reader and the host archive gate share one derivation, but the model guest carried a THIRD copy that still derived the bare account id, so the refusal simply moved from staging into a booted, paid VM. All three now derive the prefixed identity; the guest cannot import the others because it ships self-contained onto the VM, so `model_guest_executing_account_unit` EXECUTES the guest's own credential block against the host readers to prove they agree, and is red on either the two- or the three-derivation form. The executable cross-family lane today is still the local Pi reviewer.
The Pi model guest applies the same byte-pinned verdict-body parser as the local gate: a bare JSON object or exactly one complete fenced JSON object with brace-free surrounding prose is accepted, while unterminated fences, multiple complete blocks, brace-bearing remainder, malformed JSON, and non-objects fail closed. The guest independently retains the terminal stream contract too: at least one turn, a final assistant `stop`, and exactly one completed agent are required. Pi's explicit `auto_retry_start` may open a continuation only after a completed attempt executed a turn and did not stop successfully; the continuation starts with empty terminal state and must execute its own turn before completing. `model_guest_pi_verdict_unit` executes the exact shipped heredoc and byte-compares its pure fence parser to the host copy so the self-contained image artifact cannot drift back to `json.loads(final)`.
Honest limit, corrected three times. A live 2026-08-21 cross-family attempt for PR #285 provisioned a model compartment and reached the guest, but it did not return a valid verdict: Pi produced one fenced JSON object and the guest's then-naive `json.loads(final)` refused it. Until that attempt, both reasons given for the lane not running were wrong. It is not that the image lacks `pi` (stale), and it is not ONLY that the lane is switched off. `$FM_HOME/config/crosscheck-azure.json` carries `"enabled": false`, set by an operator on 2026-08-20 - but flipping that flag would NOT have restored the codex-family path, because a second, independent, in-code blocker sat behind it: the archive gate derived the executing-account identity a second time and returned the BARE account id while the admitted identity carried a `codex:` / `openai-codex:` prefix, so `archived_identity != reviewer_account_identity` was structurally always true. A live run refused there at 04:43Z, before any billable resource. NO codex-family compartment review has ever run; every one of the resource group's historical `fm-crosscheck-model-*` deployments is on the cross-family path, which passed only because both sides there read one shared value. Fixed 2026-08-21, and the FIRST attempt at that fix was incomplete in a way worth recording: it made the host reader and the host archive gate share one derivation, but the model guest carried a THIRD copy that still derived the bare account id, so the refusal simply moved from staging into a booted, paid VM. All three now derive the prefixed identity; the guest cannot import the others because it ships self-contained onto the VM, so `model_guest_executing_account_unit` EXECUTES the guest's own credential block against the host readers to prove they agree, and is red on either the two- or the three-derivation form. A compartment review has still not completed; the only cross-family verdicts accepted so far came from 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.
Expand Down Expand Up @@ -181,9 +182,7 @@ That loses exactly the numbers a failed compartment review would be most useful
**This is a known gap with a follow-up that must land before any compartment timing is relied on: the lane must be stamped at its START.** It was previously described as bound to an image rebake; there is no rebake to wait for, so the follow-up is gated only on the lane being switched on.
`docs/crosscheck.md` owns that follow-up, including why stamping `execution_mode` earlier refuses the record instead of fixing it.

These numbers do not exist yet.
The compartment lane is disabled in the operator home, by `"enabled": false` in `$FM_HOME/config/crosscheck-azure.json` rather than by any missing image capability, so no run has executed these phases since they were instrumented.
`bin/fm-crosscheck.sh timings <task-id>` shows `-` in the `create`, `stage`, `boot`, and `collect` columns for every local-lane run, which is the honest reading: that lane did not do that work.
No accepted numbers exist yet. The failed 2026-08-21 compartment attempt executed these phases but did not produce the complete Azure reviewer identity record this ledger boundary requires, so its measurements were discarded as described above. The operator-home file still defaults the lane off with `"enabled": false`; that live attempt used the explicit Azure execution-mode opt-in. `bin/fm-crosscheck.sh timings <task-id>` therefore still shows `-` in the `create`, `stage`, `boot`, and `collect` columns for local-lane runs, which is the honest reading: those runs did not do that work.

## Operator setup

Expand Down
Loading
Loading