"""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])