Skip to content

fix(bridge-cutover): accept the pinned post-cutover state so worker-state steps unblock - #9

Merged
ruby-dlee merged 7 commits into
mainfrom
claude/workerstate-registry-third-state-9628
Jul 20, 2026
Merged

fix(bridge-cutover): accept the pinned post-cutover state so worker-state steps unblock#9
ruby-dlee merged 7 commits into
mainfrom
claude/workerstate-registry-third-state-9628

Conversation

@ruby-dlee

Copy link
Copy Markdown
Owner

Intent

Captain-issued work order (2026-07-19, workorder-cutover-workerstate-registry-fix.md): the Bridge account-routing cutover is half-applied and parked because bridge_worker_state_transaction.py --verify-provisioned (6b) and --finalize (6c) transitively re-run the sealed-adoption quiet point, which accepts the live registry only at the two pre-cutover hashes, while step 4 already legitimately rewrote it to the candidate registry - the 2026-07-19 live NO-GO. Both reverse edges are closed by design (rollback refused past the post-install boundary; re-prepare impossible post-adoption), so the tooling must accept the advanced state. The same refusal arms an unrecoverable trap: once a first provider login publishes an identity bundle, worker-state rollback() must call _candidate_api(), the exact refusing loader. Deliberate design decisions: (1) The work order's candidate fix (tolerate the registry as a third accepted state, mirroring the current-link tolerance) was verified INSUFFICIENT during investigation - after the registry digest, the adoption plan's observe() also refuses the post-cutover current-link targets and front door, and validate_bundle's phase branch then rejects the applied-plus-boundary main cutover. So the fix is a pinned post-cutover assessment instead: the strict adoption plan stays byte-identical for all adoption-time callers, and only when it refuses does validate_bundle check the one further accepted state - main cutover fully applied AND post-install boundary marked AND fully sealed adoption journal AND intact retained sealed release trees AND every adoption-managed live path (both current links, front door, registry) at its exact cutover-new identity pinned from the exact reconstructed cutover manifest - reported as a new runtime-switched phase with runtime_switch_ready remaining false. Any other combination re-raises the original strict refusal with the cutover-side probe diagnosis preserved as a suffix, and a full-apply WITHOUT the marked boundary intentionally keeps refusing (pinned to the documented 4-then-5-then-6 order). (2) Regression tests pin the accepted-registry set explicitly as the work order demands: tampered post-cutover registry refuses with unknown SHA-256, candidate registry before adoption refuses, partially reverted post-cutover state refuses, and the accept path requires the boundary. (3) Worker-state tests run 6a/6b/6c and rollback identity-drift attribution in the exact live order (adoption, step 4, boundary, then worker state) - pre-fix these reproduce the literal live NO-GO error string; the rollback test proves the armed trap is defused. (4) A Transaction order doc paragraph records the pinned post-cutover acceptance and why. (5) Prior gate rounds on this branch already applied and committed: review fixes preserving the cutover probe diagnosis, refusing malformed pins without crashing, and asserting the registry pin at the assessment site (515cc78), plus a test-lab opt-in for tests/fm-bootstrap.test.sh agent-fleet preflight cases (9db99c5) - that bootstrap test failure was machine-state-caused (the live cutover installed the real ~/.local/bin front door, which the physical-path resolution found despite the sandbox PATH) and is unrelated to the bridge diff; do not re-litigate those settled rounds. A prior run (01KXYYSQT5V84ACPQFQVQV1C3N) validated this exact content through review and test fix rounds and died only from an agent-process crash at the end of the test step. Full bridge-cutover python lane, fm-bootstrap.test.sh, and the key post-cutover regression tests all pass locally at this HEAD. Constraint honored: no absolute agent-fleet releases paths appear anywhere in this work (tilde/relative only); the change alters what a security-relevant validator accepts, which is why it ships through this full pipeline with pinning tests rather than as a hot patch.

What Changed

  • Added post_cutover_plan to bridge_sealed_adoption.py: a non-mutating assessment that accepts a sealed adoption superseded by the fully applied main cutover, requiring a caller-supplied pin per adoption-managed path (registry file pin, current-link targets, front door), a fully sealed journal, the quiet-point contract, and intact retained sealed release trees. _validate_disabled_registry / _validate_quiet_point now take an optional post-cutover registry digest and return it; the strict plan path is unchanged for adoption-time callers. Malformed or missing pins raise AdoptionError rather than crashing, and the standalone adoption CLI prints a bundle-path hint (CLI-only, never embedded in the refusal) on an unknown-live-registry refusal.
  • Wired it into prepare_bridge_cutover.validate_bundle: adoption plan refusals now route through the new _superseded_adoption_phase, which probes the main cutover for full application plus a marked post-install boundary, reconstructs pins from the cutover manifest via _post_cutover_pins, and reports the new runtime-switched phase. Any other combination re-raises the original refusal with the cutover-side probe diagnosis appended as a suffix; a full apply without the marked boundary still refuses.
  • Added regression coverage pinning the accepted set (boundary required, tampered post-cutover registry, candidate registry before adoption, partially reverted state, registry pin without SHA-256) and worker-state 6a/6b/6c plus rollback identity-drift attribution after the runtime switch; documented the pinned acceptance and its rationale in docs/bridge-cutover-sealed-runtimes.md. Two unrelated harness adjustments ride along: the fm-bootstrap agent-fleet preflight cases opt into the account-routing test lab, and the agent-fleet lease timing bounds widen from 5s to 8s to absorb full-sweep scheduling load. The full bridge python lane (216 tests) and both shell suites pass.

