Complete canonical-root consumption for cross-repository MCP namespaces #739

Open
opened 2026-07-18 02:35:19 -05:00 by jcwalker3 · 1 comment
Owner

Problem

#706 (landed via PR #736, master a8d2087b) introduced an immutable, configured canonical_repository_root and routed two things through it: the #274 filesystem guards (namespace_workspace_binding.resolve_namespace_mutation_context) and the session repository slug (_trusted_session_repository).

Three consumption paths were not completed. Each is reproduced by an executable test against the production entry point on a8d2087b; none is hypothetical.

F1 — gitea_get_runtime_context does not normalize its remote argument

gitea_whoami calls remote = _effective_remote(remote) before seeding the session context (gitea_mcp_server.py:13405). _effective_remote (gitea_mcp_server.py:2349-2363) is the only place a profile's own configured remote is consulted: when the caller left the argument at its "dadeschools" default, it rewrites the remote from the active profile's base_url host.

gitea_get_runtime_context (gitea_mcp_server.py:14026-14197) never calls it. The raw argument flows into the host lookup at 14064 and 14127 and straight into _seed_session_context at 14128-14134.

Consequences when the runtime-context path is the first native call in a fresh prgs-* process:

  • the session pins remote="dadeschools" / host="gitea.dadeschools.net";
  • _authenticated_username(h) at 14064 resolves identity against the wrong host;
  • first-bind is first-write-wins (session_context_binding.py:218), so a later, correct gitea_whoami(remote="prgs") cannot repair it — it returns the already-bound context and the mutation gate then reports remote drift against a binding that was wrong from the start.

Every existing first-bind test passes remote explicitly (tests/test_issue_714_production_first_bind.py:149-154, tests/test_runtime_clarity.py:118,150,174), so the gap was invisible.

Ordering advice ("call whoami first") is not a durable fix: the production path must be correct regardless of call order.

F2 — the delete-branch repository-binding guard derives identity from the installation checkout

_delete_branch_repository_binding_block (gitea_mcp_server.py:8749-8807) computes its expected slug from _workspace_repository_slug(remote) (line 8772), which reads _local_git_remote_url with cwd=PROJECT_ROOT (gitea_mcp_server.py:8484-8498) — always the Gitea-Tools installation checkout.

_trusted_session_repository already branches on the canonical root first and only falls back to the install-derived slug (gitea_mcp_server.py:1689-1714); its own comment names this hazard. The delete path never reaches that resolver: it calls neither _mutation_config_authority_block nor _session_context_mutation_block.

For a namespace whose canonical_repository_root points at another repository this inverts the guard:

  • an explicit target naming the genuinely bound canonical repository is rejected;
  • an explicit target naming Gitea-Tools is accepted.

No test in tests/test_issue_733_delete_branch_repo_forwarding.py or tests/test_delete_branch_capability.py configures a canonical root while exercising this guard.

F3 — no target-repository dimension in commissioning evidence

gitea_assess_master_parity computes startup_head and current_head from PROJECT_ROOT only (gitea_mcp_server.py:1799, 10871-10874; master_parity_gate.py:35-68). This is intentional and must be preserved — it detects that the in-memory capability-gate code is stale relative to the on-disk Gitea-Tools checkout, exactly as master_parity_gate.py:1-21 documents.

The gap is that it is the only dimension. A cross-repository namespace has no evidence about the repository it actually mutates, so "in parity" reads as a whole-system statement when it is a statement about one checkout. Commissioning evidence must separately distinguish: server installation root and Gitea-Tools implementation commit; canonical target repository root; target checkout commit; target remote/master commit; and staleness per dimension.

Confirmed intentional — not defects

