|
|
@@ -7,6 +7,7 @@ import json
|
|
|
import os
|
|
|
import re
|
|
|
import tempfile
|
|
|
+import uuid
|
|
|
from contextlib import suppress
|
|
|
from typing import Any
|
|
|
|
|
|
@@ -39,6 +40,75 @@ _FINISH_KEYS = {
|
|
|
"sample_count",
|
|
|
"redaction_policy",
|
|
|
}
|
|
|
+_PUBLIC_RESULT_KEYS = {
|
|
|
+ "affected_rows",
|
|
|
+ "artifact_ref",
|
|
|
+ "commit_outcome",
|
|
|
+ "component_binding_id",
|
|
|
+ "digest",
|
|
|
+ "execution_plan_hash",
|
|
|
+ "expires_at",
|
|
|
+ "output_artifact",
|
|
|
+ "row_count",
|
|
|
+ "rows_aggregated",
|
|
|
+ "rows_deduplicated",
|
|
|
+ "rows_filtered",
|
|
|
+ "rows_in",
|
|
|
+ "rows_join_dropped",
|
|
|
+ "rows_out",
|
|
|
+ "rows_quarantined",
|
|
|
+ "rows_rejected",
|
|
|
+ "rule_version_id",
|
|
|
+ "schema_hash",
|
|
|
+ "violation_count",
|
|
|
+ "violations",
|
|
|
+}
|
|
|
+_ATTESTATION_RESULT_KEYS = {
|
|
|
+ "component_binding_id",
|
|
|
+ "execution_plan_hash",
|
|
|
+ "rule_version_id",
|
|
|
+}
|
|
|
+_BACKEND_PUBLIC_RESULT_KEYS = {
|
|
|
+ "sql_pushdown": _ATTESTATION_RESULT_KEYS
|
|
|
+ | {
|
|
|
+ "commit_outcome",
|
|
|
+ "output_artifact",
|
|
|
+ "rows_in",
|
|
|
+ "rows_out",
|
|
|
+ "rows_quarantined",
|
|
|
+ "rows_rejected",
|
|
|
+ },
|
|
|
+ "polars_batch": _ATTESTATION_RESULT_KEYS
|
|
|
+ | {
|
|
|
+ "artifact_ref",
|
|
|
+ "commit_outcome",
|
|
|
+ "digest",
|
|
|
+ "expires_at",
|
|
|
+ "output_artifact",
|
|
|
+ "row_count",
|
|
|
+ "rows_aggregated",
|
|
|
+ "rows_deduplicated",
|
|
|
+ "rows_filtered",
|
|
|
+ "rows_in",
|
|
|
+ "rows_join_dropped",
|
|
|
+ "rows_out",
|
|
|
+ "rows_quarantined",
|
|
|
+ "rows_rejected",
|
|
|
+ "schema_hash",
|
|
|
+ "violation_count",
|
|
|
+ "violations",
|
|
|
+ },
|
|
|
+ "quality_check": _ATTESTATION_RESULT_KEYS
|
|
|
+ | {
|
|
|
+ "commit_outcome",
|
|
|
+ "rows_in",
|
|
|
+ "rows_out",
|
|
|
+ "rows_quarantined",
|
|
|
+ "rows_rejected",
|
|
|
+ "violation_count",
|
|
|
+ "violations",
|
|
|
+ },
|
|
|
+}
|
|
|
|
|
|
|
|
|
def _uid(value: Any, label: str) -> str:
|
|
|
@@ -48,6 +118,13 @@ def _uid(value: Any, label: str) -> str:
|
|
|
raise ValueError(f"{label} is invalid") from exc
|
|
|
|
|
|
|
|
|
+def _uuid(value: Any, label: str) -> str:
|
|
|
+ try:
|
|
|
+ return str(uuid.UUID(str(value)))
|
|
|
+ except (TypeError, ValueError, AttributeError) as exc:
|
|
|
+ raise ValueError(f"{label} is invalid") from exc
|
|
|
+
|
|
|
+
|
|
|
def _canonical_digest(value: Any) -> str:
|
|
|
encoded = json.dumps(
|
|
|
value,
|
|
|
@@ -80,6 +157,55 @@ def _sample_fields(sample: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
]
|
|
|
|
|
|
|
|
|
+def validate_public_rule_result(
|
|
|
+ value: Any,
|
|
|
+ *,
|
|
|
+ backend: str | None = None,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ allowed_keys = (
|
|
|
+ _BACKEND_PUBLIC_RESULT_KEYS.get(backend)
|
|
|
+ if backend is not None
|
|
|
+ else _PUBLIC_RESULT_KEYS
|
|
|
+ )
|
|
|
+ if (
|
|
|
+ not isinstance(value, dict)
|
|
|
+ or allowed_keys is None
|
|
|
+ or set(value) - allowed_keys
|
|
|
+ or any(str(key).startswith("_") for key in value)
|
|
|
+ or len(
|
|
|
+ json.dumps(
|
|
|
+ value,
|
|
|
+ sort_keys=True,
|
|
|
+ separators=(",", ":"),
|
|
|
+ ensure_ascii=False,
|
|
|
+ ).encode("utf-8")
|
|
|
+ )
|
|
|
+ > 32_768
|
|
|
+ ):
|
|
|
+ raise ValueError("public rule result is not evidence safe")
|
|
|
+ for key, item in value.items():
|
|
|
+ if key == "violations":
|
|
|
+ if not isinstance(item, list) or len(item) > 100:
|
|
|
+ raise ValueError(
|
|
|
+ "public rule result is not evidence safe"
|
|
|
+ )
|
|
|
+ for summary in item:
|
|
|
+ if (
|
|
|
+ not isinstance(summary, dict)
|
|
|
+ or set(summary) != {"step_id", "count"}
|
|
|
+ or not isinstance(summary["step_id"], str)
|
|
|
+ or isinstance(summary["count"], bool)
|
|
|
+ or not isinstance(summary["count"], int)
|
|
|
+ or summary["count"] < 0
|
|
|
+ ):
|
|
|
+ raise ValueError(
|
|
|
+ "public rule result is not evidence safe"
|
|
|
+ )
|
|
|
+ elif isinstance(item, (dict, list, tuple, set)):
|
|
|
+ raise ValueError("public rule result is not evidence safe")
|
|
|
+ return dict(value)
|
|
|
+
|
|
|
+
|
|
|
class PostgresRuleEvidenceWriter:
|
|
|
"""Persist one immutable run and at most one expiring violation sample."""
|
|
|
|
|
|
@@ -89,16 +215,20 @@ class PostgresRuleEvidenceWriter:
|
|
|
artifact_store,
|
|
|
*,
|
|
|
sample_ttl_seconds: int = 3600,
|
|
|
+ lease_seconds: int = 300,
|
|
|
):
|
|
|
self.engine = engine
|
|
|
self.artifact_store = artifact_store
|
|
|
self.sample_ttl_seconds = int(sample_ttl_seconds)
|
|
|
+ self.lease_seconds = int(lease_seconds)
|
|
|
if (
|
|
|
self.sample_ttl_seconds < 1
|
|
|
or self.sample_ttl_seconds
|
|
|
> self.artifact_store.max_ttl_seconds
|
|
|
):
|
|
|
raise ValueError("violation sample TTL is invalid")
|
|
|
+ if self.lease_seconds < 30 or self.lease_seconds > 900:
|
|
|
+ raise ValueError("rule execution lease is invalid")
|
|
|
|
|
|
def start(
|
|
|
self,
|
|
|
@@ -108,13 +238,20 @@ class PostgresRuleEvidenceWriter:
|
|
|
plan_hash: str,
|
|
|
correlation_id: str,
|
|
|
dataflow_uid: str,
|
|
|
+ deployment_id: str,
|
|
|
+ environment: str,
|
|
|
workflow_version: int,
|
|
|
node_id: str,
|
|
|
+ lease_owner: str,
|
|
|
) -> str:
|
|
|
component = _uid(component_binding_id, "component binding id")
|
|
|
rule = _uid(rule_version_id, "rule version id")
|
|
|
correlation = _uid(correlation_id, "correlation id")
|
|
|
dataflow = _uid(dataflow_uid, "dataflow id")
|
|
|
+ deployment = _uid(deployment_id, "deployment id")
|
|
|
+ owner = _uuid(lease_owner, "lease owner")
|
|
|
+ if environment not in {"development", "test", "production"}:
|
|
|
+ raise ValueError("deployment environment is invalid")
|
|
|
if _DIGEST.fullmatch(str(plan_hash or "")) is None:
|
|
|
raise ValueError("plan hash is invalid")
|
|
|
if (
|
|
|
@@ -130,6 +267,8 @@ class PostgresRuleEvidenceWriter:
|
|
|
"component_binding_id": component,
|
|
|
"correlation_id": correlation,
|
|
|
"dataflow_uid": dataflow,
|
|
|
+ "deployment_id": deployment,
|
|
|
+ "environment": environment,
|
|
|
"node_id": node_id,
|
|
|
"plan_hash": plan_hash,
|
|
|
"rule_version_id": rule,
|
|
|
@@ -137,7 +276,7 @@ class PostgresRuleEvidenceWriter:
|
|
|
}
|
|
|
)
|
|
|
with self.engine.begin() as connection:
|
|
|
- deployments = connection.execute(
|
|
|
+ canonical = connection.execute(
|
|
|
text(
|
|
|
"""
|
|
|
SELECT d.id::text AS deployment_id
|
|
|
@@ -153,13 +292,12 @@ class PostgresRuleEvidenceWriter:
|
|
|
CAST(:rule_version_id AS uuid)
|
|
|
AND p.plan_hash = :plan_hash
|
|
|
AND p.status = 'published'
|
|
|
+ AND b.component_id = :node_id
|
|
|
AND v.dataflow_uid = CAST(:dataflow_uid AS uuid)
|
|
|
AND v.version_no = :workflow_version
|
|
|
+ AND d.id = CAST(:deployment_id AS uuid)
|
|
|
+ AND d.environment = :environment
|
|
|
AND d.status IN ('canary','active')
|
|
|
- ORDER BY
|
|
|
- CASE d.status WHEN 'active' THEN 0 ELSE 1 END,
|
|
|
- d.created_at DESC
|
|
|
- LIMIT 2
|
|
|
"""
|
|
|
),
|
|
|
{
|
|
|
@@ -167,13 +305,60 @@ class PostgresRuleEvidenceWriter:
|
|
|
"rule_version_id": rule,
|
|
|
"plan_hash": plan_hash,
|
|
|
"dataflow_uid": dataflow,
|
|
|
+ "deployment_id": deployment,
|
|
|
+ "environment": environment,
|
|
|
"workflow_version": workflow_version,
|
|
|
+ "node_id": node_id,
|
|
|
},
|
|
|
- ).mappings().all()
|
|
|
- if len(deployments) != 1:
|
|
|
- raise ValueError(
|
|
|
- "canonical rule deployment is missing or ambiguous"
|
|
|
- )
|
|
|
+ ).mappings().one_or_none()
|
|
|
+ if canonical is None:
|
|
|
+ raise ValueError("canonical rule deployment does not match")
|
|
|
+ existing = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT id::text, status, lease_owner::text,
|
|
|
+ lease_expires_at
|
|
|
+ FROM public.rule_runs
|
|
|
+ WHERE evidence_key = :evidence_key
|
|
|
+ FOR UPDATE
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"evidence_key": evidence_key},
|
|
|
+ ).mappings().one_or_none()
|
|
|
+ if existing is not None:
|
|
|
+ if (
|
|
|
+ existing["status"] == "running"
|
|
|
+ and existing["lease_expires_at"] is not None
|
|
|
+ ):
|
|
|
+ expired = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT :lease_expires_at <= CURRENT_TIMESTAMP
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "lease_expires_at": existing[
|
|
|
+ "lease_expires_at"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ ).scalar_one()
|
|
|
+ if expired:
|
|
|
+ connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.rule_runs
|
|
|
+ SET status = 'unknown',
|
|
|
+ commit_outcome = 'unknown',
|
|
|
+ failure_code = 'execution_lease_expired',
|
|
|
+ finished_at = CURRENT_TIMESTAMP,
|
|
|
+ updated_at = CURRENT_TIMESTAMP
|
|
|
+ WHERE id = CAST(:id AS uuid)
|
|
|
+ AND status = 'running'
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"id": existing["id"]},
|
|
|
+ )
|
|
|
+ return str(existing["id"])
|
|
|
rule_run_id = new_governance_uid()
|
|
|
selected = connection.execute(
|
|
|
text(
|
|
|
@@ -182,12 +367,18 @@ class PostgresRuleEvidenceWriter:
|
|
|
id, deployment_id, component_binding_id,
|
|
|
rule_version_id, plan_hash, status,
|
|
|
correlation_id, evidence_key, started_at
|
|
|
+ , attempt_no, lease_owner, lease_expires_at,
|
|
|
+ heartbeat_at
|
|
|
) VALUES (
|
|
|
CAST(:id AS uuid), CAST(:deployment_id AS uuid),
|
|
|
CAST(:component_binding_id AS uuid),
|
|
|
CAST(:rule_version_id AS uuid), :plan_hash,
|
|
|
'running', CAST(:correlation_id AS uuid),
|
|
|
- :evidence_key, CURRENT_TIMESTAMP
|
|
|
+ :evidence_key, CURRENT_TIMESTAMP, 1,
|
|
|
+ CAST(:lease_owner AS uuid),
|
|
|
+ CURRENT_TIMESTAMP
|
|
|
+ + make_interval(secs => :lease_seconds),
|
|
|
+ CURRENT_TIMESTAMP
|
|
|
)
|
|
|
ON CONFLICT (evidence_key) DO NOTHING
|
|
|
RETURNING id::text
|
|
|
@@ -195,12 +386,14 @@ class PostgresRuleEvidenceWriter:
|
|
|
),
|
|
|
{
|
|
|
"id": rule_run_id,
|
|
|
- "deployment_id": deployments[0]["deployment_id"],
|
|
|
+ "deployment_id": canonical["deployment_id"],
|
|
|
"component_binding_id": component,
|
|
|
"rule_version_id": rule,
|
|
|
"plan_hash": plan_hash,
|
|
|
"correlation_id": correlation,
|
|
|
"evidence_key": evidence_key,
|
|
|
+ "lease_owner": owner,
|
|
|
+ "lease_seconds": self.lease_seconds,
|
|
|
},
|
|
|
).scalar_one_or_none()
|
|
|
if selected is None:
|
|
|
@@ -216,6 +409,205 @@ class PostgresRuleEvidenceWriter:
|
|
|
).scalar_one()
|
|
|
return str(selected)
|
|
|
|
|
|
+ def heartbeat(self, rule_run_id: str, lease_owner: str) -> None:
|
|
|
+ run_id = _uid(rule_run_id, "rule run id")
|
|
|
+ owner = _uuid(lease_owner, "lease owner")
|
|
|
+ with self.engine.begin() as connection:
|
|
|
+ updated = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.rule_runs
|
|
|
+ SET heartbeat_at = CURRENT_TIMESTAMP,
|
|
|
+ lease_expires_at = CURRENT_TIMESTAMP
|
|
|
+ + make_interval(secs => :lease_seconds),
|
|
|
+ updated_at = CURRENT_TIMESTAMP
|
|
|
+ WHERE id = CAST(:id AS uuid)
|
|
|
+ AND status = 'running'
|
|
|
+ AND lease_owner = CAST(:lease_owner AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "id": run_id,
|
|
|
+ "lease_owner": owner,
|
|
|
+ "lease_seconds": self.lease_seconds,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if updated.rowcount != 1:
|
|
|
+ raise ValueError("rule execution lease is not owned")
|
|
|
+
|
|
|
+ def stage_sql_output(
|
|
|
+ self,
|
|
|
+ rule_run_id: str,
|
|
|
+ *,
|
|
|
+ output_binding_id: str,
|
|
|
+ ttl_seconds: int | None = None,
|
|
|
+ ) -> str:
|
|
|
+ run_id = _uid(rule_run_id, "rule run id")
|
|
|
+ binding_id = _uid(output_binding_id, "output binding id")
|
|
|
+ if ttl_seconds is None:
|
|
|
+ ttl_seconds = min(3600, self.sample_ttl_seconds)
|
|
|
+ if (
|
|
|
+ isinstance(ttl_seconds, bool)
|
|
|
+ or not isinstance(ttl_seconds, int)
|
|
|
+ or ttl_seconds < 1
|
|
|
+ or ttl_seconds > self.sample_ttl_seconds
|
|
|
+ ):
|
|
|
+ raise ValueError("SQL staging TTL is invalid")
|
|
|
+ receipt_id = new_governance_uid()
|
|
|
+ with self.engine.begin() as connection:
|
|
|
+ row = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT r.deployment_id::text, r.correlation_id::text,
|
|
|
+ b.binding_hash, b.object_ref, b.object_kind,
|
|
|
+ b.access_mode
|
|
|
+ FROM public.rule_runs r
|
|
|
+ JOIN public.dataflow_dataset_bindings b
|
|
|
+ ON b.id = CAST(:binding_id AS uuid)
|
|
|
+ AND b.dataflow_deployment_id = r.deployment_id
|
|
|
+ WHERE r.id = CAST(:run_id AS uuid)
|
|
|
+ AND r.status = 'running'
|
|
|
+ FOR SHARE OF r, b
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"run_id": run_id, "binding_id": binding_id},
|
|
|
+ ).mappings().one_or_none()
|
|
|
+ if (
|
|
|
+ row is None
|
|
|
+ or row["object_kind"] not in {"table", "view"}
|
|
|
+ or row["access_mode"] not in {"write", "read_write"}
|
|
|
+ ):
|
|
|
+ raise ValueError("SQL staging output binding is invalid")
|
|
|
+ relation_digest = _canonical_digest(
|
|
|
+ {
|
|
|
+ "binding_hash": str(row["binding_hash"]),
|
|
|
+ "object_kind": str(row["object_kind"]),
|
|
|
+ "object_ref": str(row["object_ref"]),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ selected = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ INSERT INTO public.rule_sql_staging_receipts (
|
|
|
+ id, producer_rule_run_id, deployment_id,
|
|
|
+ correlation_id, output_binding_id,
|
|
|
+ output_binding_hash, relation_ref,
|
|
|
+ relation_digest, commit_outcome, status,
|
|
|
+ expires_at
|
|
|
+ ) VALUES (
|
|
|
+ CAST(:id AS uuid), CAST(:run_id AS uuid),
|
|
|
+ CAST(:deployment_id AS uuid),
|
|
|
+ CAST(:correlation_id AS uuid),
|
|
|
+ CAST(:binding_id AS uuid), :binding_hash,
|
|
|
+ :relation_ref, :relation_digest, 'committed',
|
|
|
+ 'pending', CURRENT_TIMESTAMP
|
|
|
+ + make_interval(secs => :ttl_seconds)
|
|
|
+ )
|
|
|
+ ON CONFLICT (
|
|
|
+ producer_rule_run_id, output_binding_id
|
|
|
+ ) DO NOTHING
|
|
|
+ RETURNING id::text
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "id": receipt_id,
|
|
|
+ "run_id": run_id,
|
|
|
+ "deployment_id": row["deployment_id"],
|
|
|
+ "correlation_id": row["correlation_id"],
|
|
|
+ "binding_id": binding_id,
|
|
|
+ "binding_hash": str(row["binding_hash"]),
|
|
|
+ "relation_ref": str(row["object_ref"]),
|
|
|
+ "relation_digest": relation_digest,
|
|
|
+ "ttl_seconds": ttl_seconds,
|
|
|
+ },
|
|
|
+ ).scalar_one_or_none()
|
|
|
+ if selected is None:
|
|
|
+ selected = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT id::text
|
|
|
+ FROM public.rule_sql_staging_receipts
|
|
|
+ WHERE producer_rule_run_id = CAST(:run_id AS uuid)
|
|
|
+ AND output_binding_id =
|
|
|
+ CAST(:binding_id AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"run_id": run_id, "binding_id": binding_id},
|
|
|
+ ).scalar_one()
|
|
|
+ return f"dataops-staging://{selected}"
|
|
|
+
|
|
|
+ def resolve_sql_staging(
|
|
|
+ self,
|
|
|
+ receipt_ref: str,
|
|
|
+ *,
|
|
|
+ deployment_id: str,
|
|
|
+ correlation_id: str,
|
|
|
+ input_binding_id: str,
|
|
|
+ ) -> dict[str, str]:
|
|
|
+ match = re.fullmatch(
|
|
|
+ r"dataops-staging://([0-9a-f-]{36})",
|
|
|
+ str(receipt_ref or ""),
|
|
|
+ )
|
|
|
+ if match is None:
|
|
|
+ raise ValueError("SQL staging receipt is invalid")
|
|
|
+ receipt_id = _uid(match.group(1), "SQL staging receipt id")
|
|
|
+ deployment = _uid(deployment_id, "deployment id")
|
|
|
+ correlation = _uid(correlation_id, "correlation id")
|
|
|
+ binding_id = _uid(input_binding_id, "input binding id")
|
|
|
+ with self.engine.connect() as connection:
|
|
|
+ row = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT s.relation_ref, s.relation_digest,
|
|
|
+ s.output_binding_hash, b.binding_hash,
|
|
|
+ b.object_ref, b.object_kind, b.access_mode
|
|
|
+ FROM public.rule_sql_staging_receipts s
|
|
|
+ JOIN public.rule_runs r
|
|
|
+ ON r.id = s.producer_rule_run_id
|
|
|
+ JOIN public.dataflow_dataset_bindings b
|
|
|
+ ON b.id = s.output_binding_id
|
|
|
+ WHERE s.id = CAST(:id AS uuid)
|
|
|
+ AND s.deployment_id =
|
|
|
+ CAST(:deployment_id AS uuid)
|
|
|
+ AND s.correlation_id =
|
|
|
+ CAST(:correlation_id AS uuid)
|
|
|
+ AND s.output_binding_id =
|
|
|
+ CAST(:input_binding_id AS uuid)
|
|
|
+ AND s.status = 'ready'
|
|
|
+ AND s.expires_at > CURRENT_TIMESTAMP
|
|
|
+ AND s.commit_outcome = 'committed'
|
|
|
+ AND r.status = 'success'
|
|
|
+ AND r.commit_outcome = 'committed'
|
|
|
+ AND b.binding_hash = s.output_binding_hash
|
|
|
+ AND b.access_mode IN ('read','read_write')
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "id": receipt_id,
|
|
|
+ "deployment_id": deployment,
|
|
|
+ "correlation_id": correlation,
|
|
|
+ "input_binding_id": binding_id,
|
|
|
+ },
|
|
|
+ ).mappings().one_or_none()
|
|
|
+ if row is None:
|
|
|
+ raise ValueError("SQL staging receipt is not executable")
|
|
|
+ expected_digest = _canonical_digest(
|
|
|
+ {
|
|
|
+ "binding_hash": str(row["binding_hash"]),
|
|
|
+ "object_kind": str(row["object_kind"]),
|
|
|
+ "object_ref": str(row["object_ref"]),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ if (
|
|
|
+ expected_digest != str(row["relation_digest"])
|
|
|
+ or str(row["relation_ref"]) != str(row["object_ref"])
|
|
|
+ ):
|
|
|
+ raise ValueError("SQL staging receipt attestation does not match")
|
|
|
+ return {
|
|
|
+ "relation_ref": str(row["relation_ref"]),
|
|
|
+ "relation_digest": str(row["relation_digest"]),
|
|
|
+ }
|
|
|
+
|
|
|
def replay(self, rule_run_id: str) -> dict[str, Any] | None:
|
|
|
run_id = _uid(rule_run_id, "rule run id")
|
|
|
with self.engine.connect() as connection:
|
|
|
@@ -242,6 +634,57 @@ class PostgresRuleEvidenceWriter:
|
|
|
"commit_outcome": str(row["commit_outcome"]),
|
|
|
}
|
|
|
|
|
|
+ def replay_by_lease_owner(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ lease_owner: str,
|
|
|
+ deployment_id: str,
|
|
|
+ correlation_id: str,
|
|
|
+ component_binding_id: str,
|
|
|
+ rule_version_id: str,
|
|
|
+ plan_hash: str,
|
|
|
+ ) -> dict[str, Any] | None:
|
|
|
+ owner = _uuid(lease_owner, "lease owner")
|
|
|
+ deployment = _uid(deployment_id, "deployment id")
|
|
|
+ correlation = _uid(correlation_id, "correlation id")
|
|
|
+ component = _uid(
|
|
|
+ component_binding_id,
|
|
|
+ "component binding id",
|
|
|
+ )
|
|
|
+ rule = _uid(rule_version_id, "rule version id")
|
|
|
+ if _DIGEST.fullmatch(str(plan_hash or "")) is None:
|
|
|
+ raise ValueError("plan hash is invalid")
|
|
|
+ with self.engine.connect() as connection:
|
|
|
+ row = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT id::text
|
|
|
+ FROM public.rule_runs
|
|
|
+ WHERE lease_owner = CAST(:lease_owner AS uuid)
|
|
|
+ AND deployment_id =
|
|
|
+ CAST(:deployment_id AS uuid)
|
|
|
+ AND correlation_id =
|
|
|
+ CAST(:correlation_id AS uuid)
|
|
|
+ AND component_binding_id =
|
|
|
+ CAST(:component_binding_id AS uuid)
|
|
|
+ AND rule_version_id =
|
|
|
+ CAST(:rule_version_id AS uuid)
|
|
|
+ AND plan_hash = :plan_hash
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "lease_owner": owner,
|
|
|
+ "deployment_id": deployment,
|
|
|
+ "correlation_id": correlation,
|
|
|
+ "component_binding_id": component,
|
|
|
+ "rule_version_id": rule,
|
|
|
+ "plan_hash": plan_hash,
|
|
|
+ },
|
|
|
+ ).scalar_one_or_none()
|
|
|
+ if row is None:
|
|
|
+ return None
|
|
|
+ return self.replay(str(row))
|
|
|
+
|
|
|
@staticmethod
|
|
|
def _validate_finish(result: Any) -> dict[str, Any]:
|
|
|
if not isinstance(result, dict) or set(result) - _FINISH_KEYS:
|
|
|
@@ -263,21 +706,8 @@ class PostgresRuleEvidenceWriter:
|
|
|
):
|
|
|
raise ValueError("rule evidence timings are invalid")
|
|
|
public_result = result.get("public_result")
|
|
|
- if public_result is not None and (
|
|
|
- not isinstance(public_result, dict)
|
|
|
- or any(str(key).startswith("_") for key in public_result)
|
|
|
- or "rows" in public_result
|
|
|
- or len(
|
|
|
- json.dumps(
|
|
|
- public_result,
|
|
|
- sort_keys=True,
|
|
|
- separators=(",", ":"),
|
|
|
- ensure_ascii=False,
|
|
|
- ).encode("utf-8")
|
|
|
- )
|
|
|
- > 32_768
|
|
|
- ):
|
|
|
- raise ValueError("public rule result is not evidence safe")
|
|
|
+ if public_result is not None:
|
|
|
+ validate_public_rule_result(public_result)
|
|
|
sample = result.get("violation_sample")
|
|
|
if sample is not None:
|
|
|
if (
|
|
|
@@ -527,13 +957,15 @@ class PostgresRuleEvidenceWriter:
|
|
|
def finish(self, rule_run_id: str, result: Any) -> None:
|
|
|
run_id = _uid(rule_run_id, "rule run id")
|
|
|
normalized = self._validate_finish(result)
|
|
|
+ evidence_digest = _canonical_digest(normalized)
|
|
|
sample_path = None
|
|
|
try:
|
|
|
with self.engine.connect() as connection:
|
|
|
current = connection.execute(
|
|
|
text(
|
|
|
"""
|
|
|
- SELECT status, correlation_id::text
|
|
|
+ SELECT status, correlation_id::text,
|
|
|
+ evidence_digest
|
|
|
FROM public.rule_runs
|
|
|
WHERE id = CAST(:id AS uuid)
|
|
|
"""
|
|
|
@@ -543,8 +975,11 @@ class PostgresRuleEvidenceWriter:
|
|
|
if current is None:
|
|
|
raise ValueError("rule run was not found")
|
|
|
if current["status"] not in {"queued", "running"}:
|
|
|
- replay = self.replay(run_id)
|
|
|
- if replay and replay["status"] == normalized["status"]:
|
|
|
+ if (
|
|
|
+ current["status"] == normalized["status"]
|
|
|
+ and str(current["evidence_digest"] or "")
|
|
|
+ == evidence_digest
|
|
|
+ ):
|
|
|
return
|
|
|
raise ValueError("rule run evidence is immutable")
|
|
|
if normalized.get("violation_sample"):
|
|
|
@@ -569,6 +1004,8 @@ class PostgresRuleEvidenceWriter:
|
|
|
commit_outcome = :commit_outcome,
|
|
|
public_result = CAST(:public_result AS jsonb),
|
|
|
failure_code = :failure_code,
|
|
|
+ evidence_digest = :evidence_digest,
|
|
|
+ lease_expires_at = NULL,
|
|
|
finished_at = CURRENT_TIMESTAMP,
|
|
|
updated_at = CURRENT_TIMESTAMP
|
|
|
WHERE id = CAST(:id AS uuid)
|
|
|
@@ -607,13 +1044,15 @@ class PostgresRuleEvidenceWriter:
|
|
|
f"execution_{normalized['status']}"
|
|
|
)
|
|
|
),
|
|
|
+ "evidence_digest": evidence_digest,
|
|
|
},
|
|
|
)
|
|
|
if updated.rowcount != 1:
|
|
|
state = connection.execute(
|
|
|
text(
|
|
|
"""
|
|
|
- SELECT status, commit_outcome
|
|
|
+ SELECT status, commit_outcome,
|
|
|
+ evidence_digest
|
|
|
FROM public.rule_runs
|
|
|
WHERE id = CAST(:id AS uuid)
|
|
|
"""
|
|
|
@@ -625,22 +1064,69 @@ class PostgresRuleEvidenceWriter:
|
|
|
or state["status"] != normalized["status"]
|
|
|
or state["commit_outcome"]
|
|
|
!= normalized["commit_outcome"]
|
|
|
+ or str(state["evidence_digest"] or "")
|
|
|
+ != evidence_digest
|
|
|
):
|
|
|
raise RuntimeError(
|
|
|
"rule run finalize outcome is unknown"
|
|
|
)
|
|
|
+ receipt_status = (
|
|
|
+ "ready"
|
|
|
+ if normalized["status"] == "success"
|
|
|
+ and normalized["commit_outcome"] == "committed"
|
|
|
+ else "failed"
|
|
|
+ )
|
|
|
+ connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.rule_sql_staging_receipts
|
|
|
+ SET status = :status,
|
|
|
+ ready_at = CASE
|
|
|
+ WHEN :status = 'ready'
|
|
|
+ THEN CURRENT_TIMESTAMP
|
|
|
+ ELSE ready_at
|
|
|
+ END,
|
|
|
+ commit_outcome = CASE
|
|
|
+ WHEN :status = 'ready'
|
|
|
+ THEN 'committed'
|
|
|
+ ELSE 'unknown'
|
|
|
+ END,
|
|
|
+ updated_at = CURRENT_TIMESTAMP
|
|
|
+ WHERE producer_rule_run_id =
|
|
|
+ CAST(:run_id AS uuid)
|
|
|
+ AND status = 'pending'
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "run_id": run_id,
|
|
|
+ "status": receipt_status,
|
|
|
+ },
|
|
|
+ )
|
|
|
except Exception as exc:
|
|
|
try:
|
|
|
- replay = self.replay(run_id)
|
|
|
+ with self.engine.connect() as connection:
|
|
|
+ terminal = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT status, commit_outcome,
|
|
|
+ evidence_digest
|
|
|
+ FROM public.rule_runs
|
|
|
+ WHERE id = CAST(:id AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"id": run_id},
|
|
|
+ ).mappings().one_or_none()
|
|
|
except Exception as recheck_exc:
|
|
|
raise RuntimeError(
|
|
|
"rule run finalize outcome is unknown"
|
|
|
) from recheck_exc
|
|
|
if (
|
|
|
- replay is None
|
|
|
- or replay["status"] != normalized["status"]
|
|
|
- or replay["commit_outcome"]
|
|
|
+ terminal is None
|
|
|
+ or terminal["status"] != normalized["status"]
|
|
|
+ or terminal["commit_outcome"]
|
|
|
!= normalized["commit_outcome"]
|
|
|
+ or str(terminal["evidence_digest"] or "")
|
|
|
+ != evidence_digest
|
|
|
):
|
|
|
raise RuntimeError(
|
|
|
"rule run finalize outcome is unknown"
|
|
|
@@ -689,4 +1175,7 @@ class PostgresRuleEvidenceWriter:
|
|
|
return removed
|
|
|
|
|
|
|
|
|
-__all__ = ["PostgresRuleEvidenceWriter"]
|
|
|
+__all__ = [
|
|
|
+ "PostgresRuleEvidenceWriter",
|
|
|
+ "validate_public_rule_result",
|
|
|
+]
|