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
+97 -29
View File
@@ -1648,6 +1648,45 @@ def _workspace_repository_slug(remote: str | None) -> str | None:
return session_ctx.format_repository_slug(parsed[0], parsed[1])
def _canonical_repository_slug(
profile: dict,
remote: str | None,
) -> tuple[str | None, list[str]]:
"""Repository identity of the *configured* canonical root, if any (#739 F2).
Returns ``(slug, reasons)``. A namespace with no configured
``canonical_repository_root`` yields ``(None, [])`` so callers keep the
install-derived single-repository default. A configured root that cannot be
resolved to a repository identity yields ``(None, reasons)`` a fail-closed
signal, never a silent fallback to the installation checkout, because that
fallback is exactly what lets a cross-repository namespace validate against
the wrong repository.
Env-over-profile precedence and the existence / git-toplevel / resolvable
remote-identity validation all live in ``crr``.
"""
configured_value, configured_source = crr.configured_canonical_root(
profile, os.environ
)
if not configured_value:
return None, []
assessment = crr.assess_canonical_repository_root(
configured_value=configured_value,
source=configured_source,
expected_slug=None,
process_project_root=PROJECT_ROOT,
remote=remote,
require_binding=True,
)
slug = assessment.get("resolved_slug")
if assessment.get("block") or not slug:
return None, list(assessment.get("reasons") or [
"configured canonical repository root has no resolvable "
"repository identity (fail closed)"
])
return slug, []
def _trusted_session_repository(
profile: dict,
remote: str | None,
@@ -1686,32 +1725,10 @@ def _trusted_session_repository(
# self-authorizing). Env-over-profile precedence and fail-closed validation
# (existence, git toplevel, resolvable remote identity) are handled by
# ``crr``; unconfigured single-repo namespaces keep the install-derived slug.
configured_value, configured_source = crr.configured_canonical_root(
profile, os.environ
)
if configured_value:
assessment = crr.assess_canonical_repository_root(
configured_value=configured_value,
source=configured_source,
expected_slug=None,
process_project_root=PROJECT_ROOT,
remote=remote,
require_binding=True,
)
slug = assessment.get("resolved_slug")
if assessment.get("block") or not slug:
return {
"org": None,
"repository": None,
"reasons": assessment.get("reasons")
or [
"configured canonical repository root has no resolvable "
"repository identity; session repository cannot be "
"authorized (fail closed)"
],
}
else:
slug = _workspace_repository_slug(remote)
canonical_slug, canonical_reasons = _canonical_repository_slug(profile, remote)
if canonical_reasons:
return {"org": None, "repository": None, "reasons": canonical_reasons}
slug = canonical_slug or _workspace_repository_slug(remote)
scope = session_ctx.assess_repository_scope(
workspace_slug=slug,
allowed=allowed,
@@ -8768,8 +8785,28 @@ def _delete_branch_repository_binding_block(
(unproven target).
Returns a structured block dict, or ``None`` when the target is authorized.
#739 F2: the trusted identity is the session's configured
``canonical_repository_root`` when the namespace declares one. Deriving it
from ``_workspace_repository_slug`` alone reads the *installation* checkout
(``_local_git_remote_url`` runs in ``PROJECT_ROOT``, always Gitea-Tools),
which inverts the guard for a cross-repository namespace: the genuinely
bound target is rejected and Gitea-Tools is accepted. A configured root that
cannot be resolved fails closed rather than falling back to the install
identity; unconfigured namespaces keep the install-derived default.
"""
workspace_slug = _workspace_repository_slug(remote)
canonical_slug, canonical_reasons = _canonical_repository_slug(
get_profile(), remote
)
if canonical_reasons:
return {
"success": False,
"performed": False,
"required_permission": "gitea.branch.delete",
"reasons": canonical_reasons,
"blocker_kind": "repository_binding",
}
workspace_slug = canonical_slug or _workspace_repository_slug(remote)
workspace_parts = (
session_ctx.parse_repository_slug(workspace_slug) if workspace_slug else None
)
@@ -14036,6 +14073,16 @@ def gitea_get_runtime_context(
remote: Known instance 'dadeschools' or 'prgs'.
host: Override the Gitea host.
"""
# #739 F1: normalize the remote *before* any host lookup, identity
# resolution, or session seeding. ``remote`` defaults to 'dadeschools'
# across the whole tool surface, so a caller that omits it on a prgs-hosted
# namespace would otherwise pin the argument default: the identity lookup
# below would authenticate against the wrong host, and because first-bind is
# first-write-wins a later, correct ``gitea_whoami(remote="prgs")`` could not
# repair the binding. ``gitea_whoami`` has always normalized here; this is
# the same call, not a new policy. Explicit non-default remotes pass through
# untouched, and a dadeschools-hosted profile still resolves to dadeschools.
remote = _effective_remote(remote)
profile = get_profile()
config = gitea_config.load_config()
reveal = _reveal_endpoints()
@@ -14213,13 +14260,23 @@ def gitea_assess_master_parity(
Never mutates and makes no network calls. Read-only operations are never
blocked by staleness; only mutating operations fail closed while stale.
The top-level ``startup_head``/``current_head``/``in_parity`` fields keep
their original meaning: the *Gitea-Tools server implementation* only. #739
F3 adds two separately labelled dimensions so a cross-repository namespace
can tell them apart ``server_implementation`` (this installation, restated
under an explicit name) and ``target_repository`` (the configured canonical
target checkout and its last-known remote master). Only the server dimension
gates mutations.
Returns:
dict with 'in_parity', 'stale', 'restart_required', 'startup_head',
'current_head', 'mutation_gate_enforced', 'summary', 'reasons', and a
'report' recovery payload when stale.
'current_head', 'mutation_gate_enforced', 'summary', 'reasons',
'server_implementation', 'target_repository', and a 'report' recovery
payload when stale.
"""
parity = _current_master_parity()
enforced = not master_parity_gate.gate_disabled()
canonical_root, canonical_source = _configured_canonical_root()
out = {
"in_parity": parity["in_parity"],
"stale": parity["stale"],
@@ -14231,6 +14288,17 @@ def gitea_assess_master_parity(
"summary": master_parity_gate.format_parity(parity),
"reasons": parity["reasons"],
"process_root": PROJECT_ROOT,
"server_implementation": {
"installation_root": PROJECT_ROOT,
"startup_head": parity["startup_head"],
"current_head": parity["current_head"],
"stale": parity["stale"],
"determinable": parity["determinable"],
},
"target_repository": master_parity_gate.assess_target_repository_parity(
canonical_root=canonical_root,
source=canonical_source,
),
}
if parity["stale"] and enforced:
out["report"] = master_parity_gate.parity_report(parity)