Already-landed open PRs block the review queue: the already-landed gate correctly stops approval/merge, but no task path could close the redundant PR. Add the reconciler close path: - task_capability_map: new reconcile_close_landed_pr (gitea.pr.close) and reconcile_close_landed_issue (gitea.issue.close) tasks under a dedicated "reconciler" role. Exact close capabilities only — the role grants no review, approve, request-changes, or merge authority, and normal reviewer profiles keep no broad PR-close authority. - role_session_router: reconciler tasks route wrong_role_stop in author/reviewer sessions with a dedicated recovery message; matching reconciler sessions route allowed_current_session. - review_proofs.assess_reconciler_close_gate: PR close allowed only when every proof passes — PR live-verified open, candidate head SHA pinned and matching the live head, target branch freshly fetched with a full SHA, head an ancestor of the target, exact gitea.pr.close proven. Non-landed PRs return NOT_LANDED_CLOSE_BLOCKED; missing close capability returns RECOVERY_HANDOFF_REQUIRED; incomplete proof fails closed as GATE_NOT_PROVEN. Linked-issue close is skipped when the issue is already closed and allowed only for an open issue resolved by the landed commits with exact gitea.issue.close capability. The gate also returns the required final-report field list. 17 new tests cover the capability map, wrong-role routing, and every gate path (landed close, non-landed block, stale target, changed head, missing capabilities, linked-issue variants, review-mutation denial). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
141 lines
4.0 KiB
Python
141 lines
4.0 KiB
Python
"""Shared task→permission map for resolver and tool gates (#69).
|
|
|
|
``gitea_resolve_task_capability`` and issue-mutating MCP tools must agree on
|
|
which profile operation each task requires. This module is the single source
|
|
of truth; regression tests assert tool gates cannot drift from it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
TASK_CAPABILITY_MAP: dict[str, dict[str, str]] = {
|
|
"create_issue": {
|
|
"permission": "gitea.issue.create",
|
|
"role": "author",
|
|
},
|
|
"comment_issue": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "author",
|
|
},
|
|
"close_issue": {
|
|
"permission": "gitea.issue.close",
|
|
"role": "author",
|
|
},
|
|
"claim_issue": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "author",
|
|
},
|
|
"mark_issue": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "author",
|
|
},
|
|
"lock_issue": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "author",
|
|
},
|
|
"set_issue_labels": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "author",
|
|
},
|
|
"create_branch": {
|
|
"permission": "gitea.branch.create",
|
|
"role": "author",
|
|
},
|
|
"push_branch": {
|
|
"permission": "gitea.branch.push",
|
|
"role": "author",
|
|
},
|
|
"create_pr": {
|
|
"permission": "gitea.pr.create",
|
|
"role": "author",
|
|
},
|
|
"comment_pr": {
|
|
"permission": "gitea.pr.comment",
|
|
"role": "author",
|
|
},
|
|
"close_pr": {
|
|
"permission": "gitea.pr.close",
|
|
"role": "author",
|
|
},
|
|
"address_pr_change_requests": {
|
|
"permission": "gitea.branch.push",
|
|
"role": "author",
|
|
},
|
|
"review_pr": {
|
|
"permission": "gitea.pr.review",
|
|
"role": "reviewer",
|
|
},
|
|
"merge_pr": {
|
|
"permission": "gitea.pr.merge",
|
|
"role": "reviewer",
|
|
},
|
|
"blind_pr_queue_review": {
|
|
"permission": "gitea.pr.review",
|
|
"role": "reviewer",
|
|
},
|
|
"request_changes_pr": {
|
|
"permission": "gitea.pr.request_changes",
|
|
"role": "reviewer",
|
|
},
|
|
"approve_pr": {
|
|
"permission": "gitea.pr.approve",
|
|
"role": "reviewer",
|
|
},
|
|
"delete_branch": {
|
|
"permission": "gitea.branch.delete",
|
|
"role": "author",
|
|
},
|
|
"commit_files": {
|
|
"permission": "gitea.repo.commit",
|
|
"role": "author",
|
|
},
|
|
"gitea_commit_files": {
|
|
"permission": "gitea.repo.commit",
|
|
"role": "author",
|
|
},
|
|
"reconcile_merged_cleanups": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
# #309: dedicated reconciler path for already-landed open PRs. Exact
|
|
# close capabilities only — never review/approve/request_changes/merge.
|
|
"reconcile_close_landed_pr": {
|
|
"permission": "gitea.pr.close",
|
|
"role": "reconciler",
|
|
},
|
|
"reconcile_close_landed_issue": {
|
|
"permission": "gitea.issue.close",
|
|
"role": "reconciler",
|
|
},
|
|
}
|
|
|
|
# Issue-mutating MCP tools and their resolver task keys.
|
|
ISSUE_MUTATION_TOOL_TASKS: dict[str, str] = {
|
|
"gitea_create_issue": "create_issue",
|
|
"gitea_close_issue": "close_issue",
|
|
"gitea_create_issue_comment": "comment_issue",
|
|
"gitea_mark_issue": "mark_issue",
|
|
"gitea_set_issue_labels": "set_issue_labels",
|
|
"gitea_commit_files": "commit_files",
|
|
}
|
|
|
|
|
|
def required_permission(task: str) -> str:
|
|
"""Return the canonical operation a *task* requires (fail closed)."""
|
|
try:
|
|
return TASK_CAPABILITY_MAP[task]["permission"]
|
|
except KeyError as exc:
|
|
raise KeyError(f"Unknown task/action: {task!r} (fail closed)") from exc
|
|
|
|
|
|
def required_role(task: str) -> str:
|
|
"""Return author/reviewer role kind for *task* (fail closed)."""
|
|
try:
|
|
return TASK_CAPABILITY_MAP[task]["role"]
|
|
except KeyError as exc:
|
|
raise KeyError(f"Unknown task/action: {task!r} (fail closed)") from exc
|
|
|
|
|
|
def tool_required_permission(tool_name: str) -> str:
|
|
"""Return the operation an issue-mutating tool must gate on."""
|
|
return required_permission(ISSUE_MUTATION_TOOL_TASKS[tool_name])
|