feat: add prgs-reconciler profile model and role detection (Closes #304)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""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),
|
||||
}
|
||||
Reference in New Issue
Block a user