fix(mcp): complete canonical-root consumption for cross-repo namespaces (Closes #739)

#706 routed the #274 filesystem guards and the session repository slug through
the configured canonical_repository_root. Three consumption paths were left
deriving from the Gitea-Tools installation checkout.

F1 — gitea_get_runtime_context did not normalize its `remote` argument, while
gitea_whoami did. On a prgs-hosted namespace whose first native call was the
runtime-context path, the 'dadeschools' argument default was pinned: identity
resolved against the wrong host, and because first-bind is first-write-wins a
later correct gitea_whoami(remote="prgs") could not repair the binding. It now
calls _effective_remote before any host lookup, identity resolution, or session
seeding. Explicit remotes pass through untouched and a dadeschools-hosted
profile still resolves to dadeschools.

F2 — _delete_branch_repository_binding_block derived its expected slug from
_workspace_repository_slug, which reads _local_git_remote_url in PROJECT_ROOT
(always Gitea-Tools). For a cross-repository namespace this inverted the guard:
the genuinely bound target was rejected and Gitea-Tools was accepted. The
canonical-root branch already present in _trusted_session_repository is
extracted as _canonical_repository_slug and shared by both call sites. A
configured-but-unresolvable root now fails closed instead of falling back to
the installation identity; unconfigured namespaces keep the install-derived
default unchanged.

F3 — gitea_assess_master_parity measures Gitea-Tools server implementation
parity only. That is intentional and is preserved: startup_head, current_head,
in_parity, stale, and restart_required keep their existing meaning and values,
and only the server dimension gates mutations. Two separately labelled
dimensions are added — server_implementation (installation root and commit) and
target_repository (canonical root, repository slug, checkout head, last-known
remote master head, staleness) — so cross-repository commissioning evidence can
distinguish them. The target assessment makes no network call: the remote side
is read from the existing remote-tracking ref, and an unfetched target is
reported indeterminate rather than guessed at.

Verified intentional and left unchanged: the #274 workspace-membership,
author-mutation-worktree, and branches-only guards already validate against the
configured canonical root; explicit org/repo may confirm but never authorize a
binding; and the four-role repository-specific configuration surface already
passes audit and bind-time validation with a wrong root failing closed.

Tests: 31 new cases in tests/test_cross_repo_canonical_consumption.py using
real git repositories and no network. Full suite 3298 passed / 6 skipped
against a clean-baseline 3267; the 2 failures
(test_issue_702_review_findings_f1_f6, test_reconciler_supersession_close)
reproduce identically on unmodified master a8d2087 and are pre-existing.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_013jUxaLVLkPHuTQwAzFzztW
This commit is contained in:
2026-07-18 03:44:28 -04:00
co-authored by Claude Opus 4.8
parent a8d2087b4a
commit 15a8a76e99
3 changed files with 926 additions and 29 deletions
+125
View File
@@ -166,3 +166,128 @@ def format_parity(assessment: dict) -> str:
if not assessment.get("determinable"):
return "parity indeterminate (baseline or current HEAD unknown)"
return f"in parity at {_short(assessment.get('current_head'))}"
# ---------------------------------------------------------------------------
# Target-repository parity (#739 F3)
#
# Everything above measures ONE dimension: the Gitea-Tools server's own
# implementation commit, comparing the SHA this process was loaded from against
# the SHA now on disk at PROJECT_ROOT. That is deliberate and is left untouched
# — it is what proves the in-memory capability gates are current.
#
# It is not, however, a statement about the repository a cross-repository
# namespace actually mutates. The assessment below is a separate, separately
# labelled dimension for the configured canonical target repository. It never
# feeds the mutation gate and never changes startup_head/current_head.
# ---------------------------------------------------------------------------
DEFAULT_TARGET_TRACKING_REF = "refs/remotes/origin/master"
def _git_capture(root: str, *args: str) -> str | None:
"""Run a read-only git command in *root*; ``None`` on any failure.
Deliberately does not honour ``GITEA_TEST_CURRENT_HEAD``: that override
exists to pin the *server's* HEAD, and applying it here would make a target
repository silently report the server's forced SHA.
"""
if not root:
return None
try:
res = subprocess.run(
["git", "-C", root, *args],
capture_output=True,
text=True,
check=False,
)
except Exception:
return None
if res.returncode != 0:
return None
return (res.stdout or "").strip() or None
def assess_target_repository_parity(
*,
canonical_root: str | None,
source: str | None,
tracking_ref: str = DEFAULT_TARGET_TRACKING_REF,
) -> dict:
"""Assess the configured cross-repository target checkout.
Reports the target's canonical root, repository identity, checked-out
commit, and last-known remote master commit, plus whether the checkout is
behind that ref. No network call is made: the remote side is read from the
existing remote-tracking ref, so a target that has never been fetched is
reported as indeterminate rather than guessed at.
An unconfigured namespace is ``configured=False`` and never ``stale`` — the
single-repository default has no second dimension to be stale about. A
configured root that cannot be read is ``determinable=False`` with reasons.
"""
result = {
"configured": bool(canonical_root),
"canonical_repository_root": None,
"source": source,
"repository_slug": None,
"checkout_head": None,
"tracking_ref": tracking_ref,
"remote_tracking_head": None,
"determinable": False,
"stale": False,
"reasons": [],
}
if not canonical_root:
result["determinable"] = True
return result
result["canonical_repository_root"] = canonical_root
if not os.path.isdir(canonical_root):
result["reasons"].append(
f"configured canonical repository root '{canonical_root}' "
f"does not exist or is not a directory"
)
return result
toplevel = _git_capture(canonical_root, "rev-parse", "--show-toplevel")
if not toplevel:
result["reasons"].append(
f"configured canonical repository root '{canonical_root}' "
f"is not a git checkout"
)
return result
head = _git_capture(canonical_root, "rev-parse", "HEAD")
if not head:
result["reasons"].append(
f"target repository HEAD could not be read at '{canonical_root}'"
)
return result
result["checkout_head"] = head
result["determinable"] = True
remote_url = _git_capture(canonical_root, "remote", "get-url", "origin")
if remote_url:
# Local import keeps this module dependency-light for its startup role.
import remote_repo_guard
parsed = remote_repo_guard.parse_org_repo_from_remote_url(remote_url)
if parsed:
result["repository_slug"] = f"{parsed[0]}/{parsed[1]}"
if not result["repository_slug"]:
result["reasons"].append(
"target repository identity could not be derived from its git remote"
)
tracking_head = _git_capture(canonical_root, "rev-parse", tracking_ref)
if not tracking_head:
result["reasons"].append(
f"remote-tracking ref '{tracking_ref}' is unknown in the target "
f"checkout; target staleness is indeterminate (no fetch is "
f"performed by this assessment)"
)
return result
result["remote_tracking_head"] = tracking_head
result["stale"] = tracking_head != head
return result