Risk Assessment

✅ Low: This round is a tightly scoped relocation of one operator hint from the library validator to the adoption CLI's error handler, restoring the library refusal to its byte-identical pre-hint form and adding two tests that pin the hint's presence on the CLI path and absence from the refusal text, with no change to what the validator accepts.

Testing

Beyond the already-green baseline sweep, I ran the full bridge-cutover python lane (216 tests, OK) plus fm-bootstrap.test.sh and fm-account-routing.test.sh standalone, then wrote a manual operator harness that assembles a genuine post-cutover machine state and drives the real CLIs at both the base commit and this branch. The harness reproduces the exact live 2026-07-19 NO-GO on base tooling and shows it cleared here: the strict adoption plan still refuses identically, validate now reports the pinned runtime-switched phase with runtime_switch_ready false, worker-state 6a/6b/6c complete, rollback attributes identity drift instead of dying, and all four pinned negative states still refuse with the cutover probe diagnosis preserved. One harness-only hiccup (base tools staged under an uncanonicalized /var symlink caused a wrong-reason refusal) was fixed and re-run before capturing evidence. Everything passes; no actionable findings.

Evidence: Full end-to-end CLI transcript: live NO-GO reproduced on base tooling and cleared on this branch

==============================================================================
SETUP: switched machine, documented order 4 -> 5 -> 6
==============================================================================
  live registry           : /private/var/folders/y_/bfdbj_vx20l9b9tw7crgkzwm0000gn/T/bridge-cutover-prep-test-ja4avw02/fixture/config/accounts.toml
  live registry sha256    : 0a8833ba39e2a30481bcd28665ca71bdb48a2f1e38269a909308ff615f5e7aa0
  bundled candidate sha256: 0a8833ba39e2a30481bcd28665ca71bdb48a2f1e38269a909308ff615f5e7aa0
  agent-fleet current ->  : releases/0.2.0-new
  main cutover fully applied, post-install boundary MARKED

==============================================================================
1. BEFORE (base commit 6b67114) - the live 2026-07-19 NO-GO
==============================================================================

$ /opt/homebrew/opt/python@3.11/bin/python3.11 /private/var/folders/y_/bfdbj_vx20l9b9tw7crgkzwm0000gn/T/bridge-base-tools-ak2410qt/bridge-cutover/bridge_sealed_adoption.py <machine>/output/bundle/sealed-adoption.manifest.json
  refused: live registry has unknown SHA-256: 0a8833ba39e2a30481bcd28665ca71bdb48a2f1e38269a909308ff615f5e7aa0
  [exit 2]  # base: sealed-adoption quiet point refuses the advanced registry

$ /opt/homebrew/opt/python@3.11/bin/python3.11 /private/var/folders/y_/bfdbj_vx20l9b9tw7crgkzwm0000gn/T/bridge-base-tools-ak2410qt/bridge-cutover/prepare_bridge_cutover.py --driver /private/var/folders/y_/bfdbj_vx20l9b9tw7crgkzwm0000gn/T/bridge-base-tools-ak2410qt/bridge-cutover/bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
  refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 0a8833ba39e2a30481bcd28665ca71bdb48a2f1e38269a909308ff615f5e7aa0
  [exit 2]  # base: bundle validation (what 6b/6c transitively re-run) refuses

==============================================================================
2. AFTER (this branch) - strict adoption plan is BYTE-IDENTICAL
==============================================================================

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/bridge_sealed_adoption.py <machine>/output/bundle/sealed-adoption.manifest.json
  refused: live registry has unknown SHA-256: 0a8833ba39e2a30481bcd28665ca71bdb48a2f1e38269a909308ff615f5e7aa0
  if the normal cutover has fully applied, validate through the bundle: prepare_bridge_cutover.py validate accepts the pinned post-cutover state
  [exit 2]  # head: adoption CLI still refuses, now with an operator hint