Recorded here so they are not re-filed:

  • Repository/remote guard. assess_workspace_repo_membership, assess_author_mutation_worktree, and the branches-only guard all validate against ctx["canonical_repo_root"], which is the configured canonical root when set (namespace_workspace_binding.py:144-148). The canonical target root resolves to its own slug, the installation root stays Gitea-Tools, explicit org/repo may confirm but never authorize a binding (session_context_binding.py:495-520), and a Gitea-Tools-rooted namespace cannot reach another repository. Verified, not changed.
  • Configuration surface. A four-role repository-specific profile set (author/reviewer/merger/reconciler) with allowed_repositories limited to one repository, an absolute canonical_repository_root, and credential references only already passes config audit and bind-time validation; a wrong root fails closed; role separation holds in both directions. Verified, not changed.

Acceptance criteria

  1. gitea_get_runtime_context normalizes remote through _effective_remote before any host lookup, identity resolution, or session seeding; explicit arguments are unchanged and a dadeschools-hosted profile still resolves to dadeschools.
  2. The delete-branch repository-binding guard derives its expected identity from the session's configured canonical repository root, falling back to the install-derived slug only when no canonical root is configured.
  3. A configured-but-unresolvable canonical root fails the delete-branch guard closed rather than silently falling back to the installation identity.
  4. No request parameter can establish or override canonical repository identity on any of these paths.
  5. gitea_assess_master_parity reports separately labelled server-implementation and target-repository dimensions. startup_head, current_head, in_parity, stale, and restart_required keep their existing meaning and values.
  6. Regression tests cover each path, including the negative and unconfigured cases, using real git repositories and no network calls.
  7. The single-repository default is unchanged for every existing Gitea-Tools namespace.

Scope

Gitea-Tools server source and tests only. No client launcher, profiles.json, or credential changes; no branch is deleted by any test.

