Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""Reconciler profile model for already-landed PR closure (#304).
|
|
|
|
Defines the narrowly scoped operation set for a dedicated reconciler profile
|
|
such as ``prgs-reconciler``. Close MCP tooling and ancestry gates ship in the
|
|
#310 stack; this module validates profile shape only.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gitea_config
|
|
|
|
RECONCILER_REQUIRED_OPERATIONS = (
|
|
"gitea.read",
|
|
"gitea.pr.close",
|
|
)
|
|
|
|
RECONCILER_RECOMMENDED_OPERATIONS = (
|
|
"gitea.pr.comment",
|
|
"gitea.issue.comment",
|
|
"gitea.issue.close",
|
|
)
|
|
|
|
RECONCILER_FORBIDDEN_OPERATIONS = (
|
|
"gitea.pr.approve",
|
|
"gitea.pr.merge",
|
|
"gitea.pr.review",
|
|
"gitea.pr.create",
|
|
"gitea.branch.push",
|
|
"gitea.repo.commit",
|
|
)
|
|
|
|
|
|
def _normalized_allowed(allowed: list[str]) -> set[str]:
|
|
normalized: set[str] = set()
|
|
for entry in allowed or []:
|
|
try:
|
|
normalized.add(gitea_config.normalize_operation(entry))
|
|
except gitea_config.ConfigError:
|
|
continue
|
|
return normalized
|
|
|
|
|
|
def _forbidden_in_allowed(allowed: list[str], ops: tuple[str, ...]) -> list[str]:
|
|
"""Return forbidden ops that are explicitly listed in *allowed*."""
|
|
allowed_n = _normalized_allowed(allowed)
|
|
present: list[str] = []
|
|
for op in ops:
|
|
try:
|
|
if gitea_config.normalize_operation(op) in allowed_n:
|
|
present.append(op)
|
|
except gitea_config.ConfigError:
|
|
continue
|
|
return present
|
|
|
|
|
|
def is_reconciler_profile(allowed: list[str], forbidden: list[str]) -> bool:
|
|
"""Return True when *allowed*/*forbidden* describe a reconciler profile."""
|
|
def can(op: str) -> bool:
|
|
return gitea_config.check_operation(op, allowed, forbidden)[0]
|
|
|
|
if not can("gitea.pr.close"):
|
|
return False
|
|
if _forbidden_in_allowed(allowed, RECONCILER_FORBIDDEN_OPERATIONS):
|
|
return False
|
|
return True
|
|
|
|
|
|
def assess_reconciler_profile(allowed: list[str], forbidden: list[str]) -> dict:
|
|
"""Validate reconciler profile operations (read-only, fail closed)."""
|
|
allowed = list(allowed or [])
|
|
forbidden = list(forbidden or [])
|
|
reasons: list[str] = []
|
|
|
|
for op in RECONCILER_REQUIRED_OPERATIONS:
|
|
ok, _ = gitea_config.check_operation(op, allowed, forbidden)
|
|
if not ok:
|
|
reasons.append(f"missing required operation {op}")
|
|
|
|
for op in _forbidden_in_allowed(allowed, RECONCILER_FORBIDDEN_OPERATIONS):
|
|
reasons.append(f"forbidden operation must not be allowed: {op}")
|
|
|
|
missing_recommended = [
|
|
op for op in RECONCILER_RECOMMENDED_OPERATIONS
|
|
if not gitea_config.check_operation(op, allowed, forbidden)[0]
|
|
]
|
|
|
|
return {
|
|
"is_reconciler_profile": is_reconciler_profile(allowed, forbidden),
|
|
"valid": not reasons and is_reconciler_profile(allowed, forbidden),
|
|
"reasons": reasons,
|
|
"missing_recommended_operations": missing_recommended,
|
|
"required_operations": list(RECONCILER_REQUIRED_OPERATIONS),
|
|
"forbidden_operations": list(RECONCILER_FORBIDDEN_OPERATIONS),
|
|
} |