==============================================================================
3. AFTER (this branch) - bundle validation accepts the pinned post-cutover state
==============================================================================

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/prepare_bridge_cutover.py --driver <repo>/tools/bridge-cutover/bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
  {
    "adoption_manifest_fingerprint": "8949d74aa1575a93cd79f7373a3a218bdd676207afab33928578f7f6d07e0db9",
    "agent_fleet_contract_version": 2,
    "agent_fleet_version": "0.2.0",
    "bundle": "<machine>/output/bundle/bundle.json",
    "cutover_phase": "runtime-switched",
    "cutover_ready": false,
    "enabled": 0,
    "external_reserves": 2,
    "fleet_managed_workers": 6,
    "initial_registry_sha256": "dfb2daf3e3838103ea92c05559264e819d76f674a2c6733f00b29cd5db6dbc15",
    "manifest_fingerprint": "90d6b8d041bf9dc38d2bf7036a066dd44dd8a51d8f6f22d3c75819933133c701",
    "new_registry_sha256": "0a8833ba39e2a30481bcd28665ca71bdb48a2f1e38269a909308ff615f5e7aa0",
    "old_registry_sha256": "2c8c1072c0fd526b1235115bdf8a360a33ad4a0b333863419933ae2603c7efe5",
    "profiles": 8,
    "projects_per_provider": 1,
    "quota_binary": "<machine>/quota-axi/releases/0.1.7-new/bin/quota-axi",
    "quota_node_binary": "<machine>/quota-axi/releases/0.1.7-new/runtime/node",
    "rollback_contract_version": 1,
    "rollback_version": "0.1.5",
    "runtime_switch_ready": false,
    "transaction_id": "bridge-cutover-fixture",
    "valid": true,
    "worker_state_phase": "not-started",
    "worker_state_ready": false
  }
  [exit 0]  # head: validate reports the new runtime-switched phase

  cutover_phase       = 'runtime-switched'
  runtime_switch_ready= False
  cutover_ready       = False

==============================================================================
4. AFTER - worker-state gate 6a / 6b / 6c on the switched machine
==============================================================================

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/bridge_worker_state_transaction.py <machine>/output/bundle/worker-state.manifest.json --begin
  {
    "cleanup_allowed": false,
    "cleanup_policy": "explicit-manual-only-after-terminal-state",
    "credential_bytes_snapshotted": false,
    "identity_bundle_count": 2,
    "phase": "snapshotted",
    "provision_verified": false,
    "reserves_touched": false,
    "rollback_available": true,
    "snapshot_bytes_present": true,
    "transaction_id": "bridge-cutover-fixture-workers",
    "valid": true,
    "worker_state_ready": false,
    "workers": [
      "claude-1",
      "claude-2",
      "codex-1",
      "codex-2",
      "codex-3",
      "codex-4"
    ]
  }
  [exit 0]  # 6a --begin

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/bridge_worker_state_transaction.py <machine>/output/bundle/worker-state.manifest.json --verify-provisioned
  {
    "cleanup_allowed": false,
    "cleanup_policy": "explicit-manual-only-after-terminal-state",
    "credential_bytes_snapshotted": false,
    "identity_bundle_count": 2,
    "phase": "provision_verified",
    "provision_verified": true,
    "reserves_touched": false,
    "rollback_available": true,
    "snapshot_bytes_present": true,
    "transaction_id": "bridge-cutover-fixture-workers",
    "valid": true,
    "worker_state_ready": false,
    "workers": [
      "claude-1",
      "claude-2",
      "codex-1",
      "codex-2",
      "codex-3",
      "codex-4"
    ]
  }
  [exit 0]  # 6b --verify-provisioned (the step that NO-GO'd live)

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/bridge_worker_state_transaction.py <machine>/output/bundle/worker-state.manifest.json --finalize
  {
    "cleanup_allowed": true,
    "cleanup_policy": "explicit-manual-only-after-terminal-state",
    "credential_bytes_snapshotted": false,
    "identity_bundle_count": 2,
    "phase": "complete",
    "provision_verified": true,
    "reserves_touched": false,
    "rollback_available": false,
    "snapshot_bytes_present": true,
    "transaction_id": "bridge-cutover-fixture-workers",
    "valid": true,
    "worker_state_ready": true,
    "workers": [
      "claude-1",
      "claude-2",
      "codex-1",
      "codex-2",
      "codex-3",
      "codex-4"
    ]
  }
  [exit 0]  # 6c --finalize

==============================================================================
5. AFTER - the armed rollback trap is defused (identity drift attribution)
==============================================================================

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/bridge_worker_state_transaction.py <machine>/output/bundle/worker-state.manifest.json --begin
  {
    "cleanup_allowed": false,
    "cleanup_policy": "explicit-manual-only-after-terminal-state",
    "credential_bytes_snapshotted": false,
    "identity_bundle_count": 2,
    "phase": "snapshotted",
    "provision_verified": false,
    "reserves_touched": false,
    "rollback_available": true,
    "snapshot_bytes_present": true,
    "transaction_id": "bridge-cutover-fixture-workers",
    "valid": true,
    "worker_state_ready": false,
    "workers": [
      "claude-1",
      "claude-2",
      "codex-1",
      "codex-2",
      "codex-3",
      "codex-4"
    ]
  }
  [exit 0]  # 6a --begin

  # a first provider login rewrote codex-bundle.json: rollback must call the refusing loader

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/bridge_worker_state_transaction.py <machine>/output/bundle/worker-state.manifest.json --rollback
  {
    "cleanup_allowed": true,
    "cleanup_policy": "explicit-manual-only-after-terminal-state",
    "credential_bytes_snapshotted": false,
    "identity_bundle_count": 2,
    "phase": "rolled_back",
    "provision_verified": false,
    "reserves_touched": false,
    "rollback_available": false,
    "snapshot_bytes_present": true,
    "transaction_id": "bridge-cutover-fixture-workers",
    "valid": true,
    "worker_state_ready": false,
    "workers": [
      "claude-1",
      "claude-2",
      "codex-1",
      "codex-2",
      "codex-3",
      "codex-4"
    ]
  }
  [exit 0]  # --rollback attributes the drift instead of dying

  restored identity bundle = {"schema": 1, "provider": "codex"}

