fix(bridge-cutover): unblock preparer planning against real git-backed projects - #7
Merged
Merged
Conversation
The Bridge preparer invokes the sealed planning APIs under a pure-planning guard that refuses process execution; projects.py's git rev-parse subprocess tripped it for any real git-backed trusted project, hard-blocking the live cutover at prepare (2026-07-19). Replace _git_path with pure filesystem Git discovery mirroring git's own setup semantics: validate_headref readlink handling, gitfile/commondir realpath resolution, is_git_directory checks, one-filesystem ascent, and the dubious-ownership refusal, verified against real git across every reviewed layout. The version stays 0.2.0: the Bridge exactness lattice pins the candidate at 0.2.0/contract-2, and 0.2.0 was never tagged or adopted, so the fixed content becomes the release that binding names. Add the offline gates the incident exposed as missing: an agent-fleet test running the sealed planning APIs under a mirror of the preparer guard against a real git-backed trusted project, and a bridge-suite gate loading the real package through prepare's own loader under the real guard.
prepare's _lexical_candidate_registry constructed the candidate Registry
without config_path, so the real sealed provision API refused provision_plan
('loaded registry path is unavailable for managed hooks') as soon as the git
canonicalization blocker was fixed; the synthetic provision fixtures never
modeled that requirement.
Thread the spec's live registry through prepare's two construction sites,
derive it in the worker-state driver from the cutover manifest's registry
operation so plan verification rebuilds hook commands against the exact path
runtime hooks embed, align the synthetic Registry model with the real schema,
and make the synthetic provision_plan enforce the same refusal so every
existing gate now guards the contract.
…istry binding invariants
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
Fix the Bridge cutover preparer NO-GO from the 2026-07-19 live attempt: the sealed pure-planning guard in tools/bridge-cutover/prepare_bridge_cutover.py refuses process execution, and two real defects blocked prepare against any real git-backed trusted project. (1) agent-fleet's projects.py:_git_path shelled to 'git rev-parse' inside registered_trusted_projects, so closed_claude_state_payload tripped the guard. Deliberate fix: replace the subprocess with pure-Python git discovery mirroring git's own setup semantics - validate_headref readlink handling for symlinked HEADs, gitfile/commondir pointer resolution via strict realpath, is_git_directory HEAD/objects/refs checks, per-level .git-entry-then-bare ordering, one-filesystem ascent, git's dubious-ownership refusal on the discovered git dir, and a 1MB gitfile size cap. The guard itself is deliberately untouched. Parity was verified empirically against real git across 18 layouts including linked worktrees, relative/symlinked pointers, bare repos, detached and symlinked HEADs; deliberate fail-closed micro-divergences on hand-corrupted layouts are documented in the discovery docstring. (2) prepare's _lexical_candidate_registry built the candidate Registry without config_path, which the real provision_plan requires to compose managed hook commands - thread spec.live_registry through both prepare call sites, and in bridge_worker_state_transaction.py derive the live registry path from the cutover manifest's RegistryOperation so worker verification rebuilds hook commands against the exact path runtime hooks embed. The agent-fleet version deliberately stays 0.2.0: the Bridge exactness lattice (preparer, builder, worker-state driver, gate fixtures) pins candidate 0.2.0/contract-2 and 0.2.0 was never tagged, released, or adopted anywhere, so the fixed content becomes what that binding names; uv.lock churn is from an intermediate bump that was reverted. New regression gates close the synthetic-vs-real gap the incident exposed: an agent-fleet unit test runs the sealed planning APIs (closed_claude_state_payload, provision_plan, identity_bundle_path) under an exact mirror of the preparer's guard against a real git-backed trusted project; a bridge-suite gate (tests/test_prepare_bridge_cutover.py RealAgentFleetPurePlanningTest) loads the REAL agent_fleet package through prepare's own loader under the REAL guard; the synthetic Registry fixture gains config_path aligning with the real schema and the synthetic provision_plan now enforces the same pathless-registry refusal as the real API; and new project-bootstrap tests pin detached-HEAD registration, symlinked-HEAD readlink semantics in both accept and refuse directions, commondir trailing-space refusal, gitfile-pointer-through-symlink physical resolution, lexical-escape refusal, and bare/inside-gitdir/broken-gitfile refusals. Full verification already run locally: agent-fleet suite green, bridge control-plane suite 206/206, opt-in real-input module 2/2 with the fixed wheel, sealed v3 release rebuilt deterministically (builds=2) from commit d793ebb, and the real prepare and validate now produce a valid cutover bundle (GO) against the migrated live registry baseline.
What Changed
agent_fleet.projects._git_path'sgit rev-parsesubprocess with pure-filesystem Git discovery (_discover_git_worktree), mirroring git's setup semantics:validate_headrefreadlink/symref handling, gitfile andcommondirpointer resolution through strictrealpath,is_git_directoryHEAD/objects/refs checks, per-level.git-entry-then-bare ordering, one-filesystem ascent, dubious-ownership refusal on the discovered git dir, and a 1MB gitfile size cap — soclosed_claude_state_payloadno longer trips the preparer's pure-planning guard, which is left untouched.Registry:_lexical_candidate_registrynow takes aconfig_pathargument supplied by both prepare call sites fromspec.live_registry, andbridge_worker_state_transaction._candidate_apiderives it from the cutover manifest's singleRegistryOperation(erroring if there isn't exactly one) so worker verification rebuilds managed hook commands against the path runtime hooks embed rather than the bundled registry copy's path.RealAgentFleetPurePlanningTestloads the realagent_fleetpackage through prepare's own loader and runs the sealed planning APIs under the real guard against a git-backed trusted project; the syntheticRegistryfixture gainsconfig_pathand itsprovision_plannow raises the same pathless-registry refusal as the real API; and newtest_project_bootstrapcases pin detached/symlinked HEAD handling in both directions,commondirtrailing-space refusal, gitfile-pointer-through-symlink resolution, and lexical-escape / bare / inside-gitdir / broken-gitfile refusals.docs/bridge-cutover-sealed-runtimes.mdrecords both invariants.Risk Assessment
✅ Low: The change is well-bounded and thoroughly regression-tested with empirical git-parity verification; the pure-Python discovery is consistently fail-closed and the config_path/live-registry threading is correct across all call sites, leaving only two info-level parity/documentation nits.
Testing
Completed 1 recorded test check.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
tools/agent-fleet/src/agent_fleet/projects.py:110-_git_common_dirstrips trailing\rviaraw.rstrip(b"\r\n"), but git's commondir reader (strbuf_getline_lf) strips only the trailing\n. This contradicts the line's own comment ("Git trims only trailing newline bytes here; other whitespace stays significant") and is an undocumented fail-OPEN micro-divergence: a hand-corrupted commondir with CRLF (e.g."../..\r\n") is refused by git (the..\rcomponent fails objects/refs access) but accepted here after the\ris stripped. Practical impact is near-nil (requires write access to an already current-user-owned git dir, which git never writes with CRLF), but it diverges in the unsafe direction, unlike the deliberate fail-closed divergences the discovery docstring enumerates. Note the samerstrip(b"\r\n")in_read_gitfile(line 155) IS correct there, since git'sread_gitfile_gentlystrips both\nand\r. Change torstrip(b"\n")to match git and the comment.tools/agent-fleet/src/agent_fleet/projects.py:196- The comment "Git treats a malformed or dangling .git file as fatal, never skippable" is inaccurate for one case: a regular.gitfile that does NOT start withgitdir:(READ_GITFILE_ERR_NOT_A_FILE) is skippable in git — git falls through to the.git-directory / bare checks and then ASCENDS to a parent worktree, rather than dying. The code here correctly fail-closes (raisesrefused()), so behavior is safe (strictly more refusing); only the comment misdescribes git and could mislead a future maintainer reasoning about parity. Git is only truly fatal (GIT_DIR_INVALID) when the file DOES start withgitdir:but is dangling/oversized/not-a-repo.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"tests/test_prepare_bridge_cutover.py:73- Follow-up, out of scope for a docs pass: tests/test_prepare_bridge_cutover.py builds its synthetic agent_fleet module by hand-copying the real agent_fleet.models schema (Registry, Settings, ProviderConfig, Profile) as source text. That hand-copy is exactly what let config_path drift out of the fixture and produce the 2026-07-19 preparer NO-GO. This change closes the planning-purity half of the gap (RealAgentFleetPurePlanningTest drives the real package under the real guard) and re-aligns config_path, but the remaining fixture schema is still hand-maintained with no drift check against the authoritative dataclasses. Worth a dedicated gate that asserts the synthetic model fields equal the real ones, so the next field added to Registry cannot silently diverge again.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.