fix(mcp): forward explicit org/repo into lease/review anti-stomp

_verify_role_mutation_workspace dropped org/repo, so anti-stomp resolved bare
remote=prgs to Timesheet and failed closed wrong_repo on Gitea-Tools. Forward
explicit org/repo for merger/reviewer lease acquire, adopt, review, merge, and
lease release. Add regression for merger acquire forwarding.

Fixes #718
Refs #723
This commit is contained in:
2026-07-17 13:30:52 -04:00
parent 29c2cf2ef6
commit 83bc7246ff
2 changed files with 76 additions and 7 deletions
+45 -7
View File
@@ -1282,12 +1282,18 @@ def _verify_role_mutation_workspace(
worktree_path: str | None = None, worktree_path: str | None = None,
worktree: str | None = None, worktree: str | None = None,
task: str | None = None, task: str | None = None,
org: str | None = None,
repo: str | None = None,
) -> str: ) -> str:
"""Bind reviewer/merger mutations to the active namespace workspace (#510). """Bind reviewer/merger mutations to the active namespace workspace (#510).
#683: must NOT early-return solely because pytest/unittest is loaded. #683: must NOT early-return solely because pytest/unittest is loaded.
Production workspace binding always runs; test isolation uses explicit Production workspace binding always runs; test isolation uses explicit
env fixtures / force-on flags, never a production short-circuit here. env fixtures / force-on flags, never a production short-circuit here.
org/repo are forwarded into anti-stomp (#604) so callers that pass explicit
repository targets (e.g. prgs + Gitea-Tools when REMOTES defaults to
Timesheet) do not fail closed with wrong_repo after a successful resolve.
""" """
# Check running runtimes to prevent stale mutations # Check running runtimes to prevent stale mutations
@@ -1350,7 +1356,13 @@ def _verify_role_mutation_workspace(
) )
) )
resolved = assessment["mutation_workspace"] resolved = assessment["mutation_workspace"]
verify_preflight_purity(remote, worktree_path=resolved, task=task) verify_preflight_purity(
remote,
worktree_path=resolved,
task=task,
org=org,
repo=repo,
)
return resolved return resolved
@@ -4521,7 +4533,11 @@ def _evaluate_pr_review_submission(
) -> dict: ) -> dict:
"""Shared gate chain for live submit and dry-run review tools.""" """Shared gate chain for live submit and dry-run review tools."""
_verify_role_mutation_workspace( _verify_role_mutation_workspace(
remote, worktree_path=worktree_path, task="review_pr" remote,
worktree_path=worktree_path,
task="review_pr",
org=org,
repo=repo,
) )
action = (action or "").strip().lower() action = (action or "").strip().lower()
workflow_blockers = _review_workflow_load_gate_reasons() if live else [] workflow_blockers = _review_workflow_load_gate_reasons() if live else []
@@ -7619,7 +7635,11 @@ def gitea_merge_pr(
available. Never secrets. available. Never secrets.
""" """
_verify_role_mutation_workspace( _verify_role_mutation_workspace(
remote, worktree_path=worktree_path, task="merge_pr" remote,
worktree_path=worktree_path,
task="merge_pr",
org=org,
repo=repo,
) )
allowed = get_profile()["allowed_operations"] allowed = get_profile()["allowed_operations"]
forbidden = get_profile()["forbidden_operations"] forbidden = get_profile()["forbidden_operations"]
@@ -10652,8 +10672,14 @@ def gitea_acquire_reviewer_pr_lease(
# task=acquire_reviewer_pr_lease so verify_preflight_purity runs shared #604 # task=acquire_reviewer_pr_lease so verify_preflight_purity runs shared #604
# anti-stomp for the declared lease-acquire mutation inventory entry. # anti-stomp for the declared lease-acquire mutation inventory entry.
# Forward explicit org/repo so anti-stomp does not default bare remote=prgs
# to Timesheet when the local git remote is Gitea-Tools (#604 wrong_repo).
_verify_role_mutation_workspace( _verify_role_mutation_workspace(
remote, worktree=worktree, task="acquire_reviewer_pr_lease" remote,
worktree=worktree,
task="acquire_reviewer_pr_lease",
org=org,
repo=repo,
) )
h, o, r = _resolve(remote, host, org, repo) h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h) auth = _auth(h)
@@ -10794,7 +10820,11 @@ def gitea_acquire_merger_pr_lease(
try: try:
_verify_role_mutation_workspace( _verify_role_mutation_workspace(
remote, worktree=worktree, task="acquire_merger_pr_lease" remote,
worktree=worktree,
task="acquire_merger_pr_lease",
org=org,
repo=repo,
) )
except RuntimeError as e: except RuntimeError as e:
return { return {
@@ -10987,7 +11017,11 @@ def gitea_adopt_merger_pr_lease(
# task=adopt_merger_pr_lease so verify_preflight_purity runs shared #604 anti-stomp. # task=adopt_merger_pr_lease so verify_preflight_purity runs shared #604 anti-stomp.
_verify_role_mutation_workspace( _verify_role_mutation_workspace(
remote, worktree=worktree, task="adopt_merger_pr_lease" remote,
worktree=worktree,
task="adopt_merger_pr_lease",
org=org,
repo=repo,
) )
h, o, r = _resolve(remote, host, org, repo) h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h) auth = _auth(h)
@@ -11709,7 +11743,11 @@ def gitea_release_reviewer_pr_lease(
dict reporting release status. dict reporting release status.
""" """
_verify_role_mutation_workspace( _verify_role_mutation_workspace(
remote, worktree=worktree, task="review_pr" remote,
worktree=worktree,
task="review_pr",
org=org,
repo=repo,
) )
h, o, r = _resolve(remote, host, org, repo) h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h) auth = _auth(h)
+31
View File
@@ -145,6 +145,37 @@ class TestMergerLeaseAcquireTool(unittest.TestCase):
self.assertIn("Branches-only mutation guard", res["reasons"][0]) self.assertIn("Branches-only mutation guard", res["reasons"][0])
self.assertIsNone(leases._SESSION_LEASE) self.assertIsNone(leases._SESSION_LEASE)
@patch("gitea_mcp_server.api_request")
@patch("gitea_mcp_server._auth", return_value="token pass")
@patch("gitea_mcp_server._resolve", return_value=("gitea.prgs.cc", "Scaled-Tech-Consulting", "Gitea-Tools"))
@patch("gitea_mcp_server.get_profile")
@patch("gitea_mcp_server._fetch_pr_comments", return_value=[])
@patch("gitea_mcp_server._verify_role_mutation_workspace")
def test_acquire_merger_forwards_explicit_org_repo_to_workspace_verify(
self, mock_verify, _comments, mock_profile, _resolve, _auth, mock_api
):
"""Explicit org/repo must reach anti-stomp (prgs bare default is Timesheet)."""
mock_profile.return_value = self._merger_profile()
mock_api.side_effect = [
{"state": "open", "head": {"sha": LIVE_HEAD}},
{"id": 789},
]
with patch.dict(os.environ, {"GITEA_MCP_PROFILE_NAME": "prgs-merger"}):
res = mcp_server.gitea_acquire_merger_pr_lease(
pr_number=718,
worktree="branches/issue-718",
session_id="merger-session",
candidate_head=HEAD,
remote="prgs",
org="Scaled-Tech-Consulting",
repo="Gitea-Tools",
)
self.assertTrue(res["success"], res)
kwargs = mock_verify.call_args.kwargs
self.assertEqual(kwargs.get("org"), "Scaled-Tech-Consulting")
self.assertEqual(kwargs.get("repo"), "Gitea-Tools")
self.assertEqual(kwargs.get("task"), "acquire_merger_pr_lease")
@patch("gitea_mcp_server.get_profile") @patch("gitea_mcp_server.get_profile")
def test_acquire_merger_requires_candidate_head(self, mock_profile): def test_acquire_merger_requires_candidate_head(self, mock_profile):
mock_profile.return_value = self._merger_profile() mock_profile.return_value = self._merger_profile()