feat(#496): fail-closed canonical comment validation before Gitea posts

Add canonical_comment_validator and wire it into issue comments, PR review
bodies, reconcile post_comment, and structured comment helpers. Workflow-changing
comments without complete next-action state are rejected before any API call.
Includes unit/MCP integration tests, runbook docs, and a final-report rule
blocking false "comment posted" claims when validation failed.
This commit is contained in:
2026-07-08 22:46:22 -04:00
parent 9a2e585a9e
commit 84d921a52f
7 changed files with 855 additions and 0 deletions
+65
View File
@@ -194,6 +194,23 @@ _PAGINATION_PROOF_RE = re.compile(
re.IGNORECASE,
)
_GIT_FETCH_RE = re.compile(r"\bgit\s+fetch\b", re.IGNORECASE)
_CANONICAL_VALIDATION_REJECTED_RE = re.compile(
r"canonical comment validation failed|"
r"canonical_comment_validation|"
r'"allowed"\s*:\s*false',
re.IGNORECASE,
)
_COMMENT_POSTED_CLAIM_RE = re.compile(
r"(?:issue comments posted\s*:\s+(?!none\b)\S|"
r"pr comments posted\s*:\s+(?!none\b)\S|"
r"comment_id\s*[:=]\s*\d+|"
r"gitea comment (?:was )?posted|"
r"posted (?:issue|pr|canonical) (?:state )?comment|"
r"comment posted successfully|"
r"mcp/gitea mutations\s*:\s*[^;\n]*comment posted|"
r"reconciliation mutations\s*:\s*[^;\n]*comment posted)",
re.IGNORECASE,
)
_READONLY_DIAG_RE = re.compile(
r"read[- ]only diagnostics\s*:\s*(.+)$",
re.IGNORECASE | re.MULTILINE,
@@ -348,6 +365,43 @@ def _rule_shared_email_disclosure(report_text: str) -> list[dict[str, str]]:
)
def _rule_shared_canonical_comment_post_claim(
report_text: str,
*,
action_log: list[dict] | None = None,
) -> list[dict[str, str]]:
"""#496: reports must not claim comment posted when validator rejected."""
text = report_text or ""
rejected_in_report = bool(_CANONICAL_VALIDATION_REJECTED_RE.search(text))
rejected_in_log = False
if action_log:
for entry in action_log:
validation = entry.get("canonical_comment_validation") or {}
if validation.get("allowed") is False:
rejected_in_log = True
break
result = entry.get("result") or {}
nested = result.get("canonical_comment_validation") or {}
if nested.get("allowed") is False:
rejected_in_log = True
break
if not (rejected_in_report or rejected_in_log):
return []
if not _COMMENT_POSTED_CLAIM_RE.search(text):
return []
return [
validator_finding(
"shared.canonical_comment_post_claim",
"block",
"MCP/Gitea mutations",
"report claims a Gitea comment was posted while canonical comment "
"validation rejected the workflow comment",
"do not claim comment posted when validator fail-closed; repair "
"the canonical comment body and retry posting",
)
]
def _rule_reviewer_legacy_workspace_mutations(
report_text: str,
*,
@@ -1279,10 +1333,15 @@ _SHARED_CLEANUP_PROOF_RULES = (
_rule_shared_mcp_native_cleanup_proof,
)
_SHARED_CANONICAL_COMMENT_RULES = (
_rule_shared_canonical_comment_post_claim,
)
_RULES_BY_TASK: dict[str, list[Callable[..., list[dict[str, str]]]]] = {
"review_pr": [
_rule_shared_controller_handoff,
_rule_shared_email_disclosure,
*_SHARED_CANONICAL_COMMENT_RULES,
*_SHARED_ISSUE_LOCK_RULES,
_rule_reviewer_legacy_workspace_mutations,
_rule_reviewer_vague_mutations_none,
@@ -1311,6 +1370,7 @@ _RULES_BY_TASK: dict[str, list[Callable[..., list[dict[str, str]]]]] = {
"reconcile_already_landed": [
_rule_reconcile_controller_handoff,
_rule_shared_email_disclosure,
*_SHARED_CANONICAL_COMMENT_RULES,
*_SHARED_ISSUE_LOCK_RULES,
*_SHARED_CLEANUP_PROOF_RULES,
_rule_reconcile_stale_author_fields,
@@ -1326,12 +1386,14 @@ _RULES_BY_TASK: dict[str, list[Callable[..., list[dict[str, str]]]]] = {
"author_issue": [
_rule_shared_controller_handoff,
_rule_shared_email_disclosure,
*_SHARED_CANONICAL_COMMENT_RULES,
*_SHARED_ISSUE_LOCK_RULES,
_rule_reviewer_vague_mutations_none,
],
"work_issue": [
_rule_shared_controller_handoff,
_rule_shared_email_disclosure,
*_SHARED_CANONICAL_COMMENT_RULES,
*_SHARED_ISSUE_LOCK_RULES,
_rule_shared_issue_acceptance_gate,
_rule_reviewer_vague_mutations_none,
@@ -1341,17 +1403,20 @@ _RULES_BY_TASK: dict[str, list[Callable[..., list[dict[str, str]]]]] = {
"issue_filing": [
_rule_shared_controller_handoff,
_rule_shared_email_disclosure,
*_SHARED_CANONICAL_COMMENT_RULES,
*_SHARED_ISSUE_LOCK_RULES,
],
"inventory": [
_rule_shared_controller_handoff,
_rule_shared_email_disclosure,
*_SHARED_CANONICAL_COMMENT_RULES,
*_SHARED_ISSUE_LOCK_RULES,
_rule_reconcile_pagination_proof,
],
"issue_selection": [
_rule_shared_controller_handoff,
_rule_shared_email_disclosure,
*_SHARED_CANONICAL_COMMENT_RULES,
*_SHARED_ISSUE_LOCK_RULES,
],
}