==============================================================================
6. AFTER - the accepted set stays pinned: every other combination refuses
==============================================================================

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/prepare_bridge_cutover.py --driver <repo>/tools/bridge-cutover/bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
  refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 43497890b5b44bbf964259a919a81a30e36a78de04864a445bf24a3eeb7b4db7 (post-cutover probe: quiet-point live registry has unknown SHA-256: 43497890b5b44bbf964259a919a81a30e36a78de04864a445bf24a3eeb7b4db7)
  [exit 2]  # head: tampered post-cutover registry

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/prepare_bridge_cutover.py --driver <repo>/tools/bridge-cutover/bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
  refused: sealed-adoption state is invalid: live registry has unknown SHA-256: b0abd58cdc4c765ef3d01efafbe0ce0c57ae44de5032efb7ea8bd24e1a85e421 (post-cutover probe: main cutover is not fully applied past the marked post-install irreversible boundary)
  [exit 2]  # head: full apply WITHOUT the marked boundary

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/prepare_bridge_cutover.py --driver <repo>/tools/bridge-cutover/bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
  refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 17280ecc7c070badb1081f8e0e7c195b0724bf7b896e10253ea44f6b40a40762 (post-cutover probe: observed old/new states are not a valid transaction prefix; refusing an out-of-order or externally modified state)
  [exit 2]  # head: partially reverted post-cutover state

$ /opt/homebrew/opt/python@3.11/bin/python3.11 <repo>/tools/bridge-cutover/prepare_bridge_cutover.py --driver <repo>/tools/bridge-cutover/bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
  refused: sealed-adoption state is invalid: live registry has unknown SHA-256: d0ba66c3146ba86b0438f466572a0fdc1d23c485860017d517a4defe878aad36 (post-cutover probe: quota-current live link has an unknown target: 'releases/0.1.5-unsealed'; only the exact manifest old/new targets are accepted)
  [exit 2]  # head: candidate registry BEFORE adoption (no cutover applied)