## Problem #706 (landed via PR #736, master `a8d2087b`) introduced an immutable, configured `canonical_repository_root` and routed two things through it: the #274 filesystem guards (`namespace_workspace_binding.resolve_namespace_mutation_context`) and the session repository slug (`_trusted_session_repository`). Three consumption paths were not completed. Each is reproduced by an executable test against the production entry point on `a8d2087b`; none is hypothetical. ### F1 — `gitea_get_runtime_context` does not normalize its `remote` argument `gitea_whoami` calls `remote = _effective_remote(remote)` before seeding the session context (`gitea_mcp_server.py:13405`). `_effective_remote` (`gitea_mcp_server.py:2349-2363`) is the only place a profile's own configured remote is consulted: when the caller left the argument at its `"dadeschools"` default, it rewrites the remote from the active profile's `base_url` host. `gitea_get_runtime_context` (`gitea_mcp_server.py:14026-14197`) never calls it. The raw argument flows into the host lookup at 14064 and 14127 and straight into `_seed_session_context` at 14128-14134. Consequences when the runtime-context path is the first native call in a fresh `prgs-*` process: * the session pins `remote="dadeschools"` / `host="gitea.dadeschools.net"`; * `_authenticated_username(h)` at 14064 resolves identity against the wrong host; * first-bind is first-write-wins (`session_context_binding.py:218`), so a later, correct `gitea_whoami(remote="prgs")` cannot repair it — it returns the already-bound context and the mutation gate then reports remote drift against a binding that was wrong from the start. Every existing first-bind test passes `remote` explicitly (`tests/test_issue_714_production_first_bind.py:149-154`, `tests/test_runtime_clarity.py:118,150,174`), so the gap was invisible. Ordering advice ("call whoami first") is not a durable fix: the production path must be correct regardless of call order. ### F2 — the delete-branch repository-binding guard derives identity from the installation checkout `_delete_branch_repository_binding_block` (`gitea_mcp_server.py:8749-8807`) computes its expected slug from `_workspace_repository_slug(remote)` (line 8772), which reads `_local_git_remote_url` with `cwd=PROJECT_ROOT` (`gitea_mcp_server.py:8484-8498`) — always the Gitea-Tools installation checkout. `_trusted_session_repository` already branches on the canonical root first and only falls back to the install-derived slug (`gitea_mcp_server.py:1689-1714`); its own comment names this hazard. The delete path never reaches that resolver: it calls neither `_mutation_config_authority_block` nor `_session_context_mutation_block`. For a namespace whose `canonical_repository_root` points at another repository this inverts the guard: * an explicit target naming the genuinely bound canonical repository is **rejected**; * an explicit target naming Gitea-Tools is **accepted**. No test in `tests/test_issue_733_delete_branch_repo_forwarding.py` or `tests/test_delete_branch_capability.py` configures a canonical root while exercising this guard. ### F3 — no target-repository dimension in commissioning evidence `gitea_assess_master_parity` computes `startup_head` and `current_head` from `PROJECT_ROOT` only (`gitea_mcp_server.py:1799`, `10871-10874`; `master_parity_gate.py:35-68`). **This is intentional and must be preserved** — it detects that the in-memory capability-gate code is stale relative to the on-disk Gitea-Tools checkout, exactly as `master_parity_gate.py:1-21` documents. The gap is that it is the *only* dimension. A cross-repository namespace has no evidence about the repository it actually mutates, so "in parity" reads as a whole-system statement when it is a statement about one checkout. Commissioning evidence must separately distinguish: server installation root and Gitea-Tools implementation commit; canonical target repository root; target checkout commit; target remote/master commit; and staleness per dimension. ## Confirmed intentional — not defects Recorded here so they are not re-filed: * **Repository/remote guard.** `assess_workspace_repo_membership`, `assess_author_mutation_worktree`, and the branches-only guard all validate against `ctx["canonical_repo_root"]`, which is the configured canonical root when set (`namespace_workspace_binding.py:144-148`). The canonical target root resolves to its own slug, the installation root stays Gitea-Tools, explicit `org`/`repo` may confirm but never authorize a binding (`session_context_binding.py:495-520`), and a Gitea-Tools-rooted namespace cannot reach another repository. Verified, not changed. * **Configuration surface.** A four-role repository-specific profile set (author/reviewer/merger/reconciler) with `allowed_repositories` limited to one repository, an absolute `canonical_repository_root`, and credential *references* only already passes config audit and bind-time validation; a wrong root fails closed; role separation holds in both directions. Verified, not changed. ## Acceptance criteria 1. `gitea_get_runtime_context` normalizes `remote` through `_effective_remote` before any host lookup, identity resolution, or session seeding; explicit arguments are unchanged and a dadeschools-hosted profile still resolves to `dadeschools`. 2. The delete-branch repository-binding guard derives its expected identity from the session's configured canonical repository root, falling back to the install-derived slug only when no canonical root is configured. 3. A configured-but-unresolvable canonical root fails the delete-branch guard closed rather than silently falling back to the installation identity. 4. No request parameter can establish or override canonical repository identity on any of these paths. 5. `gitea_assess_master_parity` reports separately labelled server-implementation and target-repository dimensions. `startup_head`, `current_head`, `in_parity`, `stale`, and `restart_required` keep their existing meaning and values. 6. Regression tests cover each path, including the negative and unconfigured cases, using real git repositories and no network calls. 7. The single-repository default is unchanged for every existing Gitea-Tools namespace. ## Scope Gitea-Tools server source and tests only. No client launcher, `profiles.json`, or credential changes; no branch is deleted by any test.
jcwalker3 added the status:readytype:bug labels 2026-07-18 02:35:19 -05:00
jcwalker3 added status:in-progress and removed status:ready labels 2026-07-18 02:41:22 -05:00
Author
Owner

Issue claim heartbeat

<!-- gitea-issue-claim-heartbeat:v1 --> **Issue claim heartbeat** - kind: claim - issue: #739 - branch: feat/issue-739-canonical-root-consumption - phase: claimed - profile: prgs-author - pr: none - blocker: none - next_action: create worktree and begin implementation
jcwalker3 added status:pr-open and removed status:in-progress labels 2026-07-18 02:45:54 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#739