Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
292 lines
11 KiB
Python
292 lines
11 KiB
Python
"""Admission policy for ACT-ADR-007 bounded operations.
|
|
|
|
Definitions may select only code-registered operations. This module validates
|
|
their safety envelope during markdown parsing and exposes immutable runtime
|
|
metadata to the workflow. It never accepts a command or import path from a
|
|
definition.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Any, Iterable
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BoundedOperationSpec:
|
|
operation_id: str
|
|
source_type: str
|
|
query: str
|
|
resolve_before_execute: bool
|
|
max_timeout_seconds: int
|
|
temporal_max_attempts: int
|
|
idempotency: str
|
|
credential_route: str
|
|
evidence_mode: str
|
|
|
|
|
|
_SPECS = (
|
|
BoundedOperationSpec(
|
|
operation_id="sbom_nexus_ingest",
|
|
source_type="sbom-nexus",
|
|
query="catch_up",
|
|
resolve_before_execute=True,
|
|
max_timeout_seconds=900,
|
|
temporal_max_attempts=10,
|
|
idempotency="activity-run-id + repository; heartbeat acknowledged outcomes",
|
|
credential_route="activity-core-sbom-nexus",
|
|
evidence_mode="instruction-report:sbom_catchup",
|
|
),
|
|
BoundedOperationSpec(
|
|
operation_id="forgejo_package_prune",
|
|
source_type="shell",
|
|
query="forgejo_package_prune",
|
|
resolve_before_execute=False,
|
|
max_timeout_seconds=900,
|
|
temporal_max_attempts=1,
|
|
idempotency="single Temporal attempt; operator reconciles ambiguous failure",
|
|
credential_route="forgejo-admin-api-token",
|
|
evidence_mode="context-evidence-sink",
|
|
),
|
|
BoundedOperationSpec(
|
|
operation_id="cnpg_option_a_backup",
|
|
source_type="shell",
|
|
query="cnpg_option_a_backup",
|
|
resolve_before_execute=False,
|
|
max_timeout_seconds=7200,
|
|
temporal_max_attempts=1,
|
|
idempotency="single Temporal attempt; platform backup receipt is authoritative",
|
|
credential_route="railiance-cnpg-option-a-backup",
|
|
evidence_mode="context-evidence-sink",
|
|
),
|
|
)
|
|
|
|
BOUNDED_OPERATION_REGISTRY = {spec.operation_id: spec for spec in _SPECS}
|
|
_BY_SOURCE_QUERY = {(spec.source_type, spec.query): spec for spec in _SPECS}
|
|
|
|
# The generic ``shell`` adapter is retained only as a compatibility namespace
|
|
# for these known read-only queries. Adding a name here requires code review;
|
|
# definition data cannot supply an arbitrary command.
|
|
READ_ONLY_SHELL_QUERIES = frozenset(
|
|
{
|
|
"reuse_surface_report_gaps",
|
|
"discover_kaizen_scheduled_repos",
|
|
"discover_kaizen_projects",
|
|
}
|
|
)
|
|
|
|
_CANONICAL_SCRIPT_PATHS = {
|
|
"forgejo_package_prune": "/opt/railiance-platform/tools/cmd/forgejo-package-prune",
|
|
"cnpg_option_a_backup": "/opt/railiance-platform/tools/cmd/cnpg-option-a-backup",
|
|
}
|
|
_TARGET_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
|
|
|
|
def operation_spec_for_source(source: dict[str, Any]) -> BoundedOperationSpec | None:
|
|
return _BY_SOURCE_QUERY.get((str(source.get("type") or ""), str(source.get("query") or "")))
|
|
|
|
|
|
def operation_sources(
|
|
context_sources: Iterable[dict[str, Any]],
|
|
) -> list[tuple[dict[str, Any], BoundedOperationSpec]]:
|
|
found: list[tuple[dict[str, Any], BoundedOperationSpec]] = []
|
|
for source in context_sources:
|
|
spec = operation_spec_for_source(source)
|
|
if spec is not None:
|
|
found.append((source, spec))
|
|
return found
|
|
|
|
|
|
def pending_operation_value(spec: BoundedOperationSpec) -> dict[str, str]:
|
|
return {"operation": spec.operation_id, "status": "pending"}
|
|
|
|
|
|
def normalize_operation_result(operation_id: str, raw: Any) -> dict[str, Any]:
|
|
"""Project subprocess output to the operation's non-secret evidence shape."""
|
|
result = raw if isinstance(raw, dict) else {}
|
|
if operation_id == "forgejo_package_prune":
|
|
return {
|
|
"kind": operation_id,
|
|
"apply": bool(result.get("apply")),
|
|
"candidate_count": _safe_count(result.get("candidate_count")),
|
|
"deleted_count": _safe_count(result.get("deleted_count")),
|
|
"skipped_protected_count": _safe_count(
|
|
result.get("skipped_protected_count")
|
|
),
|
|
"error_count": len(result.get("errors") or [])
|
|
if isinstance(result.get("errors"), list)
|
|
else 0,
|
|
}
|
|
if operation_id == "cnpg_option_a_backup":
|
|
return {
|
|
"kind": operation_id,
|
|
"overall": str(result.get("overall") or "unknown")[:40],
|
|
"dry_run": bool(result.get("dry_run")),
|
|
"dumped": _safe_count(result.get("dumped")),
|
|
"uploaded": _safe_count(result.get("uploaded")),
|
|
"failed": _safe_count(result.get("failed")),
|
|
"script_exit_code": _safe_count(result.get("script_exit_code")),
|
|
}
|
|
raise ValueError(f"no result projection for bounded operation {operation_id!r}")
|
|
|
|
|
|
def _safe_count(value: Any) -> int:
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
return 0
|
|
return max(0, value)
|
|
|
|
|
|
def validate_bounded_operations(
|
|
context_sources: list[dict[str, Any]],
|
|
instructions: list[dict[str, Any]],
|
|
) -> None:
|
|
"""Fail closed on unknown shell queries and incomplete operation envelopes."""
|
|
found = operation_sources(context_sources)
|
|
if len(found) > 1:
|
|
names = ", ".join(spec.operation_id for _, spec in found)
|
|
raise ValueError(
|
|
"an ActivityDefinition may declare at most one bounded operation; "
|
|
f"found: {names}"
|
|
)
|
|
|
|
for source in context_sources:
|
|
source_type = str(source.get("type") or "")
|
|
query = str(source.get("query") or "")
|
|
spec = operation_spec_for_source(source)
|
|
if source_type == "shell" and spec is None and query not in READ_ONLY_SHELL_QUERIES:
|
|
raise ValueError(
|
|
f"shell query {query!r} is not registered as read-only or as a "
|
|
"bounded operation"
|
|
)
|
|
if spec is None:
|
|
if source.get("operation") is not None:
|
|
raise ValueError(
|
|
f"context source {source_type!r}/{query!r} declares unknown "
|
|
f"operation {source.get('operation')!r}"
|
|
)
|
|
continue
|
|
|
|
declared = source.get("operation")
|
|
if declared != spec.operation_id:
|
|
raise ValueError(
|
|
f"context source {source_type!r}/{query!r} must declare "
|
|
f"operation: {spec.operation_id}"
|
|
)
|
|
params = source.get("params")
|
|
if not isinstance(params, dict):
|
|
raise ValueError(f"bounded operation {spec.operation_id} params must be a mapping")
|
|
_validate_timeout(spec, params)
|
|
|
|
if spec.operation_id == "sbom_nexus_ingest":
|
|
_validate_sbom(params, instructions)
|
|
elif spec.operation_id == "forgejo_package_prune":
|
|
_validate_prune(params)
|
|
elif spec.operation_id == "cnpg_option_a_backup":
|
|
_validate_backup(params)
|
|
|
|
|
|
def _validate_timeout(spec: BoundedOperationSpec, params: dict[str, Any]) -> None:
|
|
raw = params.get("timeout_seconds", spec.max_timeout_seconds)
|
|
if isinstance(raw, bool) or not isinstance(raw, (int, float)):
|
|
raise ValueError(f"bounded operation {spec.operation_id} timeout_seconds must be numeric")
|
|
if raw <= 0 or raw > spec.max_timeout_seconds:
|
|
raise ValueError(
|
|
f"bounded operation {spec.operation_id} timeout_seconds must be in "
|
|
f"1..{spec.max_timeout_seconds}"
|
|
)
|
|
|
|
|
|
def _explicit_bool(params: dict[str, Any], field: str, operation_id: str) -> bool:
|
|
if field not in params or not isinstance(params[field], bool):
|
|
raise ValueError(
|
|
f"bounded operation {operation_id} must declare boolean {field} explicitly"
|
|
)
|
|
return bool(params[field])
|
|
|
|
|
|
def _validate_sbom(
|
|
params: dict[str, Any],
|
|
instructions: list[dict[str, Any]],
|
|
) -> None:
|
|
_explicit_bool(params, "apply", "sbom_nexus_ingest")
|
|
limit = params.get("limit")
|
|
if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 3:
|
|
raise ValueError("bounded operation sbom_nexus_ingest limit must be in 1..3")
|
|
if not _has_instruction_evidence(instructions, "sbom_catchup"):
|
|
raise ValueError(
|
|
"bounded operation sbom_nexus_ingest requires an instruction report "
|
|
"sink with event_type: sbom_catchup"
|
|
)
|
|
|
|
|
|
def _validate_prune(params: dict[str, Any]) -> None:
|
|
apply = _explicit_bool(params, "apply", "forgejo_package_prune")
|
|
max_versions = params.get("max_versions")
|
|
if (
|
|
isinstance(max_versions, bool)
|
|
or not isinstance(max_versions, int)
|
|
or not 1 <= max_versions <= 10
|
|
):
|
|
raise ValueError("bounded operation forgejo_package_prune max_versions must be in 1..10")
|
|
_validate_canonical_script(params, "prune_script", "forgejo_package_prune")
|
|
if apply and not str(params.get("live_images_file") or "").strip():
|
|
raise ValueError(
|
|
"bounded operation forgejo_package_prune apply=true requires live_images_file"
|
|
)
|
|
_require_context_evidence(params, "forgejo_package_prune")
|
|
|
|
|
|
def _validate_backup(params: dict[str, Any]) -> None:
|
|
_explicit_bool(params, "dry_run", "cnpg_option_a_backup")
|
|
_validate_canonical_script(params, "backup_script", "cnpg_option_a_backup")
|
|
raw_targets = params.get("targets")
|
|
if not isinstance(raw_targets, str):
|
|
raise ValueError("bounded operation cnpg_option_a_backup requires explicit targets")
|
|
targets = [item.strip() for item in raw_targets.split(",") if item.strip()]
|
|
if not targets or len(targets) > 10 or any(_TARGET_RE.fullmatch(item) is None for item in targets):
|
|
raise ValueError(
|
|
"bounded operation cnpg_option_a_backup targets must contain 1..10 safe names"
|
|
)
|
|
_require_context_evidence(params, "cnpg_option_a_backup")
|
|
|
|
|
|
def _validate_canonical_script(
|
|
params: dict[str, Any], field: str, operation_id: str
|
|
) -> None:
|
|
expected = _CANONICAL_SCRIPT_PATHS[operation_id]
|
|
if params.get(field) != expected:
|
|
raise ValueError(
|
|
f"bounded operation {operation_id} {field} must be the canonical path {expected!r}"
|
|
)
|
|
|
|
|
|
def _require_context_evidence(params: dict[str, Any], operation_id: str) -> None:
|
|
sinks = params.get("evidence_sinks")
|
|
if not isinstance(sinks, list) or not any(
|
|
isinstance(item, dict)
|
|
and item.get("type") == "state-hub-progress"
|
|
and item.get("event_type") == operation_id
|
|
for item in sinks
|
|
):
|
|
raise ValueError(
|
|
f"bounded operation {operation_id} requires a state-hub-progress "
|
|
f"evidence sink with event_type: {operation_id}"
|
|
)
|
|
|
|
|
|
def _has_instruction_evidence(
|
|
instructions: list[dict[str, Any]], event_type: str
|
|
) -> bool:
|
|
for instruction in instructions:
|
|
sinks = instruction.get("report_sinks")
|
|
if not isinstance(sinks, list):
|
|
continue
|
|
if any(
|
|
isinstance(sink, dict)
|
|
and sink.get("type") == "state-hub-progress"
|
|
and sink.get("event_type") == event_type
|
|
for sink in sinks
|
|
):
|
|
return True
|
|
return False
|