feat(bridge-cutover): refresh runtime-switched bundle identity in place - #15
Merged
Conversation
…y in place A byte-exact reinstall of the provider binary gives it a fresh inode/mtime and re-stales the recorded activation plan and worker-state manifest against the live binary, so validate_bundle's strict identity gates refuse even though the machine is genuinely runtime-switched. A from-scratch rebuild cannot recover: the runtime-switched proof is journal-bound to the applying bundle's own transaction journals, which a rebuild's empty output_dir can neither borrow byte-for-byte nor re-seal against the already-migrated live registry. Add a `refresh` preparer mode that regenerates exactly those two identity artifacts of the existing bundle in place, atomically, from the current provider-binary identity while leaving the sealed cutover and adoption journals untouched. It refuses unless the bundle is provably the applied runtime-switched one (reusing the same journal-bound phase determination as validation) and gates completion on a full strict validate_bundle that must still report runtime-switched, so the strict gates pass only because the recorded identity again equals the live binary - no proof is loosened. Factor the shared bundle reconstruction into _reconstruct_bundle_state so the strict validator and the refresh share one owner for every integrity check up to the two identity gates. Tests prove the refresh realigns identity and revalidates runtime-switched with journals preserved, that it does not loosen the strict gate, and that it refuses a not-yet-applied or unmarked-boundary bundle without mutating it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The developer wanted an automated regression check in the firstmate repository to prevent the synthetic agent-fleet dataclass fixture from drifting from the real Registry, Settings, ProviderConfig, and Profile schemas, while keeping real-package imports out of synthetic pipeline execution paths. They required isolated worktree development, default-branch protection, and shipment through the no-mistakes pipeline. They then requested a worker-state cutover fix so validation accepts the exact fully applied post-cutover runtime state without weakening strict sealed-adoption checks, with pinned refusal tests for tampering and partial states, no provider logins, and tilde-form paths in process command lines. After merging PR #9, they asked for a self-contained handoff document in Downloads so Firstmate could finish the cutover, including the required 6b → browser-free identity adoption → 6c ordering and stopping before step 8.
What Changed
refreshcommand that updates an applied runtime-switched bundle’s activation plan and worker-state manifest to the current provider identity while preserving sealed journals and strict validation.Risk Assessment
✅ Low: Captain, the updated implementation now serializes against worker-state begin, refuses every existing rollback anchor before mutation, and remains retryable after an interrupted artifact pair update.
Testing
The configured full baseline had already succeeded; focused regressions and disposable end-to-end CLI, concurrency, recovery, and refusal checks also passed, with three reviewer-visible transcripts captured and no worktree residue.
Evidence: Bridge refresh CLI transcript
Evidence: Concurrent worker-lock handoff transcript
Evidence: Bound worker-state refusal matrix
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed (2) ✅
tools/bridge-cutover/prepare_bridge_cutover.py:5733- Replacing the worker-state manifest changes its fingerprint without acquiring the worker-state lock or migrating its snapshot/journal. Any transaction already atsnapshottedor later will then fail validation and rollback because both records remain bound to the old fingerprint, contradicting the documented restart behavior and potentially stranding rollback. Lock the worker-state transaction and atomically rebind/reset its recoverable state, or refuse before modifying either artifact.🔧 Fix: Captain, refuse refresh during active worker-state transactions
2 errors still open:
tools/bridge-cutover/prepare_bridge_cutover.py:5746- The two artifact replacements are not recoverable as a unit. If the process exits after replacingbundle.jsonbut before replacing the worker-state manifest, the old manifest'sbundle_sha256no longer matches; the next refresh now fails inload_manifest()before it can repair the partial update. Add a durable refresh journal or exact partial-state recovery path so retry completes this crash window safely.tools/bridge-cutover/prepare_bridge_cutover.py:5716- The new phase check remains a TOCTOU guard because the preparation lock is unrelated to the worker-state lock. A workerbegin()can load the old manifest, start after this reportsnot-started, and publish an old-fingerprint snapshot/journal while refresh replaces the manifest, recreating the stranded rollback state. Preventing this requires cross-process exclusion with worker-state begin; please clarify whether acquiring the existing worker-state lock solely for serialization is acceptable despite the no-locking constraint.🔧 Fix: Captain, serialize and recover in-place worker-state refresh
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
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"Harness-provided baseline: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"PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_prepare_bridge_cutover.PrepareBridgeCutoverTests.test_refresh_updates_identity_and_revalidates_runtime_switched tests.test_prepare_bridge_cutover.PrepareBridgeCutoverTests.test_refresh_does_not_loosen_the_strict_identity_gate tests.test_prepare_bridge_cutover.PrepareBridgeCutoverTests.test_refresh_refuses_bundle_that_is_not_runtime_switched tests.test_prepare_bridge_cutover.PrepareBridgeCutoverTests.test_refresh_refuses_runtime_switch_ready_bundle tests.test_prepare_bridge_cutover.PrepareBridgeCutoverTests.test_refresh_refuses_when_boundary_not_marked tests.test_prepare_bridge_cutover.PrepareBridgeCutoverTests.test_refresh_refuses_active_worker_state_transaction tests.test_prepare_bridge_cutover.PrepareBridgeCutoverTests.test_refresh_repairs_partial_artifact_replacement tests.test_bridge_worker_state_transaction.WorkerStateTransactionTests.test_begin_reloads_manifest_after_lock_acquisitionDisposable end-to-end CLI sequence usingprepare_bridge_cutover.py validate,refresh, andvalidatearound simulated provider-identity drift; verifiedrefreshed: true,valid: true,cutover_phase: runtime-switched, byte-identical sealed journals, and unchanged artifacts after active-transaction refusal.Controlled concurrent lock handoff usingPYTHONDONTWRITEBYTECODE=1 python3 -; verified worker startup blocked on the refresh lock and journaled the refreshed manifest fingerprint rather than its stale pre-lock fingerprint.CLI refusal matrix for worker-state journal, snapshot, and snapshot-staging artifacts; each returned exit 2 and left the bundle and worker manifest unchanged.git status --short --untracked-files=allconfirmed testing left the worktree clean.🔧 **Document** - 1 issue found → auto-fixed ✅
tools/bridge-cutover/prepare_bridge_cutover.py:5735- The refusal message says to return worker-state tonot-started, but the driver preserves its journal and provides no reset action. Correcting this user-facing message requires executable/test changes outside the documentation-only scope.🔧 Fix: Clarify bridge refresh transaction precondition
✅ Re-checked - no issues remain.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.