| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 |
- """Load immutable rule plans and dispatch them through allowlisted adapters."""
- from __future__ import annotations
- import hashlib
- import json
- import logging
- import re
- import time
- from asyncio import CancelledError
- from collections.abc import Mapping
- from typing import Any
- from sqlalchemy import text
- from app.core.common.identifiers import ensure_governance_uid
- from app.core.data_rules.compilers.polars import (
- COMPILER_VERSION as POLARS_COMPILER_VERSION,
- )
- from app.core.data_rules.compilers.polars import (
- validate_bound_polars_plan,
- )
- from app.core.data_rules.compilers.sql import (
- COMPILER_VERSION as SQL_COMPILER_VERSION,
- )
- from app.core.data_rules.compilers.sql import (
- validate_bound_sql_plan,
- )
- from app.runner.nodes import NodeExecutionError
- from app.runner.rule_evidence import validate_public_rule_result
- CONFIG_KEYS = {
- "component_binding_id",
- "rule_version_id",
- "execution_plan_hash",
- "provenance",
- }
- IDEMPOTENCY_STRATEGIES = {
- "partition_replace",
- "upsert",
- "deduplication_key",
- }
- LOGGER = logging.getLogger(__name__)
- def _canonical_hash(value: Any) -> str:
- try:
- canonical = json.dumps(
- value,
- sort_keys=True,
- separators=(",", ":"),
- ensure_ascii=False,
- )
- except (TypeError, ValueError) as exc:
- raise NodeExecutionError(
- "published rule plan is not JSON serializable"
- ) from exc
- return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
- def _uid(value: Any, label: str) -> str:
- try:
- return ensure_governance_uid({"uid": str(value)})
- except ValueError as exc:
- raise NodeExecutionError(f"{label} is invalid") from exc
- class PostgresRulePlanRepository:
- """Read one plan through all immutable binding and version constraints."""
- def __init__(self, engine):
- self.engine = engine
- def load(
- self,
- *,
- component_binding_id: str,
- rule_version_id: str,
- plan_hash: str,
- ) -> dict[str, Any] | None:
- statement = text(
- """
- SELECT
- p.component_binding_id::text AS component_binding_id,
- b.rule_version_id::text AS rule_version_id,
- p.backend,
- p.compiler_version,
- p.plan,
- p.plan_hash,
- p.schema_hashes,
- p.status AS plan_status,
- r.status AS rule_status,
- r.spec_hash AS canonical_rule_spec_hash,
- ins.id::text AS canonical_input_schema_snapshot_id,
- ins.schema_hash AS canonical_input_schema_hash,
- outs.id::text AS canonical_output_schema_snapshot_id,
- outs.schema_hash AS canonical_output_schema_hash,
- b.component_kind,
- b.idempotency AS binding_idempotency
- FROM public.rule_execution_plans p
- JOIN public.dataflow_component_bindings b
- ON b.id = p.component_binding_id
- JOIN public.data_rule_versions r
- ON r.id = b.rule_version_id
- LEFT JOIN public.dataflow_dataset_bindings ib
- ON ib.id = CAST(p.plan->>'input_binding_id' AS uuid)
- LEFT JOIN public.data_schema_snapshots ins
- ON ins.id = ib.schema_snapshot_id
- LEFT JOIN public.dataflow_dataset_bindings ob
- ON ob.id = CAST(p.plan->>'output_binding_id' AS uuid)
- LEFT JOIN public.data_schema_snapshots outs
- ON outs.id = ob.schema_snapshot_id
- WHERE p.component_binding_id = CAST(:component_binding_id AS uuid)
- AND b.rule_version_id = CAST(:rule_version_id AS uuid)
- AND p.plan_hash = :plan_hash
- """
- )
- with self.engine.connect() as connection:
- row = connection.execute(
- statement,
- {
- "component_binding_id": component_binding_id,
- "rule_version_id": rule_version_id,
- "plan_hash": plan_hash,
- },
- ).mappings().one_or_none()
- return dict(row) if row is not None else None
- class RulePlanExecutor:
- """Fail closed unless the exact published plan is still executable."""
- def __init__(
- self,
- repository,
- *,
- adapters: Mapping[str, Any],
- evidence_writer=None,
- ):
- self.repository = repository
- self.adapters = dict(adapters)
- self.evidence_writer = evidence_writer
- @staticmethod
- def _redacted_sample(value: Any) -> list[dict[str, Any]]:
- if not isinstance(value, list):
- return []
- sample = []
- for row in value[:100]:
- if not isinstance(row, dict):
- continue
- sample.append(
- {
- str(key): (
- None if item is None else "[REDACTED]"
- )
- for key, item in sorted(row.items())
- }
- )
- return sample
- def _start_evidence(
- self,
- *,
- component_binding_id: str,
- rule_version_id: str,
- plan_hash: str,
- execution_context: Mapping[str, Any],
- ):
- if self.evidence_writer is None:
- return None, None
- correlation_id = execution_context.get("correlation_id")
- dataflow_uid = execution_context.get("dataflow_uid")
- deployment_id = execution_context.get("deployment_id")
- environment = execution_context.get("environment")
- workflow_version = execution_context.get("workflow_version")
- node_id = execution_context.get("node_id")
- task_jti = execution_context.get("task_jti")
- if (
- not correlation_id
- or not dataflow_uid
- or not deployment_id
- or environment
- not in {"development", "test", "production"}
- or isinstance(workflow_version, bool)
- or not isinstance(workflow_version, int)
- or workflow_version < 1
- or not isinstance(node_id, str)
- or not node_id
- ):
- raise NodeExecutionError(
- "trusted rule evidence context is incomplete"
- )
- try:
- rule_run_id = self.evidence_writer.start(
- component_binding_id=component_binding_id,
- rule_version_id=rule_version_id,
- plan_hash=plan_hash,
- correlation_id=correlation_id,
- dataflow_uid=dataflow_uid,
- deployment_id=deployment_id,
- environment=environment,
- workflow_version=workflow_version,
- node_id=node_id,
- lease_owner=task_jti,
- )
- replay = self.evidence_writer.replay(rule_run_id)
- except Exception as exc:
- LOGGER.exception("governed rule evidence start failed")
- raise NodeExecutionError(
- "rule execution evidence is unavailable"
- ) from exc
- return rule_run_id, replay
- def _finish_evidence(self, rule_run_id, result):
- if rule_run_id is None:
- return
- try:
- self.evidence_writer.finish(rule_run_id, result)
- except Exception as exc:
- raise NodeExecutionError(
- "rule execution evidence commit outcome is unknown",
- commit_outcome="unknown",
- ) from exc
- def replay_task(
- self,
- *,
- node,
- correlation_id,
- deployment_id,
- task_jti,
- ):
- if self.evidence_writer is None:
- return None
- config = node.get("config")
- if not isinstance(config, dict):
- return None
- try:
- replay = self.evidence_writer.replay_by_lease_owner(
- lease_owner=task_jti,
- deployment_id=deployment_id,
- correlation_id=correlation_id,
- component_binding_id=_uid(
- config.get("component_binding_id"),
- "component_binding_id",
- ),
- rule_version_id=_uid(
- config.get("rule_version_id"),
- "rule_version_id",
- ),
- plan_hash=str(
- config.get("execution_plan_hash") or ""
- ),
- )
- except Exception as exc:
- raise NodeExecutionError(
- "rule execution evidence is unavailable"
- ) from exc
- if replay is None or replay.get("status") != "success":
- return None
- return {
- key: value
- for key, value in replay.items()
- if key != "status"
- }
- def execute(
- self,
- node,
- parameters,
- *,
- write_authorized=False,
- **execution_context,
- ):
- if node.get("type") not in {"rule.apply", "quality.check"}:
- raise NodeExecutionError("unsupported governed rule node")
- config = node.get("config")
- if not isinstance(config, dict) or set(config) - CONFIG_KEYS:
- raise NodeExecutionError(
- "governed rule node contains inline or unsupported fields"
- )
- component_binding_id = _uid(
- config.get("component_binding_id"),
- "component_binding_id",
- )
- rule_version_id = _uid(
- config.get("rule_version_id"),
- "rule_version_id",
- )
- plan_hash = str(config.get("execution_plan_hash") or "")
- if not re.fullmatch(r"[0-9a-f]{64}", plan_hash):
- raise NodeExecutionError("execution plan hash is invalid")
- if node.get("type") == "rule.apply":
- idempotency = node.get("idempotency")
- if (
- node.get("purpose") != "write"
- or not write_authorized
- or not isinstance(idempotency, dict)
- or idempotency.get("strategy")
- not in IDEMPOTENCY_STRATEGIES
- or not str(idempotency.get("key") or "").strip()
- ):
- raise NodeExecutionError(
- "governed write authorization and idempotency are required"
- )
- elif node.get("purpose") != "read":
- raise NodeExecutionError("quality check must be read only")
- record = self.repository.load(
- component_binding_id=component_binding_id,
- rule_version_id=rule_version_id,
- plan_hash=plan_hash,
- )
- if not isinstance(record, dict):
- raise NodeExecutionError("published rule plan was not found")
- if (
- record.get("component_binding_id") != component_binding_id
- or record.get("rule_version_id") != rule_version_id
- or record.get("plan_hash") != plan_hash
- or record.get("plan_status") != "published"
- or record.get("rule_status") != "published"
- or _canonical_hash(record.get("plan")) != plan_hash
- ):
- raise NodeExecutionError("published rule plan is not executable")
- if node.get("type") == "rule.apply" and (
- record.get("component_kind") != "rule.apply"
- or record.get("binding_idempotency") != node.get("idempotency")
- ):
- raise NodeExecutionError(
- "governed rule idempotency does not match its binding"
- )
- backend = record.get("backend")
- if backend == "sql_pushdown":
- try:
- plan = validate_bound_sql_plan(record.get("plan"))
- except ValueError as exc:
- raise NodeExecutionError(
- "published rule plan is not executable"
- ) from exc
- expected_schema_hashes = {
- "rule_spec_hash": plan["rule_spec_hash"],
- "input_schema_snapshot_id": plan[
- "input_schema_snapshot_id"
- ],
- "input_schema_hash": plan["input_schema_hash"],
- "output_schema_snapshot_id": plan[
- "output_schema_snapshot_id"
- ],
- "output_schema_hash": plan["output_schema_hash"],
- }
- if (
- record.get("compiler_version") != SQL_COMPILER_VERSION
- or plan["compiler_version"] != SQL_COMPILER_VERSION
- or record.get("schema_hashes") != expected_schema_hashes
- or record.get("canonical_rule_spec_hash")
- != plan["rule_spec_hash"]
- or record.get("canonical_input_schema_snapshot_id")
- != plan["input_schema_snapshot_id"]
- or record.get("canonical_input_schema_hash")
- != plan["input_schema_hash"]
- or record.get("canonical_output_schema_snapshot_id")
- != plan["output_schema_snapshot_id"]
- or record.get("canonical_output_schema_hash")
- != plan["output_schema_hash"]
- ):
- raise NodeExecutionError(
- "published rule plan canonical attestation does not match"
- )
- elif backend == "polars_batch":
- try:
- plan = validate_bound_polars_plan(record.get("plan"))
- except ValueError as exc:
- raise NodeExecutionError(
- "published rule plan is not executable"
- ) from exc
- expected_schema_hashes = {
- "rule_spec_hash": plan["rule_spec_hash"],
- "input_schema_snapshot_id": plan[
- "input_schema_snapshot_id"
- ],
- "input_schema_hash": plan["input_schema_hash"],
- "output_schema_snapshot_id": plan[
- "output_schema_snapshot_id"
- ],
- "output_schema_hash": plan["output_schema_hash"],
- }
- if (
- record.get("compiler_version") != POLARS_COMPILER_VERSION
- or plan["compiler_version"] != POLARS_COMPILER_VERSION
- or record.get("schema_hashes") != expected_schema_hashes
- or record.get("canonical_rule_spec_hash")
- != plan["rule_spec_hash"]
- or record.get("canonical_input_schema_snapshot_id")
- != plan["input_schema_snapshot_id"]
- or record.get("canonical_input_schema_hash")
- != plan["input_schema_hash"]
- or record.get("canonical_output_schema_snapshot_id")
- != plan["output_schema_snapshot_id"]
- or record.get("canonical_output_schema_hash")
- != plan["output_schema_hash"]
- ):
- raise NodeExecutionError(
- "published rule plan canonical attestation does not match"
- )
- adapter = self.adapters.get(backend)
- if adapter is None or not callable(getattr(adapter, "execute", None)):
- raise NodeExecutionError("rule plan backend is not registered")
- adapter_parameters = parameters
- staging_input = None
- if backend == "sql_pushdown" and parameters not in ({}, None):
- if (
- not isinstance(parameters, dict)
- or set(parameters) != {"input_artifact"}
- or not isinstance(parameters["input_artifact"], str)
- ):
- raise NodeExecutionError(
- "SQL rule handoff must contain one opaque receipt"
- )
- staging_input = parameters["input_artifact"]
- adapter_parameters = {}
- rule_run_id, replay = self._start_evidence(
- component_binding_id=component_binding_id,
- rule_version_id=rule_version_id,
- plan_hash=plan_hash,
- execution_context=execution_context,
- )
- if isinstance(replay, dict):
- status = replay.get("status")
- if status == "success":
- return {
- key: value
- for key, value in replay.items()
- if key != "status"
- }
- if status in {"failed", "unknown", "cancelled"}:
- raise NodeExecutionError(
- "rule execution was already finalized",
- commit_outcome=str(
- replay.get("commit_outcome")
- or "not_applicable"
- ),
- )
- raise NodeExecutionError("rule execution is already in progress")
- if self.evidence_writer is not None:
- try:
- self.evidence_writer.heartbeat(
- rule_run_id,
- execution_context.get("task_jti"),
- )
- except Exception as exc:
- raise NodeExecutionError(
- "rule execution lease is unavailable"
- ) from exc
- if staging_input is not None:
- if self.evidence_writer is None:
- raise NodeExecutionError(
- "SQL staging receipt resolver is unavailable"
- )
- try:
- self.evidence_writer.resolve_sql_staging(
- staging_input,
- deployment_id=execution_context.get(
- "deployment_id"
- ),
- correlation_id=execution_context.get(
- "correlation_id"
- ),
- input_binding_id=plan["input_binding_id"],
- )
- except Exception as exc:
- self._finish_evidence(
- rule_run_id,
- {
- "status": "failed",
- "commit_outcome": "not_committed",
- "timings": {"duration_ms": 0},
- },
- )
- raise NodeExecutionError(
- "SQL staging receipt is not executable"
- ) from exc
- adapter_context = {}
- if backend == "polars_batch":
- adapter_context["correlation_id"] = execution_context.get(
- "correlation_id"
- )
- started = time.monotonic()
- try:
- result = adapter.execute(
- plan=record["plan"],
- node=node,
- parameters=adapter_parameters,
- write_authorized=write_authorized,
- **adapter_context,
- )
- except CancelledError:
- self._finish_evidence(
- rule_run_id,
- {
- "status": "cancelled",
- "commit_outcome": "not_applicable",
- "timings": {
- "duration_ms": int(
- (time.monotonic() - started) * 1000
- )
- },
- },
- )
- raise
- except NodeExecutionError as exc:
- self._finish_evidence(
- rule_run_id,
- {
- "status": (
- "unknown"
- if exc.commit_outcome == "unknown"
- else "failed"
- ),
- "commit_outcome": exc.commit_outcome,
- "timings": {
- "duration_ms": int(
- (time.monotonic() - started) * 1000
- )
- },
- },
- )
- raise
- except Exception as exc:
- self._finish_evidence(
- rule_run_id,
- {
- "status": "failed",
- "commit_outcome": "not_committed",
- "timings": {
- "duration_ms": int(
- (time.monotonic() - started) * 1000
- )
- },
- },
- )
- raise NodeExecutionError(
- "rule plan execution failed",
- commit_outcome="not_committed",
- ) from exc
- if not isinstance(result, dict):
- error = NodeExecutionError(
- "rule plan result must be an object"
- )
- self._finish_evidence(
- rule_run_id,
- {
- "status": "failed",
- "commit_outcome": error.commit_outcome,
- "timings": {
- "duration_ms": int(
- (time.monotonic() - started) * 1000
- )
- },
- },
- )
- raise error
- staging_output = None
- if backend == "sql_pushdown" and self.evidence_writer is not None:
- try:
- staging_output = self.evidence_writer.stage_sql_output(
- rule_run_id,
- output_binding_id=plan["output_binding_id"],
- )
- except Exception as exc:
- self._finish_evidence(
- rule_run_id,
- {
- "status": "unknown",
- "commit_outcome": "unknown",
- "timings": {
- "duration_ms": int(
- (time.monotonic() - started) * 1000
- )
- },
- },
- )
- raise NodeExecutionError(
- "SQL staging receipt outcome is unknown",
- commit_outcome="unknown",
- ) from exc
- try:
- sample = self._redacted_sample(
- result.pop("_violation_sample", None)
- )
- public_result = {
- **result,
- "component_binding_id": component_binding_id,
- "rule_version_id": rule_version_id,
- "execution_plan_hash": plan_hash,
- }
- for metric in (
- "rows_in",
- "rows_out",
- "rows_rejected",
- "rows_quarantined",
- ):
- value = public_result.get(metric, 0)
- if isinstance(value, bool):
- raise ValueError("boolean metric")
- public_result[metric] = max(0, int(value))
- artifact_ref = public_result.get("artifact_ref")
- if isinstance(artifact_ref, str):
- public_result["output_artifact"] = artifact_ref
- elif (
- backend == "sql_pushdown"
- and staging_output is not None
- ):
- public_result["output_artifact"] = staging_output
- public_result = validate_public_rule_result(
- public_result,
- backend=backend,
- )
- except (TypeError, ValueError) as exc:
- self._finish_evidence(
- rule_run_id,
- {
- "status": "failed",
- "commit_outcome": "not_committed",
- "timings": {
- "duration_ms": int(
- (time.monotonic() - started) * 1000
- )
- },
- },
- )
- raise NodeExecutionError(
- "rule plan result is invalid",
- commit_outcome="not_committed",
- ) from exc
- evidence_result = {
- "status": "success",
- "rows_in": max(0, int(public_result.get("rows_in", 0))),
- "rows_out": max(0, int(public_result.get("rows_out", 0))),
- "rows_rejected": max(
- 0, int(public_result.get("rows_rejected", 0))
- ),
- "rows_quarantined": max(
- 0, int(public_result.get("rows_quarantined", 0))
- ),
- "commit_outcome": str(
- public_result.get("commit_outcome", "not_applicable")
- ),
- "timings": {
- "duration_ms": int(
- (time.monotonic() - started) * 1000
- )
- },
- "public_result": public_result,
- }
- if sample:
- evidence_result.update(
- {
- "violation_sample": sample,
- "sample_count": len(sample),
- "redaction_policy": (
- "rule-violation-default-v1"
- ),
- }
- )
- self._finish_evidence(rule_run_id, evidence_result)
- return public_result
|