==============================================================================
RESULT: live NO-GO reproduced on base tooling and cleared on this branch
==============================================================================
Evidence: Reproduction harness (builds the post-cutover machine state, drives base vs HEAD CLIs)
#!/usr/bin/env python3.11
"""Reproduce the 2026-07-19 live Bridge cutover NO-GO and show it defused.

Builds a real post-cutover machine state in the documented 4-then-5-then-6
order (sealed adoption applied, main cutover step 4 executed forward, step 5
post-install irreversible boundary marked), then drives the REAL operator CLIs
as subprocesses:

  * base-commit tooling  -> the literal live 6b NO-GO refusal
  * this branch's tooling -> pinned post-cutover acceptance, 6b/6c green
  * tamper / missing-boundary / pre-adoption states -> still refused

Nothing here is a unittest assertion harness: every line of the transcript is
what an operator would actually see on the switched machine.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

REPO = Path(__file__).resolve()
REPO = Path(os.environ["FM_REPO"]).resolve()
BASE_COMMIT = os.environ.get("BASE_COMMIT", "6b6711419a774b6104b9260f8c988af8d33182aa")

sys.path.insert(0, str(REPO))
sys.path.insert(0, str(REPO / "tools" / "bridge-cutover"))

from tests.test_prepare_bridge_cutover import (  # noqa: E402
    DRIVER,
    CutoverPreparationFixture,
    prepare,
)

HEAD_TOOLS = REPO / "tools" / "bridge-cutover"
PY = sys.executable
ENV = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}


def hdr(text: str) -> None:
    print(f"\n{'=' * 78}\n{text}\n{'=' * 78}", flush=True)


def run(label: str, argv: list[str], *, expect: int | None = None) -> subprocess.CompletedProcess:
    shown = " ".join(
        str(a).replace(str(REPO), "<repo>").replace(os.environ.get("STATE_ROOT", "\0"), "<machine>")
        for a in argv
    )
    print(f"\n$ {shown}", flush=True)
    proc = subprocess.run(argv, capture_output=True, text=True, env=ENV, timeout=900)
    for stream in (proc.stdout, proc.stderr):
        if stream.strip():
            for line in stream.rstrip().splitlines():
                print(f"  {line.replace(os.environ.get('STATE_ROOT', chr(0)), '<machine>')}", flush=True)
    print(f"  [exit {proc.returncode}]  # {label}", flush=True)
    if expect is not None and proc.returncode != expect:
        raise SystemExit(f"FATAL: expected exit {expect}, got {proc.returncode} for {label}")
    return proc


def base_tools() -> Path:
    """Materialize the base-commit copy of the two changed tools."""
    # The tools refuse a non-canonical driver path, and macOS /var is a symlink.
    real_temp_root = Path(os.path.realpath(tempfile.gettempdir()))
    out = Path(tempfile.mkdtemp(prefix="bridge-base-tools-", dir=real_temp_root))
    shutil.copytree(HEAD_TOOLS, out / "bridge-cutover", dirs_exist_ok=False)
    shutil.rmtree(out / "bridge-cutover" / "__pycache__", ignore_errors=True)
    for name in ("prepare_bridge_cutover.py", "bridge_sealed_adoption.py"):
        blob = subprocess.run(
            ["git", "-C", str(REPO), "show", f"{BASE_COMMIT}:tools/bridge-cutover/{name}"],
            capture_output=True, check=True,
        ).stdout
        (out / "bridge-cutover" / name).write_bytes(blob)
    return out / "bridge-cutover"


def build_post_cutover_machine(fixture: CutoverPreparationFixture, *, mark_boundary: bool = True):
    """Steps 1-3 (prepare + adoption), step 4 (runtime switch), step 5 (boundary)."""
    fixture.prepare()
    bundle = json.loads((fixture.bundle_dir / "bundle.json").read_text(encoding="utf-8"))
    adoption = prepare._load_adoption_driver()
    adoption.apply(adoption.load_manifest(Path(bundle["adoption_manifest_path"])))
    driver = prepare._load_driver(DRIVER)
    manifest_path = Path(bundle["manifest_path"])
    driver.execute(driver.load_manifest(manifest_path), "forward")
    if mark_boundary:
        driver.mark_post_install_irreversible_boundary(driver.load_manifest(manifest_path))
    return bundle, driver, manifest_path


def main() -> int:
    base = base_tools()
    fixture = CutoverPreparationFixture()
    os.environ["STATE_ROOT"] = str(fixture.root)
    try:
        hdr("SETUP: switched machine, documented order 4 -> 5 -> 6")
        bundle, driver, manifest_path = build_post_cutover_machine(fixture)
        adoption_manifest = bundle["adoption_manifest_path"]
        bundle_json = str(fixture.bundle_dir / "bundle.json")
        worker_manifest = str(fixture.bundle_dir / "worker-state.manifest.json")
        print(f"  live registry           : {fixture.live}")
        print(f"  live registry sha256    : {prepare._sha256(fixture.live)}")
        print(f"  bundled candidate sha256: {bundle['registry_new']['sha256']}")
        print(f"  agent-fleet current ->  : {os.readlink(fixture.agent_root / 'current')}")
        print("  main cutover fully applied, post-install boundary MARKED", flush=True)

        hdr("1. BEFORE (base commit 6b67114) - the live 2026-07-19 NO-GO")
        run(
            "base: sealed-adoption quiet point refuses the advanced registry",
            [PY, str(base / "bridge_sealed_adoption.py"), adoption_manifest],
            expect=2,
        )
        run(
            "base: bundle validation (what 6b/6c transitively re-run) refuses",
            [PY, str(base / "prepare_bridge_cutover.py"), "--driver",
             str(base / "bridge_cutover_transaction.py"), "validate", bundle_json],
            expect=2,
        )

        hdr("2. AFTER (this branch) - strict adoption plan is BYTE-IDENTICAL")
        run(
            "head: adoption CLI still refuses, now with an operator hint",
            [PY, str(HEAD_TOOLS / "bridge_sealed_adoption.py"), adoption_manifest],
            expect=2,
        )

        hdr("3. AFTER (this branch) - bundle validation accepts the pinned post-cutover state")
        proc = run(
            "head: validate reports the new runtime-switched phase",
            [PY, str(HEAD_TOOLS / "prepare_bridge_cutover.py"), "--driver", str(DRIVER),
             "validate", bundle_json],
            expect=0,
        )
        result = json.loads(proc.stdout)
        print(f"\n  cutover_phase       = {result['cutover_phase']!r}")
        print(f"  runtime_switch_ready= {result['runtime_switch_ready']}")
        print(f"  cutover_ready       = {result['cutover_ready']}", flush=True)

        hdr("4. AFTER - worker-state gate 6a / 6b / 6c on the switched machine")
        run("6a --begin", [PY, str(HEAD_TOOLS / "bridge_worker_state_transaction.py"),
                           worker_manifest, "--begin"], expect=0)
        materialize(fixture)
        run("6b --verify-provisioned (the step that NO-GO'd live)",
            [PY, str(HEAD_TOOLS / "bridge_worker_state_transaction.py"),
             worker_manifest, "--verify-provisioned"], expect=0)
        write_identities(fixture, worker_manifest)
        run("6c --finalize", [PY, str(HEAD_TOOLS / "bridge_worker_state_transaction.py"),
                              worker_manifest, "--finalize"], expect=0)
    finally:
        fixture.cleanup()
        shutil.rmtree(base.parent, ignore_errors=True)

    # ---- rollback trap (fresh machine) ----
    fixture = CutoverPreparationFixture()
    os.environ["STATE_ROOT"] = str(fixture.root)
    try:
        hdr("5. AFTER - the armed rollback trap is defused (identity drift attribution)")
        bundle, _, _ = build_post_cutover_machine(fixture)
        worker_manifest = str(fixture.bundle_dir / "worker-state.manifest.json")
        write_identities(fixture, worker_manifest)
        run("6a --begin", [PY, str(HEAD_TOOLS / "bridge_worker_state_transaction.py"),
                           worker_manifest, "--begin"], expect=0)
        materialize(fixture)
        import bridge_worker_state_transaction as ws
        drifted = ws.load_manifest(Path(worker_manifest)).identity_bundles["codex"]
        drifted.write_text('{"schema":1,"provider":"codex","fresh":true}', encoding="utf-8")
        print(f"\n  # a first provider login rewrote {drifted.name}: rollback must call the refusing loader")
        run("--rollback attributes the drift instead of dying",
            [PY, str(HEAD_TOOLS / "bridge_worker_state_transaction.py"),
             worker_manifest, "--rollback"], expect=0)
        print(f"\n  restored identity bundle = {drifted.read_text(encoding='utf-8')}", flush=True)
    finally:
        fixture.cleanup()

    # ---- pinned refusals ----
    hdr("6. AFTER - the accepted set stays pinned: every other combination refuses")
    for label, mutate, mark_boundary in (
        ("tampered post-cutover registry", "tamper", True),
        ("full apply WITHOUT the marked boundary", None, False),
        ("partially reverted post-cutover state", "revert-link", True),
    ):
        fixture = CutoverPreparationFixture()
        os.environ["STATE_ROOT"] = str(fixture.root)
        try:
            build_post_cutover_machine(fixture, mark_boundary=mark_boundary)
            if mutate == "tamper":
                fixture.live.write_bytes(fixture.live.read_bytes() + b"# drift\n")
                fixture.live.chmod(0o600)
            elif mutate == "revert-link":
                current = fixture.agent_root / "current"
                os.unlink(current)
                os.symlink("releases/0.1.5-old", current)
            run(f"head: {label}",
                [PY, str(HEAD_TOOLS / "prepare_bridge_cutover.py"), "--driver", str(DRIVER),
                 "validate", str(fixture.bundle_dir / "bundle.json")], expect=2)
        finally:
            fixture.cleanup()

    fixture = CutoverPreparationFixture()
    os.environ["STATE_ROOT"] = str(fixture.root)
    try:
        fixture.prepare()
        fixture.live.write_bytes((fixture.bundle_dir / "registry.new.toml").read_bytes())
        fixture.live.chmod(0o600)
        run("head: candidate registry BEFORE adoption (no cutover applied)",
            [PY, str(HEAD_TOOLS / "prepare_bridge_cutover.py"), "--driver", str(DRIVER),
             "validate", str(fixture.bundle_dir / "bundle.json")], expect=2)
    finally:
        fixture.cleanup()

    hdr("RESULT: live NO-GO reproduced on base tooling and cleared on this branch")
    return 0


def _canonical(value: object) -> bytes:
    return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")


def materialize(fixture: CutoverPreparationFixture) -> None:
    bundle = json.loads((fixture.bundle_dir / "bundle.json").read_text(encoding="utf-8"))
    contract = bundle["activation_plan"]["provision"]["sealed_contract"]
    closed = _canonical(contract["closed_claude_state"]["payload"])
    for profile_id, sealed in contract["plans"].items():
        plan = sealed["plan"]
        home = Path(plan["home"])
        home.mkdir(parents=True, mode=0o700, exist_ok=True)
        home.chmod(0o700)
        for entry in plan["entries"]:
            relative = entry["relative_path"]
            if relative == ".":
                continue
            path = home / relative
            if entry["type"] == "dir":
                path.mkdir(parents=True, mode=int(entry["mode"], 8), exist_ok=True)
                path.chmod(int(entry["mode"], 8))
            elif entry["type"] == "symlink":
                path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
                os.symlink(entry["target"], path)
            else:
                payload = closed if relative == ".claude.json" else _canonical(
                    {"profile": profile_id, "path": relative}
                )
                path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
                path.write_bytes(payload)
                path.chmod(int(entry["mode"], 8))


def write_identities(fixture: CutoverPreparationFixture, worker_manifest: str) -> None:
    import bridge_worker_state_transaction as ws
    manifest = ws.load_manifest(Path(worker_manifest))
    for provider, path in manifest.identity_bundles.items():
        path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
        path.write_text(json.dumps({"schema": 1, "provider": provider}), encoding="utf-8")
        path.chmod(0o600)


if __name__ == "__main__":
    raise SystemExit(main())
Evidence: Before/after: the live 6b NO-GO and the pinned post-cutover acceptance
1. BEFORE (base commit 6b67114) - the live 2026-07-19 NO-GO
$ python3.11 <base>/bridge_sealed_adoption.py <machine>/output/bundle/sealed-adoption.manifest.json
refused: live registry has unknown SHA-256: 0a8833ba39e2a30481bcd28665ca71bdb48a2f1e38269a909308ff615f5e7aa0
[exit 2]
$ python3.11 <base>/prepare_bridge_cutover.py --driver <base>/bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 0a8833ba...
[exit 2] # this is what 6b/6c transitively re-run

2. AFTER (this branch) - strict adoption plan is BYTE-IDENTICAL
$ python3.11 tools/bridge-cutover/bridge_sealed_adoption.py <machine>/output/bundle/sealed-adoption.manifest.json
refused: live registry has unknown SHA-256: 0a8833ba...
if the normal cutover has fully applied, validate through the bundle: prepare_bridge_cutover.py validate accepts the pinned post-cutover state
[exit 2] # hint is stderr-only, never in the refusal string

3. AFTER - bundle validation accepts the pinned post-cutover state
$ python3.11 tools/bridge-cutover/prepare_bridge_cutover.py --driver .../bridge_cutover_transaction.py validate <machine>/output/bundle/bundle.json
"cutover_phase": "runtime-switched",
"runtime_switch_ready": false,
"cutover_ready": false
[exit 0]

4. AFTER - worker-state gate on the switched machine
--begin -> "phase": "snapshotted" [exit 0]
--verify-provisioned -> "phase": "provision_verified" [exit 0] # the step that NO-GO'd live
--finalize -> "phase": "complete", "worker_state_ready": true [exit 0]

5. AFTER - armed rollback trap defused (drifted codex identity bundle)
--rollback -> "phase": "rolled_back" [exit 0]
restored identity bundle = {"schema": 1, "provider": "codex"}
Evidence: Pinned refusal matrix: every other post-cutover combination still refuses, probe diagnosis preserved
$ validate # tampered post-cutover registry
refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 3a3f9119...
(post-cutover probe: quiet-point live registry has unknown SHA-256: 3a3f9119...)
[exit 2]

$ validate # full apply WITHOUT the marked boundary (pinned to the 4-then-5-then-6 order)
refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 1e0670b0...
(post-cutover probe: main cutover is not fully applied past the marked post-install irreversible boundary)
[exit 2]

$ validate # partially reverted post-cutover state (agent-fleet current relinked to old)
refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 663832ed...
(post-cutover probe: observed old/new states are not a valid transaction prefix; refusing an out-of-order or externally modified state)
[exit 2]

$ validate # candidate registry BEFORE adoption
refused: sealed-adoption state is invalid: live registry has unknown SHA-256: 9671ab7c...
(post-cutover probe: quota-current live link has an unknown target: 'releases/0.1.5-unsealed'; only the exact manifest old/new targets are accepted)
[exit 2]
- Outcome: 🔧 1 issue found → auto-fixed (2) ✅ across 3 runs (3h54m29s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • ⚠️ tools/bridge-cutover/bridge_sealed_adoption.py:1486 - In post_cutover_plan the per-operation pin loop only rejects pin is None, while the registry pin above it (line 1462) is fully guarded with isinstance(registry_pin, Mapping). A pin value that is present but not a Mapping (e.g. a bare string) flows into _observe_post_cutover_operation, which immediately calls pin.get(&#34;kind&#34;) at line 1415/1433 and raises AttributeError. That is not an AdoptionError, so _superseded_adoption_phase (prepare_bridge_cutover.py:5704, which catches only adoption_driver.AdoptionError) does not convert it, and the AttributeError escapes validate_bundle uncaught instead of producing a clean refusal. The in-repo caller _post_cutover_pins always builds dicts so this is not reachable today, but post_cutover_plan is a module-level API and commit 515cc78 explicitly hardened this function to refuse malformed pins without crashing — the loop was left out of that fix. Apply the same isinstance(pin, Mapping) guard (or hoist a shared pin validator) so any malformed pin raises AdoptionError.
  • ℹ️ tools/bridge-cutover/bridge_sealed_adoption.py:1597 - The pinned post-cutover assessment is only reachable through prepare_bridge_cutover.validate_bundle. The sealed-adoption CLI's default action is still strict plan (main(), line 1597), and post_cutover_plan is not exposed as a subcommand nor referenced from the refusal text. An operator on the switched machine who runs bridge_sealed_adoption.py directly — the natural first move when diagnosing the parked cutover — still gets exactly the 2026-07-19 message, refused: live registry has unknown SHA-256: &lt;digest&gt;, with no indication that a legitimate post-cutover state exists and is accepted elsewhere. Consider adding a --post-cutover-plan action (requiring the cutover manifest to derive pins) or amending the strict refusal to name the runtime-switched phase. Flagging rather than fixing because exposing a second, more permissive assessment on the adoption CLI is a deliberate product/security surface decision.

🔧 Fix: guard non-Mapping post-cutover pins; hint bundle path on strict refusal
1 warning still open:

  • ⚠️ tools/bridge-cutover/bridge_sealed_adoption.py:698 - The new operator hint is appended inside the library validator _validate_disabled_registry, gated only on post_cutover_sha256 is None. That gate suppresses it for post_cutover_plan, but NOT for the strict plan() call that validate_bundle itself makes (prepare_bridge_cutover.py:5453), which always passes None. _superseded_adoption_phase then embeds that refusal verbatim as invalid = f&#34;sealed-adoption state is invalid: {refusal}&#34;, so a failed prepare_bridge_cutover.py validate on a post-cutover machine now emits: 'sealed-adoption state is invalid: live registry has unknown SHA-256: <d>; if the main cutover has fully applied, validate through the bundle (prepare_bridge_cutover.py validate), which accepts the pinned post-cutover state (post-cutover probe: <real diagnosis>)'. The message advises running the exact command that just failed and claims the pinned state is accepted immediately before the probe explains why it was refused - muddying precisely the diagnosis commit 515cc78 was written to preserve. The same text reaches worker-state 6b/6c operators through _candidate_api's WorkerStateError(f&#34;cannot load exact candidate API/registry: {exc}&#34;) wrapper. Suggested fix: move the hint out of the library validator and append it only at the adoption CLI boundary, in main()'s except (AdoptionError, OSError) printer (~line 1610). That still points the standalone-CLI operator at the working path (the stated goal) while keeping composite messages from validate_bundle and worker-state clean. Marked ask-user rather than auto-fix because the round-1 instruction named _validate_disabled_registry or the plan-level wrapper as the location, and main() is a third option you did not specify.

🔧 Fix: move post-cutover hint from validator to adoption CLI
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed (2) ✅
  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: no fix needed; account-routing timing flake re-verified green
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: widen agent-fleet lease timing bounds to absorb sweep load
✅ Re-checked - no issues remain.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"
  • bash tests/bridge-cutover-python.test.sh — full bridge python lane, 216 tests, OK (skipped=2), including the new test_validate_accepts_post_cutover_state_only_with_boundary_marked, test_validate_refuses_tampered_live_registry_after_runtime_switch, test_validate_refuses_candidate_registry_before_adoption, test_validate_refuses_partially_reverted_state_after_runtime_switch, test_post_cutover_plan_refuses_registry_pin_without_sha256, test_verify_and_finalize_pass_after_runtime_switch, test_rollback_attributes_identity_drift_after_runtime_switch
  • bash tests/fm-bootstrap.test.sh — 23/23 ok, exit 0 (confirms the settled test-lab opt-in for the agent-fleet preflight cases)
  • bash tests/fm-account-routing.test.sh — standalone, green including ok - Agent Fleet lease mutations are bounded and ambiguous outcomes retain ownership state (the widened 8s bound from 22d0711)
  • Manual end-to-end operator reproduction: python3.11 live_nogo_repro.py — builds a real post-cutover machine state (adoption applied, driver.execute(..., &#34;forward&#34;), mark_post_install_irreversible_boundary), then runs base-commit and HEAD CLIs as subprocesses against the same on-disk state
  • python3.11 &lt;base&gt;/bridge_sealed_adoption.py &lt;adoption manifest&gt; and python3.11 &lt;base&gt;/prepare_bridge_cutover.py --driver ... validate &lt;bundle&gt; at base commit 6b67114 — reproduce the literal live NO-GO refusal (exit 2)
  • python3.11 tools/bridge-cutover/prepare_bridge_cutover.py --driver tools/bridge-cutover/bridge_cutover_transaction.py validate &lt;bundle&gt; at HEAD — exit 0, cutover_phase: runtime-switched, runtime_switch_ready: false
  • python3.11 tools/bridge-cutover/bridge_worker_state_transaction.py &lt;worker-state manifest&gt; --begin | --verify-provisioned | --finalize at HEAD on the switched machine — exit 0 for all three (6a/6b/6c)
  • python3.11 tools/bridge-cutover/bridge_worker_state_transaction.py &lt;worker-state manifest&gt; --rollback with a drifted codex identity bundle — exit 0, phase: rolled_back, snapshot restored
  • Pinned-refusal matrix via validate: tampered post-cutover registry, full apply without the marked boundary, partially reverted current-link, and candidate registry before adoption — all exit 2 with the post-cutover probe diagnosis preserved
  • git diff &lt;base&gt;..&lt;head&gt; | grep -E &#39;/(Users|home|opt|usr|var)/[^ ]*(agent-fleet|releases)&#39; — no absolute agent-fleet releases paths introduced (intent constraint)
⚠️ **Document** - 1 info
  • ℹ️ docs/bridge-cutover-sealed-runtimes.md:173 - The doc now names the runtime-switched phase but does not state that runtime_switch_ready stays false in it (prepare_bridge_cutover.py:5527). I deliberately did not add this: the guide documents cutover semantics, not validate_bundle's output fields, and no existing doc owns that field — adding it would open a new surface rather than fix a stale fact. Flagging in case you want the flag's contract pinned in prose anyway.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…d machine

The sealed-adoption quiet point accepts the live registry only at the two
pre-cutover hashes, but the documented order applies the runtime switch
(step 4) and boundary (step 5) before worker-state verification (6b) and
finalize (6c), so their transitive bundle re-validation refused the
legitimately advanced machine - the 2026-07-19 live NO-GO - and armed the
rollback-attribution trap for the first published identity bundle.

Keep the strict adoption plan byte-identical and add a pinned fallback:
when it refuses, validate_bundle accepts exactly one further state - main
cutover fully applied with the post-install boundary marked, a fully
sealed adoption journal, intact retained sealed trees, and every
adoption-managed live path at its exact cutover-new identity from the
cutover manifest - reported as the new runtime-switched phase.  Anything
else refuses exactly as before.

Regression tests pin the accepted set (initial, sealed, exact candidate)
and run worker-state 6a/6b/6c and rollback attribution in the exact live
order; pre-fix they reproduce the literal live refusal.
@ruby-dlee
ruby-dlee merged commit b4b1603 into main Jul 20, 2026
5 checks passed
@ruby-dlee
ruby-dlee deleted the claude/workerstate-registry-third-state-9628 branch July 20, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant