feat: require exact per-mutation capability proof in reviewer reports (Closes #405)
Adds mutation-capability table validation so review_pr cannot authorize merge or delete mutations without their own exact capability rows. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""Exact per-mutation capability proof verifier for reviewer reports (#405).
|
||||
|
||||
A reviewer final report may prove ``review_pr`` capability and then also merge
|
||||
a PR or delete a remote branch. Merge and branch deletion are separate
|
||||
mutations that require their own exact capability proof — a nearby capability
|
||||
must never authorize a different operation. This verifier requires a
|
||||
mutation-capability table pairing every performed mutation with the exact
|
||||
task/permission resolved *before* that mutation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
# Mutations this verifier tracks, with the exact capability tokens that
|
||||
# authorize each. A row for the mutation must cite one of its own tokens;
|
||||
# tokens from a different mutation (a "nearby capability") never count.
|
||||
_REVIEW_TOKENS = ("review_pr", "gitea.pr.review", "gitea.pr.approve",
|
||||
"gitea.pr.request_changes", "request_changes_pr", "approve_pr")
|
||||
_MERGE_TOKENS = ("merge_pr", "gitea.pr.merge")
|
||||
_DELETE_TOKENS = ("delete_branch", "gitea.branch.delete")
|
||||
|
||||
# Detect that a mutation was actually performed (not merely mentioned as a
|
||||
# non-goal or skipped).
|
||||
_MERGE_PERFORMED = re.compile(
|
||||
r"(?:gitea_merge_pr\b(?![^\n]*\b(?:not called|skipped|blocked)\b)|"
|
||||
r"^\s*[-*]?\s*merge result\s*:\s*merged\b|"
|
||||
r"\bpr merged\b|\bmerge commit\s*(?:sha)?\s*[:=]?\s*[0-9a-f]{7,})",
|
||||
re.IGNORECASE | re.MULTILINE,
|
||||
)
|
||||
_DELETE_PERFORMED = re.compile(
|
||||
r"(?:gitea_delete_branch\b(?![^\n]*\b(?:not called|skipped|blocked)\b)|"
|
||||
r"^\s*[-*]?\s*(?:remote )?branch deleted\s*:|"
|
||||
r"\bdeleted (?:the )?(?:remote )?branch\b|"
|
||||
r"^\s*[-*]?\s*branch deletion\s*:\s*(?!skipped|none|not)\S)",
|
||||
re.IGNORECASE | re.MULTILINE,
|
||||
)
|
||||
_REVIEW_PERFORMED = re.compile(
|
||||
r"(?:gitea_submit_pr_review\b|gitea_mark_final_review_decision\b|"
|
||||
r"^\s*[-*]?\s*review (?:decision|verdict|mutation)\s*:\s*"
|
||||
r"(?:approved|request[_ ]changes)\b|\breview submitted\b)",
|
||||
re.IGNORECASE | re.MULTILINE,
|
||||
)
|
||||
|
||||
# Post-hoc proof: capability resolved *after* the mutation is never valid.
|
||||
_POST_HOC = re.compile(
|
||||
r"capabilit(?:y|ies)\s+(?:resolved|proven|checked)\s+(?:after|post[- ])\s*"
|
||||
r"(?:the\s+)?(?:merge|deletion|delete|mutation|review)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# The report must carry an explicit mutation-capability table.
|
||||
_TABLE_MARKER = re.compile(
|
||||
r"mutation[- ]capability(?:\s+table)?|capability[- ]per[- ]mutation",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _tokens_present(text: str, tokens: tuple[str, ...]) -> bool:
|
||||
low = text.lower()
|
||||
return any(tok.lower() in low for tok in tokens)
|
||||
|
||||
|
||||
def assess_mutation_capability_proof(report_text: str) -> dict:
|
||||
"""Validate exact per-mutation capability proof in a reviewer report.
|
||||
|
||||
Returns ``{proven, block, reasons, safe_next_action}``. A report that
|
||||
performs no mutation beyond an ordinary review passes only when its
|
||||
review capability is cited; merge/delete each demand their own exact
|
||||
capability row. Fail closed on nearby-capability substitution, a
|
||||
missing table, missing rows, or post-hoc proof.
|
||||
"""
|
||||
text = report_text or ""
|
||||
reasons: list[str] = []
|
||||
|
||||
merged = bool(_MERGE_PERFORMED.search(text))
|
||||
deleted = bool(_DELETE_PERFORMED.search(text))
|
||||
reviewed = bool(_REVIEW_PERFORMED.search(text))
|
||||
|
||||
extra_mutation = merged or deleted
|
||||
|
||||
if _POST_HOC.search(text):
|
||||
reasons.append(
|
||||
"capability proof recorded after the mutation; exact capability "
|
||||
"must be resolved before each mutation"
|
||||
)
|
||||
|
||||
# A review-only report needs its review capability cited; no table required.
|
||||
if reviewed and not _tokens_present(text, _REVIEW_TOKENS):
|
||||
reasons.append(
|
||||
"review mutation performed without exact review capability proof "
|
||||
"(review_pr / gitea.pr.review)"
|
||||
)
|
||||
|
||||
if extra_mutation and not _TABLE_MARKER.search(text):
|
||||
reasons.append(
|
||||
"mutation beyond review performed without a mutation-capability "
|
||||
"table (mutation, exact task/capability, result, order-before)"
|
||||
)
|
||||
|
||||
if merged:
|
||||
if not _tokens_present(text, _MERGE_TOKENS):
|
||||
reasons.append(
|
||||
"merge performed without exact merge capability proof "
|
||||
"(merge_pr / gitea.pr.merge); nearby review_pr does not "
|
||||
"authorize merge"
|
||||
)
|
||||
|
||||
if deleted:
|
||||
if not _tokens_present(text, _DELETE_TOKENS):
|
||||
reasons.append(
|
||||
"branch deletion performed without exact delete capability "
|
||||
"proof (delete_branch / gitea.branch.delete); nearby "
|
||||
"merge_pr does not authorize branch deletion"
|
||||
)
|
||||
|
||||
proven = not reasons
|
||||
return {
|
||||
"proven": proven,
|
||||
"block": not proven,
|
||||
"reasons": reasons,
|
||||
"safe_next_action": (
|
||||
"proceed"
|
||||
if proven
|
||||
else "add a mutation-capability table with the exact resolved "
|
||||
"task/permission and pre-mutation order for every mutation; "
|
||||
"skip any mutation whose exact capability is unproven"
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user