Prevent merger-local empty decision locks from standing in for reviewer terminal cleanup, refuse silent re-init overwrite of unresolved terminal evidence, record post-merge recovery-required state when audit fails, and add a truthful irrecoverable-provenance path that never claims applied=true. Closes #709
347 lines
10 KiB
Python
347 lines
10 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_label": {
|
|
"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": "merger",
|
|
},
|
|
# #695 AC8: controller quarantine of contaminated formal reviews.
|
|
# Apply path posts an append-only forensic audit comment (pr.comment).
|
|
"quarantine_contaminated_review": {
|
|
"permission": "gitea.pr.comment",
|
|
"role": "reconciler",
|
|
},
|
|
"gitea_quarantine_contaminated_review": {
|
|
"permission": "gitea.pr.comment",
|
|
"role": "reconciler",
|
|
},
|
|
"adopt_merger_pr_lease": {
|
|
"permission": "gitea.pr.comment",
|
|
"role": "reviewer",
|
|
},
|
|
# #691: guarded non-owner cleanup of obsolete comment-backed reviewer leases.
|
|
# Apply path posts lease release + audit comments (gitea.pr.comment).
|
|
"cleanup_obsolete_reviewer_comment_lease": {
|
|
"permission": "gitea.pr.comment",
|
|
"role": "reviewer",
|
|
},
|
|
"gitea_cleanup_obsolete_reviewer_comment_lease": {
|
|
"permission": "gitea.pr.comment",
|
|
"role": "reviewer",
|
|
},
|
|
"blind_pr_queue_review": {
|
|
"permission": "gitea.pr.review",
|
|
"role": "reviewer",
|
|
},
|
|
"pr_queue_cleanup": {
|
|
"permission": "gitea.pr.review",
|
|
"role": "reviewer",
|
|
},
|
|
"pr-queue-cleanup": {
|
|
"permission": "gitea.pr.review",
|
|
"role": "reviewer",
|
|
},
|
|
"request_changes_pr": {
|
|
"permission": "gitea.pr.request_changes",
|
|
"role": "reviewer",
|
|
},
|
|
"approve_pr": {
|
|
"permission": "gitea.pr.approve",
|
|
"role": "reviewer",
|
|
},
|
|
# #594: clear durable #332 decision lock only when last terminal PR is
|
|
# already merged/closed (moot). Apply path requires reviewer review
|
|
# permission; assessment itself uses gitea.read inside the tool.
|
|
"cleanup_stale_review_decision_lock": {
|
|
"permission": "gitea.pr.review",
|
|
"role": "reviewer",
|
|
},
|
|
"gitea_cleanup_stale_review_decision_lock": {
|
|
"permission": "gitea.pr.review",
|
|
"role": "reviewer",
|
|
},
|
|
# #709: truthful absence-of-proof recovery record (not applied cleanup).
|
|
"record_irrecoverable_decision_lock_provenance": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "reconciler",
|
|
},
|
|
"gitea_record_irrecoverable_decision_lock_provenance": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "reconciler",
|
|
},
|
|
"delete_branch": {
|
|
"permission": "gitea.branch.delete",
|
|
"role": "author",
|
|
},
|
|
"cleanup_merged_pr_branch": {
|
|
"permission": "gitea.branch.delete",
|
|
"role": "reconciler",
|
|
},
|
|
"commit_files": {
|
|
"permission": "gitea.repo.commit",
|
|
"role": "author",
|
|
},
|
|
"gitea_commit_files": {
|
|
"permission": "gitea.repo.commit",
|
|
"role": "author",
|
|
},
|
|
"reconcile_merged_cleanups": {
|
|
"permission": "gitea.read",
|
|
"role": "reconciler",
|
|
},
|
|
"reconciliation_cleanup": {
|
|
"permission": "gitea.branch.delete",
|
|
"role": "reconciler",
|
|
},
|
|
"work_issue": {
|
|
"permission": "gitea.pr.create",
|
|
"role": "author",
|
|
},
|
|
"work-issue": {
|
|
"permission": "gitea.pr.create",
|
|
"role": "author",
|
|
},
|
|
# #600: controller-owned allocator — any authenticated profile may call;
|
|
# routing enforces role match to selected work. Uses control-plane DB (#613).
|
|
"allocate_next_work": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_allocate_next_work": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
|
|
# #601 first-class lease lifecycle — inspect/list need read; mutations gate on
|
|
# ownership in the control-plane DB (not a separate Gitea write permission).
|
|
"list_workflow_leases": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_list_workflow_leases": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"inspect_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_inspect_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"adopt_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_adopt_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"release_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_release_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"expire_workflow_leases": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_expire_workflow_leases": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"abandon_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_abandon_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"reclaim_expired_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_reclaim_expired_workflow_lease": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
# #612 incident bridge — reconcile uses create_issue for apply;
|
|
# dry-run needs read only. Tools gate apply paths themselves.
|
|
"observability_reconcile_incident": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_observability_reconcile_incident": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"observability_list_projects": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_observability_list_projects": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"observability_link_issue": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"gitea_observability_link_issue": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
|
|
|
|
"reconcile_landed_pr": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"reconcile-landed-pr": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"reconcile_already_landed_pr": {
|
|
"permission": "gitea.pr.close",
|
|
"role": "reconciler",
|
|
},
|
|
# #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",
|
|
},
|
|
"reconcile_close_superseded_pr": {
|
|
"permission": "gitea.pr.close",
|
|
"role": "reconciler",
|
|
},
|
|
"reconcile_close_satisfied_issue": {
|
|
"permission": "gitea.issue.close",
|
|
"role": "reconciler",
|
|
},
|
|
"reconcile_create_followup_issue": {
|
|
"permission": "gitea.issue.create",
|
|
"role": "reconciler",
|
|
},
|
|
"post_heartbeat": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "author",
|
|
},
|
|
"reconcile_issue_claims": {
|
|
"permission": "gitea.read",
|
|
"role": "author",
|
|
},
|
|
"cleanup_stale_claims": {
|
|
"permission": "gitea.issue.comment",
|
|
"role": "author",
|
|
},
|
|
}
|
|
|
|
# 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_create_label": "create_label",
|
|
"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])
|