fix(mcp): complete merger lease acquire + structured capability resolve (#718, #723)

Harden gitea_acquire_merger_pr_lease with required candidate_head, live-head
match, and fail-closed POST handling. Add capability-map aliases for reviewer
and merger lease acquire/adopt. Make unknown tasks return structured
unknown_task (no ValueError escape). Expand tests for roles, head scoping,
aliases, and unknown_task.

Closes #718. Addresses #723 resolver unknown_task and lease task mapping.
This commit is contained in:
2026-07-17 10:09:36 -04:00
parent 1421fa8568
commit 4f894009f6
4 changed files with 451 additions and 74 deletions
+98 -6
View File
@@ -10004,8 +10004,21 @@ def gitea_acquire_merger_pr_lease(
"permission_report": _permission_block_report("gitea.pr.merge"),
}
head_pin = (candidate_head or "").strip()
if not head_pin:
return {
"success": False,
"acquired": False,
"reasons": [
"candidate_head is required for merger lease acquisition "
"(exact-head scoping; fail closed)"
],
}
try:
_verify_role_mutation_workspace(remote, worktree=worktree, task="acquire_merger_pr_lease")
_verify_role_mutation_workspace(
remote, worktree=worktree, task="acquire_merger_pr_lease"
)
except RuntimeError as e:
return {
"success": False,
@@ -10022,13 +10035,31 @@ def gitea_acquire_merger_pr_lease(
comments = _fetch_pr_comments(
pr_number, remote=remote, host=host, org=org, repo=repo)
pr_live = api_request(
"GET", f"{repo_api_url(h, o, r)}/pulls/{pr_number}", auth) or {}
live_head = (
(pr_live.get("head") or {}).get("sha")
or pr_live.get("head_sha")
or ""
)
live_head = str(live_head).strip()
if live_head and live_head != head_pin:
return {
"success": False,
"acquired": False,
"reasons": [
f"candidate_head {head_pin[:12]}… does not match live PR head "
f"{live_head[:12]}… (exact-head scoping; fail closed)"
],
"live_head": live_head,
"candidate_head": head_pin,
}
pr_merged_or_closed = bool(
pr_live.get("merged") or pr_live.get("merged_at")
) or (str(pr_live.get("state") or "").strip().lower() == "closed")
assessment = reviewer_pr_lease.assess_acquire_lease(
comments,
pr_number=pr_number,
@@ -10038,7 +10069,7 @@ def gitea_acquire_merger_pr_lease(
repo=repo_label,
issue_number=issue_number,
worktree=worktree,
candidate_head=candidate_head,
candidate_head=head_pin,
target_branch=target_branch,
target_branch_sha=target_branch_sha,
pr_merged_or_closed=pr_merged_or_closed,
@@ -10065,6 +10096,16 @@ def gitea_acquire_merger_pr_lease(
):
posted = api_request("POST", comment_url, auth, {"body": body})
if not isinstance(posted, dict) or not posted.get("id"):
return {
"success": False,
"acquired": False,
"reasons": [
"lease comment POST failed or returned no comment id "
"(no partial session lease recorded)"
],
}
session_lease = reviewer_pr_lease.record_session_lease({
"pr_number": pr_number,
"issue_number": issue_number,
@@ -10073,7 +10114,7 @@ def gitea_acquire_merger_pr_lease(
"profile": profile.get("profile_name"),
"worktree": worktree,
"phase": "claimed",
"candidate_head": candidate_head,
"candidate_head": head_pin,
"target_branch": target_branch,
"target_branch_sha": target_branch_sha,
"repo": repo_label,
@@ -10089,6 +10130,8 @@ def gitea_acquire_merger_pr_lease(
"session_id": sid,
"comment_id": posted.get("id"),
"session_lease": session_lease,
"candidate_head": head_pin,
"repo": repo_label,
"reasons": [],
}
@@ -13951,11 +13994,56 @@ def gitea_resolve_task_capability(
TASK_MAP = task_capability_map.TASK_CAPABILITY_MAP
if task not in TASK_MAP:
raise ValueError(f"Unknown task/action: '{task}' (fail closed)")
# #723: structured fail-closed unknown_task (never raise into internal_error).
profile = get_profile()
h = host or (REMOTES.get(remote, {}).get("host") if remote in REMOTES else None)
username = _authenticated_username(h) if h else None
active_role_kind = _profile_role_kind(profile)
switching = gitea_config.is_runtime_switching_enabled()
result = {
"requested_task": task,
"required_operation_permission": "unknown",
"required_role_kind": "unknown",
"active_profile": profile.get("profile_name", "unknown"),
"active_identity": username,
"active_role_kind": active_role_kind,
"active_profile_allowed_operations": profile.get("allowed_operations") or [],
"active_profile_permission_allowed": False,
"allowed_in_current_session": False,
"available_in_session": False,
"configured": False,
"restart_required": False,
"stop_required": True,
"task_role_guidance": [
f"STOP: Unknown task/action: '{task}' (fail closed)"
],
"matching_configured_profile": [],
"runtime_switching_supported": switching,
"different_mcp_namespace_required": False,
"exact_safe_next_action": (
f"None; task '{task}' is unrecognized by the capability map."
),
"reason": f"Unknown task/action: '{task}' (fail closed)",
"reason_code": "unknown_task",
"mutation_performed": False,
}
role_session_router.sync_route_from_capability(result)
was_terminal = capability_stop_terminal.is_active()
terminal = capability_stop_terminal.sync_from_capability_result(result)
if terminal:
result["terminal_mode"] = True
result["terminal_report"] = (
capability_stop_terminal.build_terminal_report(result)
)
elif was_terminal:
result["cleared_stale_denial"] = True
return result
required_permission = task_capability_map.required_permission(task)
required_role = task_capability_map.required_role(task)
role_exclusive_tasks = {
"acquire_reviewer_pr_lease",
"gitea_acquire_reviewer_pr_lease",
"review_pr",
"approve_pr",
"request_changes_pr",
@@ -13963,6 +14051,10 @@ def gitea_resolve_task_capability(
"pr_queue_cleanup",
"pr-queue-cleanup",
"merge_pr",
"acquire_merger_pr_lease",
"gitea_acquire_merger_pr_lease",
"adopt_merger_pr_lease",
"gitea_adopt_merger_pr_lease",
"create_branch",
"push_branch",
"create_pr",