Files
CASAN/packages/casan-harness/kernel/contracts.py
T

173 lines
6.7 KiB
Python

"""Canonical CASAN wire-contract builders and lightweight validation."""
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timezone
from typing import Any, Mapping
CONTRACT_VERSION = "1.0.0"
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def actor(actor_id: str, actor_type: str, issuer: str, trust_level: str, auth_evidence_ref: str | None = None) -> dict[str, Any]:
return {
"actor_id": actor_id,
"actor_type": actor_type,
"issuer": issuer,
"trust_level": trust_level,
"authentication_evidence_ref": auth_evidence_ref,
}
def outcome(execution: str, assurance: str, certification: str, business: str = "not_evaluated") -> dict[str, str]:
return {
"execution_result": execution,
"assurance_result": assurance,
"certification_result": certification,
"business_result": business,
}
def evidence_digest(evidence: Mapping[str, Any]) -> str:
"""Digest the claim-bearing evidence metadata, excluding its digest field."""
material = {
key: evidence.get(key)
for key in (
"evidence_id", "claim_refs", "producer_identity", "timestamp",
"artifact_ref", "validation_status", "retention_class", "category",
)
}
payload = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def verify_evidence_integrity(evidence: Mapping[str, Any]) -> bool:
integrity = evidence.get("integrity")
if not isinstance(integrity, Mapping) or integrity.get("algorithm") != "sha256":
return False
digest = integrity.get("digest")
return isinstance(digest, str) and digest == evidence_digest(evidence)
def validate_bundle(bundle: Mapping[str, Any]) -> list[str]:
"""Return deterministic contract errors; an empty list means valid.
JSON Schema is canonical. This small validator keeps runtime adapters stdlib-
only and catches the invariants needed at enforcement boundaries.
"""
errors: list[str] = []
if bundle.get("schema_version") != CONTRACT_VERSION:
errors.append("unsupported_schema_version")
run = bundle.get("run")
if not isinstance(run, Mapping):
errors.append("missing_execution_run")
return errors
for key in ("run_id", "runtime", "mode", "status", "correlation_id", "outcome"):
if not run.get(key):
errors.append(f"run.{key}_required")
actors = bundle.get("actors")
actor_ids: set[object] = set()
if not isinstance(actors, list) or not actors:
errors.append("actors_required")
else:
actor_ids = {item.get("actor_id") for item in actors if isinstance(item, Mapping)}
if "" in actor_ids or None in actor_ids:
errors.append("actor_identity_required")
steps = bundle.get("steps")
if not isinstance(steps, list):
errors.append("steps_required")
else:
for index, step in enumerate(steps):
if not isinstance(step, Mapping):
errors.append(f"steps.{index}_invalid")
continue
for key in ("step_id", "run_id", "sequence", "actor_ref", "action", "outcome"):
if step.get(key) is None:
errors.append(f"steps.{index}.{key}_required")
if step.get("actor_ref") not in actor_ids:
errors.append(f"steps.{index}.actor_ref_unknown")
evidence = bundle.get("evidence")
evidence_ids: set[object] = set()
if not isinstance(evidence, list):
errors.append("evidence_required")
else:
for index, item in enumerate(evidence):
if not isinstance(item, Mapping):
errors.append(f"evidence.{index}_invalid")
continue
evidence_ids.add(item.get("evidence_id"))
if not verify_evidence_integrity(item):
errors.append(f"evidence.{index}.integrity_invalid")
claims = bundle.get("claims")
claim_ids = {
item.get("claim_id") for item in claims or [] if isinstance(item, Mapping)
} if isinstance(claims, list) else set()
if isinstance(claims, list):
for index, claim in enumerate(claims):
if not isinstance(claim, Mapping):
errors.append(f"claims.{index}_invalid")
continue
for evidence_ref in claim.get("evidence_refs") or []:
if evidence_ref not in evidence_ids:
errors.append(f"claims.{index}.evidence_ref_unknown")
if isinstance(evidence, list):
for index, item in enumerate(evidence):
if isinstance(item, Mapping):
for claim_ref in item.get("claim_refs") or []:
if claim_ref not in claim_ids:
errors.append(f"evidence.{index}.claim_ref_unknown")
delegations = bundle.get("delegations")
if delegations is not None:
errors.extend(_validate_delegations(delegations, actor_ids))
if run.get("mode") == "observe" and isinstance(run.get("outcome"), Mapping):
if run["outcome"].get("certification_result") == "certified":
errors.append("observed_only_cannot_be_certified")
return errors
def _validate_delegations(delegations: object, actor_ids: set[object]) -> list[str]:
if not isinstance(delegations, list):
return ["delegations_invalid"]
errors: list[str] = []
records = {
item.get("delegation_id"): item
for item in delegations if isinstance(item, Mapping) and item.get("delegation_id")
}
for index, item in enumerate(delegations):
if not isinstance(item, Mapping):
errors.append(f"delegations.{index}_invalid")
continue
for actor_key in ("delegator_ref", "delegate_ref"):
if item.get(actor_key) not in actor_ids:
errors.append(f"delegations.{index}.{actor_key}_unknown")
current: Mapping[str, Any] = item
visited: set[object] = set()
depth = 0
while current.get("parent_delegation_ref"):
parent_ref = current.get("parent_delegation_ref")
if parent_ref in visited:
errors.append(f"delegations.{index}.cycle")
break
visited.add(parent_ref)
parent = records.get(parent_ref)
if not isinstance(parent, Mapping):
errors.append(f"delegations.{index}.parent_unknown")
break
depth += 1
current = parent
maximum_depth = item.get("maximum_depth")
if isinstance(maximum_depth, int) and depth > maximum_depth:
errors.append(f"delegations.{index}.maximum_depth_exceeded")
return errors