"""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