feat(casan): establish assurance kernel and harden control plane
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"description": "Deterministic action classes and minimum risk floors shared by CASAN runtimes.",
|
||||
"risk_order": ["low", "medium", "high", "critical"],
|
||||
"classes": {
|
||||
"read_only": {"risk_floor": "low", "side_effect_level": "none", "requires_approval": false, "actor_required": false, "evidence_required": false},
|
||||
"write": {"risk_floor": "medium", "side_effect_level": "write", "requires_approval": false, "actor_required": false, "evidence_required": true},
|
||||
"delete": {"risk_floor": "high", "side_effect_level": "destructive", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"database_mutation": {"risk_floor": "high", "side_effect_level": "write", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"migration": {"risk_floor": "high", "side_effect_level": "write", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"deployment": {"risk_floor": "high", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"release": {"risk_floor": "high", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"credential_access": {"risk_floor": "high", "side_effect_level": "sensitive_read", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"identity_permission_modification": {"risk_floor": "high", "side_effect_level": "write", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"external_network_side_effect": {"risk_floor": "medium", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"infrastructure_modification": {"risk_floor": "high", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
|
||||
"unknown": {"risk_floor": "high", "side_effect_level": "unknown", "requires_approval": true, "actor_required": true, "evidence_required": true}
|
||||
},
|
||||
"action_aliases": {
|
||||
"agent_step": "read_only",
|
||||
"read": "read_only",
|
||||
"search": "read_only",
|
||||
"write": "write",
|
||||
"write_code": "write",
|
||||
"write_file": "write",
|
||||
"delete": "delete",
|
||||
"delete_file": "delete",
|
||||
"db_write": "database_mutation",
|
||||
"database_mutation": "database_mutation",
|
||||
"migration": "migration",
|
||||
"migrate": "migration",
|
||||
"deploy": "deployment",
|
||||
"deployment": "deployment",
|
||||
"launch": "deployment",
|
||||
"release": "release",
|
||||
"credential_access": "credential_access",
|
||||
"secret_access": "credential_access",
|
||||
"permission_change": "identity_permission_modification",
|
||||
"identity_change": "identity_permission_modification",
|
||||
"external_api": "external_network_side_effect",
|
||||
"infrastructure_change": "infrastructure_modification"
|
||||
},
|
||||
"tool_aliases": {
|
||||
"read": "read_only",
|
||||
"grep": "read_only",
|
||||
"glob": "read_only",
|
||||
"search": "read_only",
|
||||
"view": "read_only",
|
||||
"codegraph_search": "read_only",
|
||||
"codegraph_node": "read_only",
|
||||
"codegraph_context": "read_only",
|
||||
"edit": "write",
|
||||
"write": "write",
|
||||
"multiedit": "write",
|
||||
"notebookedit": "write",
|
||||
"apply_patch": "write",
|
||||
"str_replace_editor": "write",
|
||||
"create_file": "write",
|
||||
"update_file": "write",
|
||||
"delete_file": "delete",
|
||||
"webfetch": "external_network_side_effect",
|
||||
"web_fetch": "external_network_side_effect",
|
||||
"browser": "external_network_side_effect",
|
||||
"sendmessage": "external_network_side_effect"
|
||||
},
|
||||
"command_patterns": [
|
||||
{"pattern": "(^|\\s)(kubectl|helm)(\\s|$).*(apply|delete|upgrade|install)|(^|\\s)terraform\\s+(apply|destroy)", "class": "infrastructure_modification"},
|
||||
{"pattern": "(^|\\s)(deploy|deployment|release)(\\s|$)|git\\s+push", "class": "deployment"},
|
||||
{"pattern": "(^|\\s)(prisma|alembic|flyway|liquibase|rails)\\s+.*(migrat|upgrade)|\\bmigration\\b", "class": "migration"},
|
||||
{"pattern": "\\b(delete|insert|update|alter|drop|truncate)\\s+(from|into|table|database)|\\b(db_write|database_mutation)\\b", "class": "database_mutation"},
|
||||
{"pattern": "(^|\\s)(rm|rmdir|unlink)\\s|delete_file", "class": "delete"},
|
||||
{"pattern": "(\\.ssh/|id_rsa|id_ed25519|\\.aws/credentials|\\.env($|\\s)|secret|credential|api[_-]?key)", "class": "credential_access"},
|
||||
{"pattern": "\\b(iam|chmod|chown|setfacl|role|permission)\\b.*\\b(add|create|delete|grant|modify|remove|set|update)\\b", "class": "identity_permission_modification"},
|
||||
{"pattern": "(^|\\s)(curl|wget|scp|sftp|ssh|nc|ncat)\\s", "class": "external_network_side_effect"},
|
||||
{"pattern": "(^|\\s)(cp|mv|mkdir|touch|tee|sed\\s+-i)\\s|(^|\\s)(npm|pnpm|yarn|pip|cargo|go)\\s+(install|add|get)\\b|(^|\\s)(cat|printf|echo).*(>|>>)", "class": "write"},
|
||||
{"pattern": "^\\s*(ls|pwd|cat|head|tail|wc|rg|grep|find|stat|git\\s+(status|diff|log|show))\\b", "class": "read_only"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Framework-independent CASAN Assurance Kernel contracts and policy primitives."""
|
||||
|
||||
from .adapters import AgenticBridgeAdapter, NativeHarnessAdapter
|
||||
from .contracts import CONTRACT_VERSION, validate_bundle
|
||||
from .policy import (
|
||||
classify_action,
|
||||
evaluate_failure_policy,
|
||||
evaluate_h2_gate,
|
||||
evaluate_registry_configuration,
|
||||
evaluate_risk,
|
||||
evaluate_trust_capabilities,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AgenticBridgeAdapter",
|
||||
"CONTRACT_VERSION",
|
||||
"NativeHarnessAdapter",
|
||||
"classify_action",
|
||||
"evaluate_failure_policy",
|
||||
"evaluate_h2_gate",
|
||||
"evaluate_registry_configuration",
|
||||
"evaluate_risk",
|
||||
"evaluate_trust_capabilities",
|
||||
"validate_bundle",
|
||||
]
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Compatibility adapters from existing runtimes into the canonical contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .contracts import CONTRACT_VERSION, actor, evidence_digest, outcome, utc_now
|
||||
from .policy import evaluate_risk
|
||||
|
||||
|
||||
class NativeHarnessAdapter:
|
||||
runtime_type = "casan-native-harness"
|
||||
|
||||
def map_execution(self, event: Mapping[str, Any]) -> dict[str, Any]:
|
||||
run_id = str(event.get("run_id") or event.get("trace_id") or "")
|
||||
actor_id = str(event.get("actor") or "unidentified")
|
||||
action_name = str(event.get("action") or "agent_step")
|
||||
mode = str(event.get("mode") or "enforce")
|
||||
risk = evaluate_risk(
|
||||
action=action_name,
|
||||
tool=str(event.get("tool") or action_name),
|
||||
resource=str(event.get("resource") or ""),
|
||||
command=str(event.get("command") or ""),
|
||||
actor=actor_id if actor_id != "unidentified" else "",
|
||||
environment=str(event.get("environment") or "development"),
|
||||
)
|
||||
execution_status = str(event.get("execution_status") or "unknown")
|
||||
assurance_status = str(event.get("assurance_status") or "unknown")
|
||||
certification = str(event.get("certification_status") or "non_certified")
|
||||
return _bundle(
|
||||
run_id=run_id,
|
||||
runtime_type=self.runtime_type,
|
||||
runtime_version=str(event.get("runtime_version") or "legacy-compatible"),
|
||||
mode=mode,
|
||||
actor_id=actor_id,
|
||||
action_name=action_name,
|
||||
risk=risk,
|
||||
execution_status=execution_status,
|
||||
assurance_status=assurance_status,
|
||||
certification=certification,
|
||||
event=event,
|
||||
)
|
||||
|
||||
|
||||
class AgenticBridgeAdapter:
|
||||
runtime_type = "agentic-bridge"
|
||||
|
||||
def map_execution(self, event: Mapping[str, Any]) -> dict[str, Any]:
|
||||
run_id = str(event.get("trace_id") or event.get("run_id") or "")
|
||||
actor_id = str(event.get("actor") or "unidentified")
|
||||
action_name = str(event.get("action") or event.get("last_action") or "agent_step")
|
||||
mode = str(event.get("mode") or event.get("hook_trust_mode") or "observe")
|
||||
risk = evaluate_risk(
|
||||
action=action_name,
|
||||
tool=str(event.get("tool") or event.get("last_tool") or action_name),
|
||||
resource=str(event.get("resource") or ""),
|
||||
command=str(event.get("command") or ""),
|
||||
actor=actor_id if actor_id != "unidentified" else "",
|
||||
environment=str(event.get("environment") or "development"),
|
||||
)
|
||||
execution_status = str(event.get("execution_status") or "unknown")
|
||||
assurance_status = str(event.get("assurance_status") or "unknown")
|
||||
certification = "certified" if event.get("certified") else "non_certified"
|
||||
return _bundle(
|
||||
run_id=run_id,
|
||||
runtime_type=self.runtime_type,
|
||||
runtime_version=str(event.get("adapter_version") or "legacy-compatible"),
|
||||
mode=mode,
|
||||
actor_id=actor_id,
|
||||
action_name=action_name,
|
||||
risk=risk,
|
||||
execution_status=execution_status,
|
||||
assurance_status=assurance_status,
|
||||
certification=certification,
|
||||
event=event,
|
||||
)
|
||||
|
||||
|
||||
def _bundle(
|
||||
*,
|
||||
run_id: str,
|
||||
runtime_type: str,
|
||||
runtime_version: str,
|
||||
mode: str,
|
||||
actor_id: str,
|
||||
action_name: str,
|
||||
risk: Mapping[str, Any],
|
||||
execution_status: str,
|
||||
assurance_status: str,
|
||||
certification: str,
|
||||
event: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
step_id = str(event.get("step_id") or f"{run_id}:1")
|
||||
correlation_id = str(event.get("correlation_id") or run_id)
|
||||
raw_evidence = list(event.get("evidence") or [])
|
||||
claim_id = f"{run_id}:assurance-claim"
|
||||
evidence = []
|
||||
for index, item in enumerate(raw_evidence, 1):
|
||||
raw = item if isinstance(item, Mapping) else {"detail": str(item)}
|
||||
canonical_evidence = {
|
||||
"evidence_id": str(raw.get("evidence_id") or f"{run_id}:evidence:{index}"),
|
||||
"claim_refs": [claim_id],
|
||||
"producer_identity": runtime_type,
|
||||
"timestamp": raw.get("at") or raw.get("timestamp") or utc_now(),
|
||||
"artifact_ref": str(raw.get("artifact_ref") or f"inline:{run_id}:{index}"),
|
||||
"validation_status": "valid" if raw.get("decision") in {"allow", "pass", "passed", "certified"} else "unverified",
|
||||
"retention_class": str(raw.get("retention_class") or "runtime_assurance"),
|
||||
"category": str(raw.get("category") or "runtime_control"),
|
||||
}
|
||||
canonical_evidence["integrity"] = {
|
||||
"algorithm": "sha256",
|
||||
"digest": evidence_digest(canonical_evidence),
|
||||
"source_content_hash": raw.get("content_hash"),
|
||||
}
|
||||
evidence.append(canonical_evidence)
|
||||
policy_decisions = []
|
||||
for item in list(event.get("policy_decisions") or []):
|
||||
if not isinstance(item, Mapping):
|
||||
continue
|
||||
decision = str(item.get("decision") or "observe_only")
|
||||
if decision not in {"allow", "deny", "quarantine", "require_approval", "transform", "observe_only"}:
|
||||
decision = "observe_only"
|
||||
policy_decisions.append({
|
||||
"policy_id": str(item.get("policy_id") or "casan.compatibility.policy"),
|
||||
"policy_version": str(item.get("policy_version") or item.get("schema_version") or CONTRACT_VERSION),
|
||||
"decision": decision,
|
||||
"reason_codes": list(item.get("reason_codes") or [item.get("reason_code") or "compatibility_mapping"]),
|
||||
"effective_risk": str(item.get("effective_risk") or "high"),
|
||||
"input_facts": dict(item),
|
||||
"enforcement_point": str(item.get("enforcement_path") or runtime_type),
|
||||
"timestamp": str(item.get("timestamp") or utc_now()),
|
||||
"decision_engine_identity": "casan-assurance-kernel",
|
||||
"evidence_ref": evidence[0]["evidence_id"] if evidence else None,
|
||||
})
|
||||
result = outcome(execution_status, assurance_status, certification)
|
||||
return {
|
||||
"schema_version": CONTRACT_VERSION,
|
||||
"extension_namespace": f"casan.runtime.{runtime_type}",
|
||||
"run": {
|
||||
"run_id": run_id,
|
||||
"parent_run_id": event.get("parent_run_id"),
|
||||
"tenant": str(event.get("tenant") or "default"),
|
||||
"project": str(event.get("project") or event.get("project_id") or "default"),
|
||||
"environment": str(event.get("environment") or "development"),
|
||||
"runtime": {"type": runtime_type, "version": runtime_version},
|
||||
"mode": mode,
|
||||
"requested_goal": str(event.get("requested_goal") or ""),
|
||||
"started_at": event.get("started_at") or event.get("timestamp") or utc_now(),
|
||||
"completed_at": event.get("completed_at") or utc_now(),
|
||||
"status": execution_status,
|
||||
"risk_summary": dict(risk),
|
||||
"evidence_manifest_ref": event.get("evidence_manifest_ref"),
|
||||
"correlation_id": correlation_id,
|
||||
"outcome": result,
|
||||
},
|
||||
"actors": [actor(actor_id, "agent" if runtime_type == "casan-native-harness" else "runtime", runtime_type, "verified" if actor_id != "unidentified" else "unverified", event.get("authentication_evidence_ref"))],
|
||||
"steps": [{
|
||||
"step_id": step_id,
|
||||
"run_id": run_id,
|
||||
"parent_step_id": event.get("parent_step_id"),
|
||||
"sequence": int(event.get("sequence") or 1),
|
||||
"actor_ref": actor_id,
|
||||
"action": {"class": risk["action_class"], "name": action_name, "tool": event.get("tool"), "side_effect_level": risk["side_effect_level"]},
|
||||
"resource": event.get("resource"),
|
||||
"input_context_refs": list(event.get("input_context_refs") or []),
|
||||
"policy_decisions": policy_decisions,
|
||||
"tool_invocation": event.get("tool_invocation"),
|
||||
"verification_results": list(event.get("verification_results") or []),
|
||||
"evidence_refs": [item["evidence_id"] for item in evidence],
|
||||
"outcome": result,
|
||||
"started_at": event.get("started_at") or event.get("timestamp") or utc_now(),
|
||||
"completed_at": event.get("completed_at") or utc_now(),
|
||||
}],
|
||||
"runtime_capabilities": {
|
||||
"canCancel": False, "canPause": False, "canResume": False,
|
||||
"canRollback": False, "canReduceAuthority": False,
|
||||
"canQuarantine": runtime_type == "agentic-bridge",
|
||||
},
|
||||
"claims": [{
|
||||
"claim_id": claim_id,
|
||||
"statement": "CASAN evaluated the runtime assurance outcome",
|
||||
"evidence_refs": [item["evidence_id"] for item in evidence],
|
||||
"validation_status": "validated" if assurance_status == "passed" else "insufficient",
|
||||
}],
|
||||
"evidence": evidence,
|
||||
"trace_links": [
|
||||
{"type": "parent_to_child_execution", "from": run_id, "to": step_id},
|
||||
*[
|
||||
{"type": "evidence_to_outcome", "from": item["evidence_id"], "to": claim_id}
|
||||
for item in evidence
|
||||
],
|
||||
],
|
||||
"extensions": {f"casan.runtime.{runtime_type}": dict(event.get("extensions") or {})},
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
"""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
|
||||
@@ -0,0 +1,400 @@
|
||||
"""Deterministic security policy primitives shared by every CASAN adapter.
|
||||
|
||||
This module is deliberately stdlib-only. It never calls a model and never treats
|
||||
model text as an enforcement decision. The JSON action registry is the canonical
|
||||
machine-readable source; callers receive structured decisions rather than booleans.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, MutableMapping, Optional
|
||||
|
||||
|
||||
POLICY_VERSION = "1.0.0"
|
||||
RISK_ORDER = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
||||
PRODUCTION_PROFILES = {"prod", "production", "strict"}
|
||||
DEVELOPMENT_PROFILES = {"", "dev", "development", "local", "test", "testing"}
|
||||
FAILURE_POLICIES = {"halt", "quarantine", "require_approval", "record_only"}
|
||||
|
||||
|
||||
def _harness_root() -> Path:
|
||||
override = os.environ.get("CASAN_HARNESS_ROOT")
|
||||
return Path(override).resolve() if override else Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _action_registry_path() -> Path:
|
||||
override = os.environ.get("CASAN_ACTION_CLASS_REGISTRY")
|
||||
return Path(override).resolve() if override else _harness_root() / "config" / "action-classes.json"
|
||||
|
||||
|
||||
def _load_action_registry() -> dict[str, Any]:
|
||||
with _action_registry_path().open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
if not isinstance(payload, dict) or not isinstance(payload.get("classes"), dict):
|
||||
raise ValueError("action class registry is malformed")
|
||||
return payload
|
||||
|
||||
|
||||
def _normal(value: object) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
|
||||
|
||||
|
||||
def _higher(left: str, right: str) -> str:
|
||||
return right if RISK_ORDER.get(right, 3) > RISK_ORDER.get(left, 3) else left
|
||||
|
||||
|
||||
def _content_risk(text: str) -> str:
|
||||
value = (text or "").lower()
|
||||
if re.search(r"\b(drop\s+table|truncate|password|api[_-]?key|secret|credential|shutdown|rm\s+-rf)\b", value):
|
||||
return "high"
|
||||
if re.search(r"\b(deploy|release|migration|permission|policy|external\s+api|database)\b", value):
|
||||
return "medium"
|
||||
return "low"
|
||||
|
||||
|
||||
def _resource_risk(resource: str) -> str:
|
||||
value = (resource or "").lower()
|
||||
if re.search(r"(^|/)(\.env|\.ssh|\.aws)(/|$)|\.(pem|key|p12|pfx)$|credential|secret", value):
|
||||
return "high"
|
||||
if re.search(r"\b(prod|production|customer|tenant)\b", value):
|
||||
return "medium"
|
||||
return "low"
|
||||
|
||||
|
||||
def _environment_risk(environment: str) -> str:
|
||||
return "medium" if _normal(environment) in PRODUCTION_PROFILES else "low"
|
||||
|
||||
|
||||
def classify_action(
|
||||
action: str = "",
|
||||
tool: str = "",
|
||||
resource: str = "",
|
||||
command: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Classify an action using deterministic aliases and command patterns.
|
||||
|
||||
When several signals match, the class with the highest risk floor wins. An
|
||||
unknown side-effect-shaped tool is therefore never downgraded by benign text.
|
||||
"""
|
||||
|
||||
registry = _load_action_registry()
|
||||
classes = registry["classes"]
|
||||
candidates: list[tuple[str, str]] = []
|
||||
action_key = _normal(action)
|
||||
tool_key = _normal(tool)
|
||||
|
||||
action_class = registry.get("action_aliases", {}).get(action_key)
|
||||
if action_class:
|
||||
candidates.append(("action", action_class))
|
||||
tool_class = registry.get("tool_aliases", {}).get(tool_key)
|
||||
if tool_class:
|
||||
candidates.append(("tool", tool_class))
|
||||
combined = " ".join(part for part in (command, resource) if part)
|
||||
for rule in registry.get("command_patterns", []):
|
||||
try:
|
||||
if re.search(str(rule["pattern"]), combined, re.IGNORECASE):
|
||||
candidates.append(("command_or_resource", str(rule["class"])))
|
||||
except (KeyError, re.error, TypeError) as exc:
|
||||
raise ValueError("invalid action class pattern") from exc
|
||||
|
||||
shell_like = tool_key in {"bash", "shell", "exec", "run", "run_command", "terminal"}
|
||||
if not candidates and shell_like:
|
||||
candidates.append(("unknown_shell_command", "unknown"))
|
||||
if not candidates:
|
||||
candidates.append(("unclassified", "unknown"))
|
||||
|
||||
selected_source, selected_class = candidates[0]
|
||||
selected_risk = str(classes[selected_class]["risk_floor"])
|
||||
for source, candidate in candidates[1:]:
|
||||
risk = str(classes[candidate]["risk_floor"])
|
||||
if RISK_ORDER.get(risk, 3) > RISK_ORDER.get(selected_risk, 3):
|
||||
selected_source, selected_class, selected_risk = source, candidate, risk
|
||||
|
||||
metadata = classes[selected_class]
|
||||
return {
|
||||
"schema_version": registry.get("schema_version", POLICY_VERSION),
|
||||
"action_class": selected_class,
|
||||
"classification_source": selected_source,
|
||||
"risk_floor": selected_risk,
|
||||
"side_effect_level": metadata["side_effect_level"],
|
||||
"side_effecting": metadata["side_effect_level"] != "none",
|
||||
"requires_approval": bool(metadata["requires_approval"]),
|
||||
"actor_required": bool(metadata["actor_required"]),
|
||||
"evidence_required": bool(metadata["evidence_required"]),
|
||||
"matched_classes": [candidate for _source, candidate in candidates],
|
||||
}
|
||||
|
||||
|
||||
def evaluate_risk(
|
||||
action: str = "",
|
||||
tool: str = "",
|
||||
resource: str = "",
|
||||
command: str = "",
|
||||
content: str = "",
|
||||
actor: str = "",
|
||||
environment: str = "",
|
||||
) -> dict[str, Any]:
|
||||
classification = classify_action(action, tool, resource, command)
|
||||
actor_present = bool(str(actor or "").strip())
|
||||
identity_risk = "high" if classification["actor_required"] and not actor_present else "low"
|
||||
factors = {
|
||||
"content_risk": _content_risk(content),
|
||||
"action_risk": classification["risk_floor"],
|
||||
"resource_risk": _resource_risk(resource),
|
||||
"identity_risk": identity_risk,
|
||||
"environment_risk": _environment_risk(environment),
|
||||
}
|
||||
effective = "low"
|
||||
for factor in factors.values():
|
||||
effective = _higher(effective, factor)
|
||||
|
||||
reason_codes: list[str] = ["action_risk_floor_applied"]
|
||||
if classification["actor_required"] and not actor_present:
|
||||
decision = "deny"
|
||||
reason_codes.append("actor_identity_required")
|
||||
elif classification["requires_approval"] or effective in {"high", "critical"}:
|
||||
decision = "require_approval"
|
||||
reason_codes.append("explicit_approval_required")
|
||||
else:
|
||||
decision = "allow"
|
||||
reason_codes.append("risk_within_auto_approval_policy")
|
||||
|
||||
return {
|
||||
"schema_version": POLICY_VERSION,
|
||||
"policy_id": "casan.action-risk-floor",
|
||||
"policy_version": POLICY_VERSION,
|
||||
"decision": decision,
|
||||
"reason_codes": reason_codes,
|
||||
"effective_risk": effective,
|
||||
"risk_factors": factors,
|
||||
"actor_identity_present": actor_present,
|
||||
"evidence_requirement": "required" if classification["evidence_required"] else "standard",
|
||||
**classification,
|
||||
}
|
||||
|
||||
|
||||
def evaluate_registry_configuration(
|
||||
mode: str,
|
||||
profile: str,
|
||||
explicit_value: Optional[str],
|
||||
) -> dict[str, Any]:
|
||||
normalized_mode = _normal(mode)
|
||||
normalized_profile = _normal(profile)
|
||||
explicit = None if explicit_value is None else _normal(explicit_value) in {"1", "true", "yes", "on", "enabled"}
|
||||
production = normalized_profile in PRODUCTION_PROFILES
|
||||
|
||||
if explicit is False and production:
|
||||
return {
|
||||
"enabled": True,
|
||||
"valid": False,
|
||||
"unsafe_bypass": False,
|
||||
"severity": "critical",
|
||||
"reason_code": "h2_registry_bypass_forbidden",
|
||||
}
|
||||
if explicit is False:
|
||||
return {
|
||||
"enabled": False,
|
||||
"valid": normalized_profile in DEVELOPMENT_PROFILES,
|
||||
"unsafe_bypass": True,
|
||||
"severity": "high",
|
||||
"reason_code": "h2_registry_unsafe_development_bypass",
|
||||
}
|
||||
return {
|
||||
"enabled": True if explicit is None else explicit,
|
||||
"valid": True,
|
||||
"unsafe_bypass": False,
|
||||
"severity": "info",
|
||||
"reason_code": "h2_registry_default_enabled" if explicit is None and normalized_mode == "enforce" else "h2_registry_enabled",
|
||||
}
|
||||
|
||||
|
||||
def evaluate_failure_policy(
|
||||
*,
|
||||
side_effecting: bool,
|
||||
mode: str,
|
||||
profile: str,
|
||||
configured: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Select single-step failure handling without conflating telemetry success.
|
||||
|
||||
Invalid configuration fails closed. Production side effects may be made
|
||||
stricter, but they may never be configured as record-only.
|
||||
"""
|
||||
|
||||
normalized = _normal(configured or "")
|
||||
production = _normal(profile) in PRODUCTION_PROFILES
|
||||
enforce = _normal(mode) == "enforce"
|
||||
if normalized and normalized not in FAILURE_POLICIES:
|
||||
policy, source, valid = "halt", "invalid_policy_fail_closed", False
|
||||
elif normalized == "record_only" and side_effecting and production:
|
||||
policy, source, valid = "halt", "production_record_only_forbidden", False
|
||||
elif normalized:
|
||||
policy, source, valid = normalized, "configured", True
|
||||
elif side_effecting and (enforce or production):
|
||||
policy, source, valid = "halt", "safe_enforce_default", True
|
||||
else:
|
||||
policy, source, valid = "record_only", "observe_or_read_only_default", True
|
||||
return {
|
||||
"schema_version": POLICY_VERSION,
|
||||
"policy_id": "casan.single-step-failure",
|
||||
"policy_version": POLICY_VERSION,
|
||||
"failure_policy": policy,
|
||||
"source": source,
|
||||
"valid": valid,
|
||||
"side_effecting": side_effecting,
|
||||
"mode": _normal(mode) or "observe",
|
||||
"profile": _normal(profile) or "development",
|
||||
"execution_result": "failed",
|
||||
"telemetry_result": "recorded",
|
||||
"assurance_may_continue": policy == "record_only",
|
||||
"reason_codes": [source],
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class H2GateContext:
|
||||
mode: str
|
||||
actor: str
|
||||
action: str
|
||||
tool: str
|
||||
execution_id: str
|
||||
enforcement_path: str
|
||||
idempotency_key: str
|
||||
timeout_seconds: float = 8.0
|
||||
|
||||
|
||||
def _h2_result(context: H2GateContext, underlying: str, reason_code: str, detail: str) -> dict[str, Any]:
|
||||
enforce = _normal(context.mode) == "enforce"
|
||||
actual_deny = underlying == "deny"
|
||||
dependency_failure = underlying == "error"
|
||||
execution_allowed = not enforce or (not actual_deny and not dependency_failure)
|
||||
if enforce:
|
||||
decision = "allow" if execution_allowed else "deny"
|
||||
else:
|
||||
decision = "observe_only"
|
||||
return {
|
||||
"schema_version": POLICY_VERSION,
|
||||
"policy_id": "casan.h2.tool-registry",
|
||||
"policy_version": POLICY_VERSION,
|
||||
"decision": decision,
|
||||
"underlying_decision": underlying,
|
||||
"reason_codes": [reason_code],
|
||||
"reason_code": reason_code,
|
||||
"mode": _normal(context.mode) or "observe",
|
||||
"actor": context.actor or "unidentified",
|
||||
"action": context.action,
|
||||
"tool": context.tool,
|
||||
"execution_id": context.execution_id,
|
||||
"enforcement_path": context.enforcement_path,
|
||||
"execution_allowed": execution_allowed,
|
||||
"certifiable": enforce and execution_allowed,
|
||||
"assurance_status": "enforced" if enforce and execution_allowed else "denied" if enforce else "degraded",
|
||||
"severity": "high" if reason_code != "h2_ok" else "info",
|
||||
"detail": detail[:400],
|
||||
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
|
||||
def evaluate_h2_gate(
|
||||
gate_path: str,
|
||||
context: H2GateContext,
|
||||
env: Optional[Mapping[str, str]] = None,
|
||||
bash_path: str = "bash",
|
||||
) -> dict[str, Any]:
|
||||
"""Execute the H2 dependency and convert every outcome into one policy decision."""
|
||||
|
||||
path = Path(gate_path)
|
||||
if not path.is_file():
|
||||
return _h2_result(context, "error", "h2_gate_unavailable", "gate file is missing")
|
||||
try:
|
||||
mode_bits = stat.S_IMODE(path.stat().st_mode)
|
||||
except OSError as exc:
|
||||
return _h2_result(context, "error", "h2_gate_unavailable", f"gate stat failed: {type(exc).__name__}")
|
||||
if mode_bits & 0o444 == 0:
|
||||
return _h2_result(context, "error", "h2_gate_permission_denied", "gate file is not readable")
|
||||
resolved_bash = shutil.which(bash_path) if not os.path.isabs(bash_path) else bash_path
|
||||
if not resolved_bash or not Path(resolved_bash).is_file():
|
||||
return _h2_result(context, "error", "h2_gate_unavailable", "bash runtime is unavailable")
|
||||
|
||||
child_env: MutableMapping[str, str] = dict(os.environ)
|
||||
if env:
|
||||
child_env.update({str(key): str(value) for key, value in env.items()})
|
||||
child_env["CASAN_IDEMPOTENCY_KEY"] = context.idempotency_key
|
||||
if context.actor and not child_env.get("CASAN_AGENT"):
|
||||
child_env["CASAN_AGENT"] = context.actor
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[resolved_bash, str(path), context.action],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=max(0.001, context.timeout_seconds),
|
||||
env=child_env,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return _h2_result(context, "error", "h2_gate_timeout", "gate exceeded internal timeout")
|
||||
except (OSError, ValueError, subprocess.SubprocessError) as exc:
|
||||
return _h2_result(context, "error", "h2_gate_internal_error", type(exc).__name__)
|
||||
|
||||
stdout = proc.stdout.strip()[:4096]
|
||||
stderr = proc.stderr.strip()[:4096]
|
||||
if proc.returncode == 0 and re.search(r"\bTOOL_APPROVED\b", stdout):
|
||||
return _h2_result(context, "allow", "h2_ok", stdout)
|
||||
if re.search(r"\bTOOL_DENIED\b", stdout + "\n" + stderr):
|
||||
reason_match = re.search(r"reason=([^\s]+)", stdout + "\n" + stderr)
|
||||
reason = "h2_denied:%s" % (reason_match.group(1) if reason_match else "policy")
|
||||
return _h2_result(context, "deny", reason, stderr or stdout)
|
||||
if proc.returncode in {126, 127}:
|
||||
return _h2_result(context, "error", "h2_gate_unavailable", stderr or "gate runtime unavailable")
|
||||
if proc.returncode != 0:
|
||||
return _h2_result(context, "error", "h2_gate_internal_error", stderr or stdout or f"exit={proc.returncode}")
|
||||
return _h2_result(context, "error", "h2_gate_malformed_response", stdout or "empty response")
|
||||
|
||||
|
||||
def evaluate_trust_capabilities(env: Optional[Mapping[str, str]] = None) -> dict[str, Any]:
|
||||
values = dict(os.environ if env is None else env)
|
||||
profile = _normal(values.get("CASAN_PROFILE", ""))
|
||||
production = profile in PRODUCTION_PROFILES
|
||||
signing_provider = _normal(values.get("CASAN_SIGNING_PROVIDER", "local_openssl"))
|
||||
anchor_provider = _normal(values.get("CASAN_IMMUTABLE_ANCHOR_PROVIDER", "local_hash_chain"))
|
||||
emergency = _normal(values.get("CASAN_TRUST_EMERGENCY_OVERRIDE", "")) in {"1", "true", "yes", "on"}
|
||||
|
||||
signing_external = signing_provider in {"vault", "vault_kms", "kms", "hsm"}
|
||||
anchor_external = anchor_provider in {"s3_object_lock", "qldb", "external_worm"}
|
||||
vault_addr = str(values.get("VAULT_ADDR") or "")
|
||||
signing_configured = signing_external and vault_addr.startswith("https://") and bool(values.get("VAULT_TOKEN"))
|
||||
anchor_configured = anchor_external and bool(values.get("CASAN_S3_BUCKET")) and bool(values.get("CASAN_S3_KMS_KEY_ID"))
|
||||
ready = (not production) or (signing_configured and anchor_configured)
|
||||
certifiable = ready and not emergency
|
||||
reasons: list[str] = []
|
||||
if production and not signing_configured:
|
||||
reasons.append("external_signing_trust_root_required")
|
||||
if production and not anchor_configured:
|
||||
reasons.append("external_immutable_anchor_required")
|
||||
if emergency:
|
||||
reasons.append("emergency_local_trust_override_active")
|
||||
|
||||
return {
|
||||
"schema_version": POLICY_VERSION,
|
||||
"profile": profile or "development",
|
||||
"production": production,
|
||||
"signing_provider": signing_provider or "local_openssl",
|
||||
"signing_capability": "external" if signing_configured else "local_or_unavailable",
|
||||
"immutable_anchor_provider": anchor_provider or "local_hash_chain",
|
||||
"immutable_anchor_capability": "external" if anchor_configured else "local_or_unavailable",
|
||||
"ready": ready,
|
||||
"certifiable": certifiable,
|
||||
"emergency_override": emergency,
|
||||
"severity": "critical" if emergency or not ready else "info",
|
||||
"reason_codes": reasons or ["trust_capabilities_satisfied"],
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Runtime supervision capability negotiation without pretending support."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeCapabilities:
|
||||
canCancel: bool = False
|
||||
canPause: bool = False
|
||||
canResume: bool = False
|
||||
canRollback: bool = False
|
||||
canReduceAuthority: bool = False
|
||||
canQuarantine: bool = False
|
||||
|
||||
|
||||
CAPABILITY_FOR_INTERVENTION = {
|
||||
"cancel": "canCancel",
|
||||
"pause": "canPause",
|
||||
"resume": "canResume",
|
||||
"roll_back": "canRollback",
|
||||
"reduce_authority": "canReduceAuthority",
|
||||
"quarantine": "canQuarantine",
|
||||
}
|
||||
|
||||
|
||||
def negotiate_intervention(intervention_id: str, intervention: str, requested_by: str, capabilities: RuntimeCapabilities) -> dict[str, Any]:
|
||||
capability = CAPABILITY_FOR_INTERVENTION.get(intervention)
|
||||
supported = bool(capability and getattr(capabilities, capability))
|
||||
return {
|
||||
"intervention_id": intervention_id,
|
||||
"type": intervention,
|
||||
"requested_by": requested_by,
|
||||
"status": "pending" if supported else "unsupported",
|
||||
"reason_code": "runtime_intervention_supported_pending_adapter" if supported else "runtime_intervention_unsupported",
|
||||
"required_capability": capability,
|
||||
"runtime_capabilities": asdict(capabilities),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Typed H1-H7 namespaces. Legacy `Hn` labels remain display aliases only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AssuranceCategory(str, Enum):
|
||||
RUNTIME_CONTROL = "runtime_control"
|
||||
READINESS_CHECK = "readiness_check"
|
||||
REPORT_DIMENSION = "report_dimension"
|
||||
CERTIFICATION_CLAIM = "certification_claim"
|
||||
|
||||
|
||||
class _HNamespace(str, Enum):
|
||||
@property
|
||||
def legacy_id(self) -> str:
|
||||
return self.value.rsplit(".", 1)[-1]
|
||||
|
||||
|
||||
class RuntimeControl(_HNamespace):
|
||||
H1 = "RuntimeControl.H1"; H2 = "RuntimeControl.H2"; H3 = "RuntimeControl.H3"
|
||||
H4 = "RuntimeControl.H4"; H5 = "RuntimeControl.H5"; H6 = "RuntimeControl.H6"; H7 = "RuntimeControl.H7"
|
||||
|
||||
|
||||
class ReadinessCheck(_HNamespace):
|
||||
H1 = "ReadinessCheck.H1"; H2 = "ReadinessCheck.H2"; H3 = "ReadinessCheck.H3"
|
||||
H4 = "ReadinessCheck.H4"; H5 = "ReadinessCheck.H5"; H6 = "ReadinessCheck.H6"; H7 = "ReadinessCheck.H7"
|
||||
|
||||
|
||||
class ReportDimension(_HNamespace):
|
||||
H1 = "ReportDimension.H1"; H2 = "ReportDimension.H2"; H3 = "ReportDimension.H3"
|
||||
H4 = "ReportDimension.H4"; H5 = "ReportDimension.H5"; H6 = "ReportDimension.H6"; H7 = "ReportDimension.H7"
|
||||
|
||||
|
||||
class CertificationClaim(_HNamespace):
|
||||
H1 = "CertificationClaim.H1"; H2 = "CertificationClaim.H2"; H3 = "CertificationClaim.H3"
|
||||
H4 = "CertificationClaim.H4"; H5 = "CertificationClaim.H5"; H6 = "CertificationClaim.H6"; H7 = "CertificationClaim.H7"
|
||||
|
||||
|
||||
def same_legacy_label(left: _HNamespace, right: _HNamespace) -> bool:
|
||||
"""Compatibility display helper; never authorizes cross-category interpretation."""
|
||||
return left.legacy_id == right.legacy_id
|
||||
@@ -58,6 +58,10 @@
|
||||
"type": ["string", "null"],
|
||||
"description": "Client-native session id. Hashed by the bridge, never stored raw."
|
||||
},
|
||||
"actor": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Stable actor identity asserted by the authenticated runtime boundary."
|
||||
},
|
||||
"integration_mode": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["casan_owned", "managed_hook", "project_hook", "observed_only", null],
|
||||
@@ -167,7 +171,7 @@
|
||||
"schema_version": { "type": "string" },
|
||||
"decision": {
|
||||
"type": "string",
|
||||
"enum": ["allow", "block", "deny", "recorded", "certified", "non_certified", "error"]
|
||||
"enum": ["allow", "block", "deny", "require_approval", "halt", "quarantine", "recorded", "certified", "non_certified", "error"]
|
||||
},
|
||||
"admission_id": { "type": ["string", "null"] },
|
||||
"trace_id": { "type": ["string", "null"] },
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://casan.dev/schemas/assurance-kernel/1.0.0",
|
||||
"title": "CASAN Assurance Kernel Execution Envelope",
|
||||
"description": "Framework-independent canonical wire contract. Runtime-specific fields belong under a namespaced extensions object.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "extension_namespace", "run", "actors", "steps", "evidence", "trace_links", "extensions"],
|
||||
"properties": {
|
||||
"schema_version": {"const": "1.0.0"},
|
||||
"extension_namespace": {"type": "string", "pattern": "^[a-z][a-z0-9.-]+$"},
|
||||
"run": {"$ref": "#/$defs/ExecutionRun"},
|
||||
"actors": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/Actor"}},
|
||||
"steps": {"type": "array", "items": {"$ref": "#/$defs/ExecutionStep"}},
|
||||
"delegations": {"type": "array", "items": {"$ref": "#/$defs/Delegation"}},
|
||||
"context_items": {"type": "array", "items": {"$ref": "#/$defs/ContextItem"}},
|
||||
"runtime_constraints": {"type": "array", "items": {"$ref": "#/$defs/RuntimeConstraint"}},
|
||||
"runtime_capabilities": {"$ref": "#/$defs/RuntimeCapabilities"},
|
||||
"approvals": {"type": "array", "items": {"$ref": "#/$defs/Approval"}},
|
||||
"interventions": {"type": "array", "items": {"$ref": "#/$defs/Intervention"}},
|
||||
"claims": {"type": "array", "items": {"$ref": "#/$defs/Claim"}},
|
||||
"evidence": {"type": "array", "items": {"$ref": "#/$defs/EvidenceItem"}},
|
||||
"trace_links": {"type": "array", "items": {"$ref": "#/$defs/TraceLink"}},
|
||||
"extensions": {"type": "object", "additionalProperties": {"type": "object"}}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"Timestamp": {"type": "string", "format": "date-time"},
|
||||
"Outcome": {
|
||||
"type": "object",
|
||||
"required": ["execution_result", "assurance_result", "certification_result", "business_result"],
|
||||
"properties": {
|
||||
"execution_result": {"enum": ["pending", "success", "failed", "cancelled", "quarantined", "unknown"]},
|
||||
"assurance_result": {"enum": ["pending", "passed", "failed", "degraded", "not_evaluated", "unknown"]},
|
||||
"certification_result": {"enum": ["pending", "certified", "non_certified", "ineligible"]},
|
||||
"business_result": {"enum": ["achieved", "not_achieved", "partial", "not_evaluated", "unknown"]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExecutionRun": {
|
||||
"type": "object",
|
||||
"required": ["run_id", "tenant", "project", "environment", "runtime", "mode", "requested_goal", "started_at", "status", "risk_summary", "correlation_id", "outcome"],
|
||||
"properties": {
|
||||
"run_id": {"type": "string", "minLength": 1},
|
||||
"parent_run_id": {"type": ["string", "null"]},
|
||||
"tenant": {"type": "string"},
|
||||
"project": {"type": "string"},
|
||||
"environment": {"type": "string"},
|
||||
"runtime": {"type": "object", "required": ["type", "version"], "properties": {"type": {"type": "string"}, "version": {"type": "string"}}, "additionalProperties": false},
|
||||
"mode": {"enum": ["observe", "enforce"]},
|
||||
"requested_goal": {"type": "string"},
|
||||
"started_at": {"$ref": "#/$defs/Timestamp"},
|
||||
"completed_at": {"anyOf": [{"$ref": "#/$defs/Timestamp"}, {"type": "null"}]},
|
||||
"status": {"enum": ["pending", "running", "success", "failed", "cancelled", "quarantined", "unknown"]},
|
||||
"risk_summary": {"type": "object"},
|
||||
"evidence_manifest_ref": {"type": ["string", "null"]},
|
||||
"correlation_id": {"type": "string", "minLength": 1},
|
||||
"outcome": {"$ref": "#/$defs/Outcome"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExecutionStep": {
|
||||
"type": "object",
|
||||
"required": ["step_id", "run_id", "sequence", "actor_ref", "action", "input_context_refs", "policy_decisions", "verification_results", "evidence_refs", "outcome", "started_at", "completed_at"],
|
||||
"properties": {
|
||||
"step_id": {"type": "string"}, "run_id": {"type": "string"}, "parent_step_id": {"type": ["string", "null"]}, "sequence": {"type": "integer", "minimum": 1},
|
||||
"actor_ref": {"type": "string"}, "action": {"$ref": "#/$defs/Action"}, "resource": {"type": ["object", "string", "null"]},
|
||||
"input_context_refs": {"type": "array", "items": {"type": "string"}}, "policy_decisions": {"type": "array", "items": {"$ref": "#/$defs/PolicyDecision"}},
|
||||
"tool_invocation": {"type": ["object", "null"]}, "verification_results": {"type": "array", "items": {"$ref": "#/$defs/VerificationResult"}},
|
||||
"evidence_refs": {"type": "array", "items": {"type": "string"}}, "outcome": {"$ref": "#/$defs/Outcome"},
|
||||
"started_at": {"$ref": "#/$defs/Timestamp"}, "completed_at": {"anyOf": [{"$ref": "#/$defs/Timestamp"}, {"type": "null"}]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Actor": {
|
||||
"type": "object",
|
||||
"required": ["actor_id", "actor_type", "issuer", "trust_level"],
|
||||
"properties": {
|
||||
"actor_id": {"type": "string"}, "actor_type": {"enum": ["human", "agent", "child_agent", "model", "tool", "service_account", "runtime", "approver", "policy_engine"]},
|
||||
"issuer": {"type": "string"}, "trust_level": {"enum": ["unverified", "asserted", "verified", "hardware_backed"]}, "authentication_evidence_ref": {"type": ["string", "null"]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Delegation": {
|
||||
"type": "object",
|
||||
"required": ["delegation_id", "delegator_ref", "delegate_ref", "allowed_authority", "resource_scope", "expiry", "maximum_depth", "revoked"],
|
||||
"properties": {
|
||||
"delegation_id": {"type": "string"}, "delegator_ref": {"type": "string"}, "delegate_ref": {"type": "string"}, "allowed_authority": {"type": "array", "items": {"type": "string"}},
|
||||
"resource_scope": {"type": "array", "items": {"type": "string"}}, "expiry": {"$ref": "#/$defs/Timestamp"}, "maximum_depth": {"type": "integer", "minimum": 0},
|
||||
"parent_delegation_ref": {"type": ["string", "null"]}, "revoked": {"type": "boolean"}, "approval_ref": {"type": ["string", "null"]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ContextItem": {
|
||||
"type": "object", "required": ["context_id", "source", "trust_classification", "content_hash", "transform_history", "compression_lineage", "classification", "injection_scan_result"],
|
||||
"properties": {
|
||||
"context_id": {"type": "string"}, "source": {"type": "string"}, "trust_classification": {"enum": ["trusted", "untrusted", "mixed", "unknown"]}, "content_hash": {"type": "string"},
|
||||
"transform_history": {"type": "array", "items": {"type": "object"}}, "compression_lineage": {"type": "array", "items": {"type": "string"}},
|
||||
"origin_ref": {"type": ["string", "null"]}, "classification": {"enum": ["instruction", "data", "mixed", "unknown"]}, "injection_scan_result": {"type": "object"}
|
||||
}, "additionalProperties": false
|
||||
},
|
||||
"Action": {
|
||||
"type": "object", "required": ["class", "name", "side_effect_level"],
|
||||
"properties": {"class": {"enum": ["read_only", "write", "delete", "database_mutation", "migration", "deployment", "release", "credential_access", "identity_permission_modification", "external_network_side_effect", "infrastructure_modification", "unknown"]}, "name": {"type": "string"}, "tool": {"type": ["string", "null"]}, "side_effect_level": {"enum": ["none", "sensitive_read", "write", "destructive", "external", "unknown"]}, "required_authority": {"type": ["string", "null"]}, "requested_operation": {"type": ["string", "null"]}, "environment": {"type": ["string", "null"]}},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ResourceAccess": {
|
||||
"type": "object", "required": ["resource", "operation", "side_effect_level", "environment"],
|
||||
"properties": {"resource": {"type": "string"}, "operation": {"type": "string"}, "side_effect_level": {"type": "string"}, "required_authority": {"type": ["string", "null"]}, "environment": {"type": "string"}}, "additionalProperties": false
|
||||
},
|
||||
"PolicyDecision": {
|
||||
"type": "object", "required": ["policy_id", "policy_version", "decision", "reason_codes", "effective_risk", "input_facts", "enforcement_point", "timestamp", "decision_engine_identity"],
|
||||
"properties": {"policy_id": {"type": "string"}, "policy_version": {"type": "string"}, "decision": {"enum": ["allow", "deny", "quarantine", "require_approval", "transform", "observe_only"]}, "reason_codes": {"type": "array", "items": {"type": "string"}}, "effective_risk": {"enum": ["low", "medium", "high", "critical"]}, "input_facts": {"type": "object"}, "enforcement_point": {"type": "string"}, "timestamp": {"$ref": "#/$defs/Timestamp"}, "decision_engine_identity": {"type": "string"}, "evidence_ref": {"type": ["string", "null"]}}, "additionalProperties": false
|
||||
},
|
||||
"RuntimeConstraint": {
|
||||
"type": "object", "required": ["constraint_id", "kind", "limit", "enforcement"],
|
||||
"properties": {"constraint_id": {"type": "string"}, "kind": {"enum": ["iteration", "time", "cost", "token", "child_agent_count", "graph_depth", "retry", "repetition", "network", "filesystem", "tool", "environment"]}, "limit": {}, "enforcement": {"enum": ["hard", "soft", "observe"]}}, "additionalProperties": false
|
||||
},
|
||||
"RuntimeCapabilities": {
|
||||
"type": "object",
|
||||
"required": ["canCancel", "canPause", "canResume", "canRollback", "canReduceAuthority", "canQuarantine"],
|
||||
"properties": {"canCancel": {"type": "boolean"}, "canPause": {"type": "boolean"}, "canResume": {"type": "boolean"}, "canRollback": {"type": "boolean"}, "canReduceAuthority": {"type": "boolean"}, "canQuarantine": {"type": "boolean"}},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"VerificationRequirement": {
|
||||
"type": "object", "required": ["requirement_id", "validator", "expected_result", "independence_level", "failure_severity"],
|
||||
"properties": {"requirement_id": {"type": "string"}, "validator": {"type": "string"}, "expected_result": {}, "independence_level": {"enum": ["same_runtime", "independent_process", "independent_service", "external_authority"]}, "failure_severity": {"enum": ["info", "warning", "error", "critical"]}}, "additionalProperties": false
|
||||
},
|
||||
"VerificationResult": {
|
||||
"type": "object", "required": ["requirement_id", "validator", "expected_result", "actual_result", "status", "evidence_refs", "independence_level", "failure_severity"],
|
||||
"properties": {"requirement_id": {"type": "string"}, "validator": {"type": "string"}, "expected_result": {}, "actual_result": {}, "status": {"enum": ["passed", "failed", "degraded", "not_run"]}, "evidence_refs": {"type": "array", "items": {"type": "string"}}, "independence_level": {"type": "string"}, "failure_severity": {"type": "string"}}, "additionalProperties": false
|
||||
},
|
||||
"Claim": {
|
||||
"type": "object", "required": ["claim_id", "statement", "evidence_refs", "validation_status"],
|
||||
"properties": {"claim_id": {"type": "string"}, "statement": {"type": "string"}, "evidence_refs": {"type": "array", "items": {"type": "string"}}, "validation_status": {"enum": ["validated", "rejected", "insufficient", "not_validated"]}}, "additionalProperties": false
|
||||
},
|
||||
"EvidenceItem": {
|
||||
"type": "object", "required": ["evidence_id", "claim_refs", "integrity", "producer_identity", "timestamp", "artifact_ref", "validation_status", "retention_class"],
|
||||
"properties": {"evidence_id": {"type": "string"}, "claim_refs": {"type": "array", "items": {"type": "string"}}, "integrity": {"type": "object", "required": ["algorithm", "digest"], "properties": {"algorithm": {"const": "sha256"}, "digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, "source_content_hash": {"type": ["string", "null"]}}, "additionalProperties": false}, "producer_identity": {"type": "string"}, "timestamp": {"$ref": "#/$defs/Timestamp"}, "artifact_ref": {"type": "string"}, "validation_status": {"enum": ["valid", "invalid", "unverified", "unavailable"]}, "retention_class": {"type": "string"}, "category": {"enum": ["runtime_control", "readiness_check", "report_dimension", "certification_claim"]}}, "additionalProperties": false
|
||||
},
|
||||
"TraceLink": {
|
||||
"type": "object", "required": ["type", "from", "to"],
|
||||
"properties": {"type": {"enum": ["requirement_to_policy", "policy_to_decision", "decision_to_action", "action_to_artifact", "artifact_to_verification", "verification_to_evidence", "evidence_to_outcome", "parent_to_child_execution"]}, "from": {"type": "string"}, "to": {"type": "string"}}, "additionalProperties": false
|
||||
},
|
||||
"Approval": {
|
||||
"type": "object", "required": ["approval_id", "decision", "approver_ref", "timestamp"],
|
||||
"properties": {"approval_id": {"type": "string"}, "decision": {"enum": ["approve", "reject"]}, "approver_ref": {"type": "string"}, "reason": {"type": ["string", "null"]}, "timestamp": {"$ref": "#/$defs/Timestamp"}}, "additionalProperties": false
|
||||
},
|
||||
"Intervention": {
|
||||
"type": "object", "required": ["intervention_id", "type", "requested_by", "status", "timestamp"],
|
||||
"properties": {"intervention_id": {"type": "string"}, "type": {"enum": ["pause", "resume", "cancel", "quarantine", "reduce_authority", "redirect", "roll_back"]}, "requested_by": {"type": "string"}, "status": {"enum": ["applied", "rejected", "unsupported", "pending"]}, "timestamp": {"$ref": "#/$defs/Timestamp"}}, "additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,10 @@ LATENCY_MS=$((END_MS - START_MS))
|
||||
if [[ ! -f "$OUTPUT_FILE" ]]; then
|
||||
STATUS="failed"
|
||||
ERROR_MSG="${ERROR_MSG:-output file not produced}"
|
||||
# A zero command exit does not make the step successful when the runtime
|
||||
# contract requires an output artifact and none was produced. Telemetry can
|
||||
# record this failure successfully, but must propagate a failed step outcome.
|
||||
[[ "$EXIT_CODE" -eq 0 ]] && EXIT_CODE=1
|
||||
: > "$OUTPUT_FILE"
|
||||
fi
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ fi
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
PROJECT_ROOT="$CASAN_APP_ROOT"
|
||||
KERNEL_CLI="$CASAN_HARNESS_ROOT/scripts/python/kernel_cli.py"
|
||||
# SEC-23 (MT-01): make state (control-plane settings, telemetry, audit) tenant-scoped
|
||||
# when CASAN_TENANT_ID is set, so a run for tenant A never touches tenant B's state.
|
||||
# No-op when no tenant is set (baseline unchanged); invalid tenant fails closed.
|
||||
@@ -56,12 +57,52 @@ write_phase_report() {
|
||||
"$ACTION_NAME" "$CACHE_STATUS" "$PHASE_LOG" > "$PHASE_REPORT" 2>/dev/null || true
|
||||
}
|
||||
|
||||
emit_failed_kernel() { # phase rc — best effort, never masks the original failure
|
||||
local failed_phase="$1" failed_rc="$2"
|
||||
[[ -f "$KERNEL_CLI" ]] || return 0
|
||||
local run_id="${EXECUTION_ID:-native-failed-${TRACE_SUFFIX:-$$}}"
|
||||
local mode="${NATIVE_MODE:-observe}"
|
||||
local event bundle path
|
||||
event="$(CASAN_NATIVE_RISK="${ACTION_RISK_JSON:-}" python3 - "$run_id" "$ACTION_NAME" "$mode" "$failed_phase" "$failed_rc" "${CASAN_ACTOR:-${CASAN_AGENT:-}}" <<'PY'
|
||||
import json, os, sys
|
||||
run_id, action, mode, phase, rc, actor = sys.argv[1:]
|
||||
try: risk = json.loads(os.environ.get("CASAN_NATIVE_RISK") or "{}")
|
||||
except ValueError: risk = {}
|
||||
print(json.dumps({
|
||||
"run_id": run_id, "correlation_id": run_id, "action": action,
|
||||
"tool": action, "actor": actor, "mode": mode,
|
||||
"environment": os.environ.get("CASAN_PROFILE", "development"),
|
||||
"execution_status": "failed", "assurance_status": "failed",
|
||||
"certification_status": "non_certified", "policy_decisions": [risk] if risk else [],
|
||||
"extensions": {"failed_phase": phase, "exit_code": int(rc)},
|
||||
}, separators=(",", ":")))
|
||||
PY
|
||||
)" || return 0
|
||||
bundle="$(printf '%s' "$event" | python3 "$KERNEL_CLI" adapt-native - 2>/dev/null)" || return 0
|
||||
path="$CASAN_STATE_ROOT/logs/kernel/$run_id.json"
|
||||
CASAN_KERNEL_BUNDLE="$bundle" python3 - "$path" <<'PY' || return 0
|
||||
import json, os, sys, tempfile
|
||||
path = sys.argv[1]; payload = json.loads(os.environ["CASAN_KERNEL_BUNDLE"])
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(prefix=".kernel-", dir=os.path.dirname(path))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, sort_keys=True, separators=(",", ":")); handle.write("\n")
|
||||
handle.flush(); os.fsync(handle.fileno())
|
||||
os.replace(tmp, path)
|
||||
finally:
|
||||
try: os.unlink(tmp)
|
||||
except OSError: pass
|
||||
PY
|
||||
}
|
||||
|
||||
run_phase() { # <phase-name> <command...> — preserves the failing rc exactly
|
||||
local phase="$1"; shift
|
||||
local rc=0
|
||||
"$@" || rc=$?
|
||||
record_phase "$phase" "$rc"
|
||||
if [[ "$rc" -ne 0 ]]; then
|
||||
emit_failed_kernel "$phase" "$rc"
|
||||
write_phase_report
|
||||
exit "$rc"
|
||||
fi
|
||||
@@ -76,9 +117,16 @@ hash_text() {
|
||||
}
|
||||
|
||||
CMD_STR="${*:-no_cmd}"
|
||||
NATIVE_MODE="${CASAN_ENFORCEMENT_MODE:-}"
|
||||
if [[ -z "$NATIVE_MODE" ]]; then
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then NATIVE_MODE="enforce"; else NATIVE_MODE="observe"; fi
|
||||
fi
|
||||
case "$NATIVE_MODE" in observe|enforce) : ;; *) NATIVE_MODE="enforce"; casan_log error harness "INVALID_ENFORCEMENT_MODE fail_closed=enforce" ;; esac
|
||||
INPUT_HASH="$(cat "$INPUT_FILE" | hash_text)"
|
||||
CMD_HASH="$(printf '%s' "$CMD_STR" | hash_text)"
|
||||
IDEMPOTENCY_KEY="$(printf '%s|%s|%s' "$INPUT_HASH" "$CMD_HASH" "$ACTION_NAME" | hash_text)"
|
||||
EXECUTION_ID="${CASAN_EXECUTION_ID:-native-${IDEMPOTENCY_KEY:0:24}}"
|
||||
export CASAN_EXECUTION_ID="$EXECUTION_ID"
|
||||
CACHE_META="$CACHE_DIR/$IDEMPOTENCY_KEY.json"
|
||||
CACHE_OUT="$CACHE_DIR/$IDEMPOTENCY_KEY.output"
|
||||
|
||||
@@ -92,7 +140,7 @@ casan_log debug harness "action=$ACTION_NAME input=$INPUT_FILE output=$FINAL_OUT
|
||||
# C7: honor an engaged kill-switch before doing any work (incident containment).
|
||||
# Opt-in (default off) so the baseline is unchanged. SEC-17 (ARCH-03): under
|
||||
# CASAN_PROFILE=prod it defaults ON (secure-by-default); an explicit =0 still wins.
|
||||
if [[ "${CASAN_KILLSWITCH_ENFORCE:-0}" == "1" || ( -z "${CASAN_KILLSWITCH_ENFORCE+x}" && "${CASAN_PROFILE:-}" == "prod" ) ]]; then
|
||||
if [[ "${CASAN_KILLSWITCH_ENFORCE:-0}" == "1" || ( -z "${CASAN_KILLSWITCH_ENFORCE+x}" && ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ) ) ]]; then
|
||||
KS_SCOPE="${CASAN_KILLSWITCH_SCOPE:-project}"
|
||||
KS_ID="${CASAN_KILLSWITCH_ID:-${CASAN_PROJECT:-current}}"
|
||||
if ! bash "$SCRIPT_DIR/kill-switch.sh" check "$KS_SCOPE" "$KS_ID" >/dev/null 2>&1; then
|
||||
@@ -116,7 +164,7 @@ fi
|
||||
# signed manifest and REFUSE to run on any drift — editing a gate/policy is a bypass
|
||||
# that leaves no input trace. Only active when a manifest is provisioned (so dev and
|
||||
# prod-without-a-manifest are unaffected); a present-but-drifted bundle fails closed.
|
||||
if [[ ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ) \
|
||||
if [[ ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" || "${CASAN_VERIFY_STRICT:-}" == "1" ) \
|
||||
&& -f "$SCRIPT_DIR/bundle-integrity.py" ]]; then
|
||||
BUNDLE_MANIFEST="${CASAN_BUNDLE_MANIFEST:-$CASAN_GOVERNANCE_ROOT/harness-bundle-manifest.json}"
|
||||
if [[ -f "$BUNDLE_MANIFEST" ]]; then
|
||||
@@ -132,15 +180,62 @@ fi
|
||||
run_phase "H4-in" "$SCRIPT_DIR/security-check.sh" "$INPUT_FILE" "$SAFE_INPUT" input
|
||||
run_phase "H5" "$SCRIPT_DIR/governance-check.sh" "$SAFE_INPUT" "$APPROVED_INPUT" "$ACTION_NAME"
|
||||
|
||||
# H2 tool registry gate is in the line of fire for side-effecting actions:
|
||||
# it enforces idempotency key, per-agent permission, and rollback strategy
|
||||
# before the command is allowed to execute. The wrapper already derived a
|
||||
# content-addressed idempotency key above.
|
||||
case "$ACTION_NAME" in
|
||||
write_code|migration|deploy|db_write|external_api|write_file)
|
||||
run_phase "H2-gate" env CASAN_IDEMPOTENCY_KEY="$IDEMPOTENCY_KEY" "$SCRIPT_DIR/tool-registry-gate.sh" "$ACTION_NAME"
|
||||
;;
|
||||
esac
|
||||
# Canonical action classification decides whether the H2 registry and isolated
|
||||
# executor are required. A classifier failure is treated as unknown/high-risk.
|
||||
ACTION_RISK_JSON=""
|
||||
ACTION_RISK_RC=0
|
||||
ACTION_RISK_JSON="$(python3 "$KERNEL_CLI" risk --action "$ACTION_NAME" --tool "$ACTION_NAME" \
|
||||
--command "$CMD_STR" --content-file "$SAFE_INPUT" --actor "${CASAN_ACTOR:-${CASAN_AGENT:-}}" \
|
||||
--environment "${CASAN_PROFILE:-development}" 2>/dev/null)" || ACTION_RISK_RC=$?
|
||||
if [[ "$ACTION_RISK_RC" -eq 0 && -n "$ACTION_RISK_JSON" ]]; then
|
||||
ACTION_FIELDS="$(python3 - "$ACTION_RISK_JSON" <<'PY'
|
||||
import json, sys
|
||||
p=json.loads(sys.argv[1])
|
||||
print("%s\t%s" % ("1" if p["side_effecting"] else "0", p["action_class"]))
|
||||
PY
|
||||
)" || ACTION_RISK_RC=$?
|
||||
fi
|
||||
if [[ "$ACTION_RISK_RC" -eq 0 && -n "${ACTION_FIELDS:-}" ]]; then
|
||||
IFS=$'\t' read -r SIDE_EFFECTING ACTION_CLASS <<< "$ACTION_FIELDS"
|
||||
else
|
||||
SIDE_EFFECTING=1
|
||||
ACTION_CLASS="unknown"
|
||||
casan_log error harness "ACTION_CLASSIFIER_FAILED_CLOSED action=$ACTION_NAME"
|
||||
fi
|
||||
|
||||
if [[ "$SIDE_EFFECTING" == "1" ]]; then
|
||||
REGISTRY_ARGS=(registry-config --mode "$NATIVE_MODE" --profile "${CASAN_PROFILE:-development}" \
|
||||
--evidence-log "$CASAN_STATE_ROOT/logs/policy/h2-registry-config.jsonl")
|
||||
[[ -n "${CASAN_H2_REGISTRY+x}" ]] && REGISTRY_ARGS+=(--explicit "$CASAN_H2_REGISTRY")
|
||||
REGISTRY_RC=0
|
||||
REGISTRY_JSON="$(python3 "$KERNEL_CLI" "${REGISTRY_ARGS[@]}")" || REGISTRY_RC=$?
|
||||
if [[ "$REGISTRY_RC" -ne 0 ]]; then
|
||||
record_phase "H2-config" "$REGISTRY_RC"
|
||||
echo "H2_REGISTRY_CONFIGURATION_DENIED $REGISTRY_JSON" >&2
|
||||
write_phase_report
|
||||
exit "$REGISTRY_RC"
|
||||
fi
|
||||
REGISTRY_ENABLED="$(python3 -c 'import json,sys; print("1" if json.load(sys.stdin)["enabled"] else "0")' <<< "$REGISTRY_JSON")"
|
||||
if [[ "$REGISTRY_ENABLED" == "1" ]]; then
|
||||
H2_ACTION="$ACTION_NAME"
|
||||
case "$ACTION_CLASS" in
|
||||
write) H2_ACTION="write_file" ;;
|
||||
delete) H2_ACTION="delete_file" ;;
|
||||
database_mutation) H2_ACTION="db_write" ;;
|
||||
deployment|release|infrastructure_modification) H2_ACTION="deploy" ;;
|
||||
external_network_side_effect) H2_ACTION="external_api" ;;
|
||||
unknown) H2_ACTION="unknown_tool" ;;
|
||||
esac
|
||||
run_phase "H2-gate" python3 "$KERNEL_CLI" h2-gate \
|
||||
--gate "${CASAN_H2_GATE_PATH:-$SCRIPT_DIR/tool-registry-gate.sh}" --mode "$NATIVE_MODE" \
|
||||
--actor "${CASAN_ACTOR:-${CASAN_AGENT:-}}" --action "$H2_ACTION" --tool "$ACTION_NAME" \
|
||||
--execution-id "$EXECUTION_ID" --enforcement-path "native_harness.pre_execution.h2_registry" \
|
||||
--idempotency-key "$IDEMPOTENCY_KEY" --timeout "${CASAN_H2_GATE_TIMEOUT_SECONDS:-8}" \
|
||||
--evidence-log "$CASAN_STATE_ROOT/logs/policy/h2-decisions.jsonl"
|
||||
else
|
||||
casan_log warn harness "HIGH H2 registry unsafe development/test bypass active; run is non-certifiable"
|
||||
fi
|
||||
fi
|
||||
|
||||
# T4: propagate step name so any nested model calls (model-call.py) log against the same step
|
||||
# name, enabling provider-cost-lookup.py to match real Ollama token counts in agent-metrics.sh.
|
||||
@@ -155,13 +250,34 @@ if [[ -f "$CACHE_META" && -f "$CACHE_OUT" ]]; then
|
||||
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- bash -c 'cp "$1" "$CASAN_OUTPUT"' _ "$CACHE_OUT"
|
||||
elif [[ "$#" -gt 0 ]]; then
|
||||
CACHE_STATUS="stored"
|
||||
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
|
||||
"$SCRIPT_DIR/tool-exec.sh" "$TOOL_TIMEOUT" -- "$@"
|
||||
if [[ "$SIDE_EFFECTING" == "1" && "$NATIVE_MODE" == "enforce" ]]; then
|
||||
run_phase "H6-exec" env CASAN_ENFORCEMENT_MODE="$NATIVE_MODE" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
|
||||
"$SCRIPT_DIR/sandbox-run.sh" --workspace "$PROJECT_ROOT" --timeout "$TOOL_TIMEOUT" -- "$@"
|
||||
else
|
||||
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
|
||||
"$SCRIPT_DIR/tool-exec.sh" "$TOOL_TIMEOUT" -- "$@"
|
||||
fi
|
||||
else
|
||||
CACHE_STATUS="stored"
|
||||
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT"
|
||||
fi
|
||||
|
||||
TOOL_OUTPUT_MAX_BYTES="${CASAN_TOOL_OUTPUT_MAX_BYTES:-1048576}"
|
||||
if [[ ! "$TOOL_OUTPUT_MAX_BYTES" =~ ^[1-9][0-9]*$ ]]; then
|
||||
casan_log error harness "TOOL_OUTPUT_LIMIT_INVALID value=$TOOL_OUTPUT_MAX_BYTES"
|
||||
: > "$FINAL_OUTPUT"
|
||||
exit 2
|
||||
fi
|
||||
RAW_OUTPUT_BYTES="$(wc -c < "$RAW_OUTPUT" | tr -d ' ')"
|
||||
if [[ "$RAW_OUTPUT_BYTES" -gt "$TOOL_OUTPUT_MAX_BYTES" ]]; then
|
||||
record_phase "H4-output-size" 2
|
||||
casan_log error harness "TOOL_OUTPUT_QUARANTINED bytes=$RAW_OUTPUT_BYTES limit=$TOOL_OUTPUT_MAX_BYTES"
|
||||
: > "$FINAL_OUTPUT"
|
||||
write_phase_report
|
||||
echo "TOOL_OUTPUT_QUARANTINED reason=output_size_limit bytes=$RAW_OUTPUT_BYTES limit=$TOOL_OUTPUT_MAX_BYTES" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# V7: tool output can carry indirect injection that would re-enter a downstream
|
||||
# model's context. Scan RAW_OUTPUT for injection/secret patterns before it is
|
||||
# reused. Mode: off | warn (default) | block. Strict mode upgrades to block.
|
||||
@@ -170,7 +286,7 @@ fi
|
||||
TOOL_OUTPUT_SCAN_MODE="${CASAN_TOOL_OUTPUT_SCAN:-}"
|
||||
if [[ -z "$TOOL_OUTPUT_SCAN_MODE" ]]; then
|
||||
# SEC-17/M-02: prod profile defaults tool-output scanning to block (fail-closed).
|
||||
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" || ( -z "${CASAN_SECURITY_STRICT+x}" && "${CASAN_PROFILE:-}" == "prod" ) ]]; then TOOL_OUTPUT_SCAN_MODE="block"; else TOOL_OUTPUT_SCAN_MODE="warn"; fi
|
||||
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" || ( -z "${CASAN_SECURITY_STRICT+x}" && ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ) ) ]]; then TOOL_OUTPUT_SCAN_MODE="block"; else TOOL_OUTPUT_SCAN_MODE="warn"; fi
|
||||
fi
|
||||
if [[ "$TOOL_OUTPUT_SCAN_MODE" != "off" ]]; then
|
||||
TOS_RC=0
|
||||
@@ -204,6 +320,65 @@ EOF
|
||||
cp "$FINAL_OUTPUT" "$CACHE_OUT"
|
||||
fi
|
||||
|
||||
# Dual-emit the framework-independent kernel envelope. Legacy phase reports and
|
||||
# outputs remain unchanged; the canonical contract is an additive artifact.
|
||||
TRUST_JSON="$(python3 "$KERNEL_CLI" trust-capabilities 2>/dev/null || true)"
|
||||
RUN_CERTIFICATION="non_certified"
|
||||
RUN_ASSURANCE="passed"
|
||||
[[ "$NATIVE_MODE" == "observe" ]] && RUN_ASSURANCE="degraded"
|
||||
if [[ "$NATIVE_MODE" == "enforce" && -n "$TRUST_JSON" ]]; then
|
||||
TRUST_CERTIFIABLE="$(python3 -c 'import json,sys; print("1" if json.load(sys.stdin).get("certifiable") else "0")' <<< "$TRUST_JSON" 2>/dev/null || echo 0)"
|
||||
REGISTRY_BYPASS="$(printf '%s' "${REGISTRY_JSON:-{}}" | python3 -c 'import json,sys; print("1" if json.load(sys.stdin).get("unsafe_bypass") else "0")' 2>/dev/null || echo 0)"
|
||||
[[ "$REGISTRY_BYPASS" == "1" ]] && RUN_ASSURANCE="degraded"
|
||||
[[ "$TRUST_CERTIFIABLE" == "1" && "$REGISTRY_BYPASS" == "0" ]] && RUN_CERTIFICATION="certified"
|
||||
fi
|
||||
NATIVE_EVENT="$(CASAN_NATIVE_RISK="$ACTION_RISK_JSON" python3 - "$EXECUTION_ID" "$ACTION_NAME" "$ACTION_CLASS" "$NATIVE_MODE" "$RUN_ASSURANCE" "$RUN_CERTIFICATION" "${CASAN_ACTOR:-${CASAN_AGENT:-}}" "$INPUT_HASH" "$CMD_HASH" <<'PY'
|
||||
import json, os, sys
|
||||
run_id, action, action_class, mode, assurance, certification, actor, input_hash, command_hash = sys.argv[1:]
|
||||
try:
|
||||
risk = json.loads(os.environ.get("CASAN_NATIVE_RISK") or "{}")
|
||||
except ValueError:
|
||||
risk = {}
|
||||
print(json.dumps({
|
||||
"run_id": run_id, "correlation_id": run_id, "action": action,
|
||||
"tool": action, "actor": actor, "mode": mode,
|
||||
"environment": os.environ.get("CASAN_PROFILE", "development"),
|
||||
"execution_status": "success", "assurance_status": assurance,
|
||||
"certification_status": certification,
|
||||
"policy_decisions": [risk] if risk else [],
|
||||
"extensions": {"input_hash": input_hash, "command_hash": command_hash, "action_class": action_class},
|
||||
}, separators=(",", ":")))
|
||||
PY
|
||||
)"
|
||||
KERNEL_RC=0
|
||||
KERNEL_BUNDLE="$(printf '%s' "$NATIVE_EVENT" | python3 "$KERNEL_CLI" adapt-native -)" || KERNEL_RC=$?
|
||||
if [[ "$KERNEL_RC" -ne 0 ]]; then
|
||||
casan_log error harness "KERNEL_CONTRACT_EMISSION_FAILED rc=$KERNEL_RC"
|
||||
if [[ "$NATIVE_MODE" == "enforce" ]]; then
|
||||
: > "$FINAL_OUTPUT"
|
||||
exit 2
|
||||
fi
|
||||
RUN_CERTIFICATION="non_certified"
|
||||
else
|
||||
KERNEL_PATH="$CASAN_STATE_ROOT/logs/kernel/$EXECUTION_ID.json"
|
||||
python3 - "$KERNEL_PATH" "$KERNEL_BUNDLE" <<'PY'
|
||||
import json, os, sys, tempfile
|
||||
path, raw = sys.argv[1:]
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
payload = json.loads(raw)
|
||||
fd, tmp = tempfile.mkstemp(prefix=".kernel-", dir=os.path.dirname(path))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
|
||||
handle.write("\n")
|
||||
handle.flush(); os.fsync(handle.fileno())
|
||||
os.replace(tmp, path)
|
||||
finally:
|
||||
try: os.unlink(tmp)
|
||||
except OSError: pass
|
||||
PY
|
||||
fi
|
||||
|
||||
write_phase_report
|
||||
casan_log debug harness "action=$ACTION_NAME complete cache=$CACHE_STATUS"
|
||||
echo "CASAN_HARNESS_COMPLETE cache=$CACHE_STATUS key=$IDEMPOTENCY_KEY output=$FINAL_OUTPUT"
|
||||
echo "CASAN_HARNESS_COMPLETE cache=$CACHE_STATUS key=$IDEMPOTENCY_KEY execution=success assurance=$RUN_ASSURANCE certification=$RUN_CERTIFICATION output=$FINAL_OUTPUT"
|
||||
|
||||
@@ -58,27 +58,48 @@ TRACE_ID="$(new_trace_id)"
|
||||
TIMESTAMP="$(timestamp)"
|
||||
INPUT="$(cat "$INPUT_FILE")"
|
||||
LOWER_INPUT="$(printf '%s' "$INPUT" | tr '[:upper:]' '[:lower:]')"
|
||||
ACTOR="${CASAN_ACTOR:-developer}"
|
||||
ACTOR="${CASAN_ACTOR:-${CASAN_AGENT:-}}"
|
||||
APPROVER="${CASAN_APPROVER:-}"
|
||||
APPROVAL_DECISION="${CASAN_APPROVAL_DECISION:-auto}"
|
||||
AUDIT_LOG="$AUDIT_DIR/audit.jsonl"
|
||||
|
||||
RISK_LEVEL="low"
|
||||
REASONS=()
|
||||
|
||||
case "$ACTION_NAME" in
|
||||
deploy|launch|write_code|write_file|migration|db_write|external_api|tool_call)
|
||||
RISK_LEVEL="medium"
|
||||
REASONS+=("sensitive-action:$ACTION_NAME")
|
||||
;;
|
||||
esac
|
||||
|
||||
if printf '%s' "$LOWER_INPUT" | grep -Eq "(delete|drop table|password|api[_-]?key|secret|token|credential|migration|deploy|external api|shutdown|dump database)"; then
|
||||
ACTION_CLASS="unknown"
|
||||
RISK_FACTORS_JSON='{"action_risk":"high","content_risk":"high","environment_risk":"low","identity_risk":"low","resource_risk":"low"}'
|
||||
EVIDENCE_REQUIREMENT="required"
|
||||
RISK_POLICY_DECISION="require_approval"
|
||||
KERNEL_CLI="$CASAN_HARNESS_ROOT/scripts/python/kernel_cli.py"
|
||||
RISK_JSON=""
|
||||
RISK_RC=0
|
||||
if [[ -f "$KERNEL_CLI" ]]; then
|
||||
RISK_JSON="$(python3 "$KERNEL_CLI" risk --action "$ACTION_NAME" --tool "$ACTION_NAME" \
|
||||
--content-file "$INPUT_FILE" --actor "$ACTOR" --environment "${CASAN_PROFILE:-development}" 2>/dev/null)" || RISK_RC=$?
|
||||
else
|
||||
RISK_RC=127
|
||||
fi
|
||||
if [[ "$RISK_RC" -eq 0 && -n "$RISK_JSON" ]]; then
|
||||
RISK_FIELDS="$(python3 - "$RISK_JSON" <<'PY'
|
||||
import json, sys
|
||||
payload = json.loads(sys.argv[1])
|
||||
print("\t".join([
|
||||
str(payload["action_class"]),
|
||||
str(payload["effective_risk"]),
|
||||
json.dumps(payload["risk_factors"], sort_keys=True, separators=(",", ":")),
|
||||
str(payload["evidence_requirement"]),
|
||||
str(payload["decision"]),
|
||||
]))
|
||||
PY
|
||||
)" || RISK_RC=$?
|
||||
fi
|
||||
if [[ "$RISK_RC" -eq 0 && -n "${RISK_FIELDS:-}" ]]; then
|
||||
IFS=$'\t' read -r ACTION_CLASS RISK_LEVEL RISK_FACTORS_JSON EVIDENCE_REQUIREMENT RISK_POLICY_DECISION <<< "$RISK_FIELDS"
|
||||
REASONS+=("action-risk-floor:$ACTION_CLASS")
|
||||
else
|
||||
RISK_LEVEL="high"
|
||||
REASONS+=("high-risk-content")
|
||||
elif printf '%s' "$LOWER_INPUT" | grep -Eq "(internal|config|system|policy|permission)"; then
|
||||
[[ "$RISK_LEVEL" == "low" ]] && RISK_LEVEL="medium"
|
||||
REASONS+=("medium-risk-content")
|
||||
ACTION_CLASS="unknown"
|
||||
RISK_POLICY_DECISION="require_approval"
|
||||
REASONS+=("action-risk-classifier-failed-closed")
|
||||
fi
|
||||
|
||||
APPROVAL_STATUS="auto_approved"
|
||||
@@ -88,8 +109,14 @@ if [[ "$RISK_LEVEL" == "medium" ]]; then
|
||||
APPROVAL_STATUS="policy_auto_approved_with_audit"
|
||||
fi
|
||||
|
||||
if [[ "$RISK_LEVEL" == "high" ]]; then
|
||||
if [[ "${CASAN_APPROVAL_STRICT:-0}" == "1" ]]; then
|
||||
if [[ "$RISK_POLICY_DECISION" == "deny" ]]; then
|
||||
APPROVAL_STATUS="actor_identity_required"
|
||||
DECISION="denied"
|
||||
REASONS+=("actor-identity-required")
|
||||
elif [[ "$RISK_LEVEL" == "high" || "$RISK_LEVEL" == "critical" || "$RISK_POLICY_DECISION" == "require_approval" ]]; then
|
||||
APPROVAL_STRICT_EFFECTIVE="${CASAN_APPROVAL_STRICT:-0}"
|
||||
[[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]] && APPROVAL_STRICT_EFFECTIVE="1"
|
||||
if [[ "$APPROVAL_STRICT_EFFECTIVE" == "1" ]]; then
|
||||
# Approval-identity mode (V20): an env-var approver is NOT enough — the
|
||||
# reviewer must cryptographically SIGN this exact request and their role must
|
||||
# be authorized for the action. SoD (actor != approver) still enforced.
|
||||
@@ -147,7 +174,7 @@ fi
|
||||
REASONS_JSON="$(printf '%s\n' "${REASONS[@]:-}" | python -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
|
||||
# approver and output_hash are part of the hashed core so they cannot be
|
||||
# silently mutated after the fact.
|
||||
RECORD_CORE="$(printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s' "$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTOR" "$RISK_LEVEL" "$DECISION" "$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH")"
|
||||
RECORD_CORE="$(printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s' "$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTION_CLASS" "$ACTOR" "$RISK_LEVEL" "$RISK_FACTORS_JSON" "$EVIDENCE_REQUIREMENT" "$DECISION" "$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH")"
|
||||
RECORD_HASH="$(printf '%s' "$RECORD_CORE" | hash_text)"
|
||||
|
||||
TRACE_FILE="$TRACE_DIR/governance-$TRACE_ID.json"
|
||||
@@ -160,18 +187,24 @@ TRACE_FILE="$TRACE_DIR/governance-$TRACE_ID.json"
|
||||
# written (disk full, read-only, quota), there must be NO governed action without
|
||||
# its accountability record — deny and empty the output rather than proceed.
|
||||
if ! CASAN_GC_REASONS="$REASONS_JSON" python - "$TRACE_FILE" "$AUDIT_LOG" \
|
||||
"$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTOR" "$RISK_LEVEL" "$DECISION" \
|
||||
"$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH" "$RECORD_HASH" <<'PY'
|
||||
"$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTION_CLASS" "$ACTOR" "$RISK_LEVEL" \
|
||||
"$RISK_FACTORS_JSON" "$EVIDENCE_REQUIREMENT" "$DECISION" "$APPROVAL_STATUS" \
|
||||
"$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH" "$RECORD_HASH" <<'PY'
|
||||
import json, os, sys
|
||||
(trace_file, audit_log, ts, trace_id, action, actor, risk, decision,
|
||||
approval_status, approver, input_hash, output_hash, prev_hash, record_hash) = sys.argv[1:]
|
||||
(trace_file, audit_log, ts, trace_id, action, action_class, actor, risk,
|
||||
risk_factors_json, evidence_requirement, decision, approval_status, approver,
|
||||
input_hash, output_hash, prev_hash, record_hash) = sys.argv[1:]
|
||||
try:
|
||||
reasons = json.loads(os.environ.get("CASAN_GC_REASONS") or "[]")
|
||||
except ValueError:
|
||||
reasons = []
|
||||
rec = {
|
||||
"schema_version": 2, "category": "runtime_control",
|
||||
"timestamp": ts, "trace_id": trace_id, "harness": "H5-governance",
|
||||
"action": action, "actor": actor, "risk_level": risk, "decision": decision,
|
||||
"action": action, "action_class": action_class, "actor": actor,
|
||||
"risk_level": risk, "effective_risk": risk,
|
||||
"risk_factors": json.loads(risk_factors_json),
|
||||
"evidence_requirement": evidence_requirement, "decision": decision,
|
||||
"approval_status": approval_status, "approver": approver,
|
||||
"input_hash": input_hash, "output_hash": output_hash,
|
||||
"previous_record_hash": prev_hash, "record_hash": record_hash,
|
||||
@@ -196,8 +229,43 @@ fi
|
||||
# --- External anchor: cryptographically sign the new chain head ---
|
||||
# A re-forged chain (recomputed hashes) changes the head; without the private
|
||||
# key the attacker cannot produce a matching signature, so verification fails.
|
||||
# Production note: the private key must live off-repo (KMS/HSM). It is local
|
||||
# here only for self-contained demonstration.
|
||||
# Development may use a local key for self-contained demonstration. Production
|
||||
# refuses that path unless an explicit emergency override is visible in evidence.
|
||||
PRODUCTION_PROFILE=0
|
||||
[[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]] && PRODUCTION_PROFILE=1
|
||||
EMERGENCY_TRUST_OVERRIDE="${CASAN_TRUST_EMERGENCY_OVERRIDE:-0}"
|
||||
TRUST_LOG="$CASAN_STATE_ROOT/logs/readiness/trust-capabilities.jsonl"
|
||||
mkdir -p "$(dirname "$TRUST_LOG")"
|
||||
|
||||
if [[ "$PRODUCTION_PROFILE" == "1" && "$EMERGENCY_TRUST_OVERRIDE" != "1" ]]; then
|
||||
TRUST_RC=0
|
||||
TRUST_JSON="$(python3 "$KERNEL_CLI" trust-capabilities 2>/dev/null)" || TRUST_RC=$?
|
||||
if [[ "$TRUST_RC" -ne 0 ]]; then
|
||||
if [[ -n "$TRUST_JSON" ]]; then
|
||||
printf '%s\n' "$TRUST_JSON" >> "$TRUST_LOG"
|
||||
else
|
||||
printf '{"ready":false,"severity":"critical","reason_codes":["production_trust_configuration_invalid"]}\n' >> "$TRUST_LOG"
|
||||
fi
|
||||
: > "$OUTPUT_FILE"
|
||||
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=production_trust_root_unavailable" >&2
|
||||
exit 2
|
||||
fi
|
||||
printf '%s\n' "$TRUST_JSON" >> "$TRUST_LOG"
|
||||
if ! bash "$SCRIPT_DIR/sign-audit-head.sh" "$AUDIT_LOG" >/dev/null 2>&1; then
|
||||
: > "$OUTPUT_FILE"
|
||||
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=external_signing_failed" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! bash "$SCRIPT_DIR/audit-ship-s3.sh" "$AUDIT_DIR/audit-head.txt" >/dev/null 2>&1; then
|
||||
: > "$OUTPUT_FILE"
|
||||
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=external_immutable_anchor_failed" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
if [[ "$PRODUCTION_PROFILE" == "1" ]]; then
|
||||
printf '{"schema_version":"1.0.0","profile":"production","ready":false,"certifiable":false,"emergency_override":true,"severity":"critical","reason_codes":["emergency_local_trust_override_active"]}\n' >> "$TRUST_LOG"
|
||||
echo "CRITICAL: emergency local trust override active; execution cannot be production-certified" >&2
|
||||
fi
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
# Private signing key lives OFF-REPO (default ~/.casan/audit-keys); only the
|
||||
# public key is committed. Production: replace with KMS/HSM.
|
||||
@@ -207,7 +275,7 @@ if command -v openssl >/dev/null 2>&1; then
|
||||
AUDIT_PUB="$PUB_DIR/audit-public.pem"
|
||||
mkdir -p "$PUB_DIR" "$PRIV_DIR"
|
||||
if [[ ! -f "$AUDIT_PRIV" ]]; then
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ]]; then
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" || "${CASAN_VERIFY_STRICT:-}" == "1" ]]; then
|
||||
# SEC-02 (H-02): in enforced mode NEVER auto-generate a local signing key.
|
||||
# A freshly-minted key next to the data lets any file-writer re-sign a forged
|
||||
# head. Prod must provision the key out-of-band (KMS/HSM — see sign-audit-head.sh
|
||||
@@ -229,6 +297,7 @@ if command -v openssl >/dev/null 2>&1; then
|
||||
openssl dgst -sha256 -sign "$AUDIT_PRIV" -out "$AUDIT_DIR/audit-head.sig" "$AUDIT_DIR/audit-head.txt" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$DECISION" != "approved" ]]; then
|
||||
: > "$OUTPUT_FILE"
|
||||
|
||||
@@ -21,22 +21,22 @@ while IFS= read -r raw || [[ -n "$raw" ]]; do
|
||||
[[ "$line" =~ ^([A-Z0-9_]+)=(.*)$ ]] || fail "invalid_env_syntax"
|
||||
key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}"
|
||||
case "$key" in
|
||||
CASAN_PUBLIC_FQDN|CASAN_CP_HTTPS_PORT|CASAN_CP_TLS_DIR|CASAN_CP_OAUTH_ENV|CASAN_CP_RUNTIME_ENV|CASAN_CP_VAULT_ENV|CASAN_CP_STATE_DIR|CASAN_CP_OUTPUT_DIR|CASAN_CP_API_IMAGE|CASAN_CP_UI_IMAGE|CASAN_S3_BUCKET|CASAN_S3_PREFIX|CASAN_S3_REGION|CASAN_S3_RETENTION_DAYS|CASAN_S3_KMS_KEY_ID) export "$key=$value" ;;
|
||||
CASAN_PUBLIC_FQDN|CASAN_CP_HTTPS_PORT|CASAN_CP_TLS_DIR|CASAN_CP_OAUTH_ENV|CASAN_CP_RUNTIME_ENV|CASAN_CP_VAULT_ENV|CASAN_CP_STATE_DIR|CASAN_CP_OUTPUT_DIR|CASAN_CP_IDP_PUBLIC_KEY|CASAN_CP_API_IMAGE|CASAN_CP_UI_IMAGE|CASAN_CP_OAUTH2_PROXY_IMAGE|CASAN_S3_BUCKET|CASAN_S3_PREFIX|CASAN_S3_REGION|CASAN_S3_RETENTION_DAYS|CASAN_S3_KMS_KEY_ID) export "$key=$value" ;;
|
||||
*) fail "unexpected_env_key key=$key" ;;
|
||||
esac
|
||||
done < "$ENV_FILE"
|
||||
|
||||
required=(CASAN_PUBLIC_FQDN CASAN_CP_TLS_DIR CASAN_CP_OAUTH_ENV CASAN_CP_RUNTIME_ENV CASAN_CP_VAULT_ENV CASAN_CP_STATE_DIR CASAN_CP_OUTPUT_DIR CASAN_CP_API_IMAGE CASAN_CP_UI_IMAGE CASAN_S3_BUCKET CASAN_S3_REGION CASAN_S3_KMS_KEY_ID)
|
||||
required=(CASAN_PUBLIC_FQDN CASAN_CP_TLS_DIR CASAN_CP_OAUTH_ENV CASAN_CP_RUNTIME_ENV CASAN_CP_VAULT_ENV CASAN_CP_STATE_DIR CASAN_CP_OUTPUT_DIR CASAN_CP_IDP_PUBLIC_KEY CASAN_CP_API_IMAGE CASAN_CP_UI_IMAGE CASAN_CP_OAUTH2_PROXY_IMAGE CASAN_S3_BUCKET CASAN_S3_REGION CASAN_S3_KMS_KEY_ID)
|
||||
for key in "${required[@]}"; do [[ -n "${!key:-}" ]] || fail "missing_env key=$key"; done
|
||||
|
||||
case "$CASAN_PUBLIC_FQDN" in *localhost*|*127.0.0.1*|*example.com*|*replace-with*|*/*|[0-9]* ) fail "invalid_fqdn";; esac
|
||||
[[ "$CASAN_PUBLIC_FQDN" == *.* ]] || fail "fqdn_required"
|
||||
for image in "$CASAN_CP_API_IMAGE" "$CASAN_CP_UI_IMAGE"; do
|
||||
for image in "$CASAN_CP_API_IMAGE" "$CASAN_CP_UI_IMAGE" "$CASAN_CP_OAUTH2_PROXY_IMAGE"; do
|
||||
[[ "$image" =~ @sha256:[a-f0-9]{64}$ ]] || fail "image_must_be_digest_pinned image=$image"
|
||||
done
|
||||
pass "public FQDN and images are production-safe"
|
||||
|
||||
for file in "$CASAN_CP_TLS_DIR/tls.crt" "$CASAN_CP_TLS_DIR/tls.key" "$CASAN_CP_OAUTH_ENV" "$CASAN_CP_RUNTIME_ENV" "$CASAN_CP_VAULT_ENV"; do
|
||||
for file in "$CASAN_CP_TLS_DIR/tls.crt" "$CASAN_CP_TLS_DIR/tls.key" "$CASAN_CP_IDP_PUBLIC_KEY" "$CASAN_CP_OAUTH_ENV" "$CASAN_CP_RUNTIME_ENV" "$CASAN_CP_VAULT_ENV"; do
|
||||
[[ -s "$file" ]] || fail "missing_or_empty path=$file"
|
||||
done
|
||||
openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -noout >/dev/null || fail "invalid_tls_certificate"
|
||||
@@ -45,6 +45,7 @@ openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -noout -checkhost "$CASAN_PUBLIC_FQ
|
||||
cert_pub="$(openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 | awk '{print $NF}')"
|
||||
key_pub="$(openssl pkey -in "$CASAN_CP_TLS_DIR/tls.key" -pubout -outform DER | openssl dgst -sha256 | awk '{print $NF}')"
|
||||
[[ "$cert_pub" == "$key_pub" ]] || fail "tls_key_does_not_match_certificate"
|
||||
openssl rsa -pubin -in "$CASAN_CP_IDP_PUBLIC_KEY" -noout -modulus >/dev/null 2>&1 || fail "invalid_idp_rsa_public_key"
|
||||
pass "TLS certificate is valid for at least 30 days"
|
||||
|
||||
value_of() { sed -n -E "s/^${1}=//p" "$CASAN_CP_OAUTH_ENV" | tail -1; }
|
||||
@@ -56,8 +57,24 @@ done
|
||||
[[ "$(value_of OAUTH2_PROXY_OIDC_ISSUER_URL)" == https://* ]] || fail "oidc_issuer_https_required"
|
||||
[[ "$(value_of OAUTH2_PROXY_REDIRECT_URL)" == "https://$CASAN_PUBLIC_FQDN/oauth2/callback" ]] || fail "oidc_redirect_mismatch"
|
||||
[[ "$(value_of OAUTH2_PROXY_COOKIE_SECURE)" == true ]] || fail "oidc_secure_cookie_required"
|
||||
[[ "$(value_of OAUTH2_PROXY_SET_XAUTHREQUEST)" == true ]] || fail "oidc_xauthrequest_required"
|
||||
[[ "$(value_of OAUTH2_PROXY_PASS_ACCESS_TOKEN)" == true ]] || fail "oidc_access_token_forwarding_required"
|
||||
[[ "$(value_of OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER)" == true ]] || fail "oidc_authorization_header_forwarding_required"
|
||||
pass "enterprise OIDC configuration"
|
||||
|
||||
runtime_value_of() { sed -n -E "s/^${1}=//p" "$CASAN_CP_RUNTIME_ENV" | tail -1; }
|
||||
[[ "$(runtime_value_of CASAN_PROFILE)" == prod ]] || fail "runtime_profile_must_be_prod"
|
||||
[[ "$(runtime_value_of CASAN_CP_AUTH_MODE)" == jwt ]] || fail "runtime_jwt_auth_required"
|
||||
[[ "$(runtime_value_of CASAN_CP_JWT_ISSUER)" == "$(value_of OAUTH2_PROXY_OIDC_ISSUER_URL)" ]] || fail "runtime_oidc_issuer_mismatch"
|
||||
[[ "$(runtime_value_of CASAN_CP_JWT_AUDIENCE)" == "$(value_of OAUTH2_PROXY_CLIENT_ID)" ]] || fail "runtime_oidc_audience_mismatch"
|
||||
[[ "$(runtime_value_of CASAN_CP_JWT_PUBLIC_KEY_FILE)" == /run/casan-idp/idp-public.pem ]] || fail "runtime_idp_public_key_path_invalid"
|
||||
[[ "$(runtime_value_of CASAN_SIGNING_PROVIDER)" == vault_kms ]] || fail "runtime_external_signing_required"
|
||||
[[ "$(runtime_value_of CASAN_IMMUTABLE_ANCHOR_PROVIDER)" == s3_object_lock ]] || fail "runtime_immutable_anchor_required"
|
||||
[[ -z "$(runtime_value_of CASAN_CP_TRUST_AUTH_PROXY)" ]] || fail "legacy_trusted_header_auth_forbidden"
|
||||
clock_skew="$(runtime_value_of CASAN_CP_JWT_CLOCK_SKEW_SECONDS)"
|
||||
[[ "$clock_skew" =~ ^[0-9]+$ && "$clock_skew" -le 300 ]] || fail "runtime_jwt_clock_skew_invalid"
|
||||
pass "Control Plane verifies OIDC token identity cryptographically"
|
||||
|
||||
vault_addr="$(sed -n -E 's/^VAULT_ADDR=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
|
||||
vault_token="$(sed -n -E 's/^VAULT_TOKEN=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
|
||||
vault_cacert="$(sed -n -E 's/^VAULT_CACERT=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
|
||||
@@ -88,7 +105,8 @@ COMPOSE="$ROOT/docker-compose.control-panel.yml"
|
||||
CASAN_CP_TLS_DIR="$CASAN_CP_TLS_DIR" CASAN_CP_OAUTH_ENV="$CASAN_CP_OAUTH_ENV" \
|
||||
CASAN_CP_RUNTIME_ENV="$CASAN_CP_RUNTIME_ENV" CASAN_CP_VAULT_ENV="$CASAN_CP_VAULT_ENV" \
|
||||
CASAN_CP_STATE_DIR="$CASAN_CP_STATE_DIR" CASAN_CP_OUTPUT_DIR="$CASAN_CP_OUTPUT_DIR" \
|
||||
CASAN_CP_API_IMAGE="$CASAN_CP_API_IMAGE" CASAN_CP_UI_IMAGE="$CASAN_CP_UI_IMAGE" \
|
||||
CASAN_CP_IDP_PUBLIC_KEY="$CASAN_CP_IDP_PUBLIC_KEY" CASAN_CP_API_IMAGE="$CASAN_CP_API_IMAGE" \
|
||||
CASAN_CP_UI_IMAGE="$CASAN_CP_UI_IMAGE" CASAN_CP_OAUTH2_PROXY_IMAGE="$CASAN_CP_OAUTH2_PROXY_IMAGE" \
|
||||
docker compose -f "$COMPOSE" config >/dev/null || fail "compose_config_invalid"
|
||||
pass "production compose config"
|
||||
|
||||
|
||||
@@ -47,10 +47,15 @@ docker info >/dev/null 2>&1 || { echo "SANDBOX_CONTAINER_DOCKER_DOWN" >&2; exit
|
||||
# rootful Docker daemon because a compromised daemon socket defeats container
|
||||
# isolation. Local developer/test profiles may use a rootful daemon, but cannot
|
||||
# claim that configuration as a hardened production runner.
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" \
|
||||
|| "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then
|
||||
docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'rootless' \
|
||||
|| { echo "SANDBOX_CONTAINER_ROOTLESS_REQUIRED" >&2; exit 2; }
|
||||
fi
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
|
||||
[[ "$IMAGE" =~ @sha256:[a-f0-9]{64}$ ]] \
|
||||
|| { echo "SANDBOX_CONTAINER_IMAGE_DIGEST_REQUIRED image=$IMAGE" >&2; exit 2; }
|
||||
fi
|
||||
|
||||
WS_ABS="$(cd "$WORKSPACE" 2>/dev/null && pwd)" || { echo "SANDBOX_CONTAINER_BAD_WORKSPACE" >&2; exit 2; }
|
||||
|
||||
|
||||
@@ -36,6 +36,37 @@ CPU_SECONDS="${CASAN_SANDBOX_CPU_SECONDS:-30}"
|
||||
# check is the real gate; a container --pids-limit is the production backstop.
|
||||
MAX_PROCS="${CASAN_SANDBOX_MAX_PROCS:-}"
|
||||
TIMEOUT="${CASAN_SANDBOX_TIMEOUT:-30}"
|
||||
SANDBOX_MODE="${CASAN_SANDBOX_MODE:-}"
|
||||
STRICT_SANDBOX=0
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" \
|
||||
|| "${CASAN_ENFORCEMENT_MODE:-}" == "enforce" || "${CASAN_SANDBOX_STRICT:-0}" == "1" ]]; then
|
||||
STRICT_SANDBOX=1
|
||||
fi
|
||||
[[ -n "$SANDBOX_MODE" ]] || { if [[ "$STRICT_SANDBOX" == "1" ]]; then SANDBOX_MODE="container"; else SANDBOX_MODE="static"; fi; }
|
||||
|
||||
record_sandbox() { # decision reason backend capability-json
|
||||
local decision="$1" reason="$2" backend="$3" capabilities="$4"
|
||||
local log="$CASAN_STATE_ROOT/logs/sandbox/decisions.jsonl"
|
||||
mkdir -p "$(dirname "$log")"
|
||||
CASAN_SANDBOX_CAPABILITIES="$capabilities" python3 - "$log" "$decision" "$reason" "$backend" "${CASAN_EXECUTION_ID:-sandbox-$$}" <<'PY'
|
||||
import json, os, sys
|
||||
path, decision, reason, backend, execution_id = sys.argv[1:]
|
||||
try:
|
||||
capabilities = json.loads(os.environ.get("CASAN_SANDBOX_CAPABILITIES", "{}"))
|
||||
except ValueError:
|
||||
capabilities = {}
|
||||
record = {
|
||||
"schema_version": "1.0.0", "category": "runtime_control",
|
||||
"policy_id": "casan.sandbox.backend", "decision": decision,
|
||||
"reason_code": reason, "backend": backend, "execution_id": execution_id,
|
||||
"capabilities": capabilities,
|
||||
}
|
||||
with open(path, "a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
PY
|
||||
}
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -54,12 +85,35 @@ if [[ "$#" -eq 0 ]]; then
|
||||
exit 64
|
||||
fi
|
||||
|
||||
# C6 production form: CASAN_SANDBOX_MODE=container runs under TRUE kernel
|
||||
# C6 production form: CASAN_SANDBOX_MODE=container runs under kernel-backed
|
||||
# isolation (sandbox-container.sh: --network=none --read-only --pids-limit …).
|
||||
# Default stays the static-policy + ulimit scaffold so existing behaviour is
|
||||
# unchanged. Falls back to the scaffold if Docker is unavailable.
|
||||
if [[ "${CASAN_SANDBOX_MODE:-static}" == "container" ]] && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
||||
exec "$SCRIPT_DIR/sandbox-container.sh" --workspace "$WORKSPACE" --timeout "$TIMEOUT" -- "$@"
|
||||
# A requested/required container backend never silently falls back.
|
||||
if [[ "$SANDBOX_MODE" == "container" ]]; then
|
||||
if [[ "${CASAN_SANDBOX_TEST_FORCE_UNAVAILABLE:-0}" != "1" ]] \
|
||||
&& command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
||||
record_sandbox "allow" "sandbox_container_selected" "docker" '{"network_disabled":true,"read_only_root":true,"workspace_write_restricted":true,"environment_filtered":true,"non_root":true,"resource_limits":true}'
|
||||
exec "$SCRIPT_DIR/sandbox-container.sh" --workspace "$WORKSPACE" --timeout "$TIMEOUT" -- "$@"
|
||||
fi
|
||||
if [[ "$STRICT_SANDBOX" == "1" ]]; then
|
||||
record_sandbox "deny" "sandbox_isolation_backend_unavailable" "none" '{"timeout_only":false}'
|
||||
echo "SANDBOX_ISOLATION_REQUIRED backend=container reason=unavailable" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "${CASAN_SANDBOX_ALLOW_STATIC_FALLBACK:-0}" != "1" ]]; then
|
||||
record_sandbox "deny" "sandbox_fallback_not_approved" "none" '{}'
|
||||
echo "SANDBOX_FALLBACK_REQUIRES_EXPLICIT_DEVELOPMENT_APPROVAL" >&2
|
||||
exit 2
|
||||
fi
|
||||
record_sandbox "observe_only" "sandbox_static_fallback_development_only" "static_rlimit" '{"network_disabled":false,"read_only_root":false,"workspace_write_restricted":false,"environment_filtered":false,"non_root":false,"resource_limits":true}'
|
||||
echo "HIGH: container sandbox unavailable; explicit development static fallback is not production isolation" >&2
|
||||
elif [[ "$SANDBOX_MODE" != "static" ]]; then
|
||||
record_sandbox "deny" "sandbox_backend_unknown" "$SANDBOX_MODE" '{}'
|
||||
echo "SANDBOX_BACKEND_UNKNOWN mode=$SANDBOX_MODE" >&2
|
||||
exit 2
|
||||
elif [[ "$STRICT_SANDBOX" == "1" ]]; then
|
||||
record_sandbox "deny" "sandbox_static_forbidden_in_enforce_mode" "static_rlimit" '{"network_disabled":false,"read_only_root":false}'
|
||||
echo "SANDBOX_STATIC_FORBIDDEN_IN_ENFORCE_MODE" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
CMD_STR="$*"
|
||||
@@ -96,6 +150,7 @@ done < <(printf '%s\n' "$CMD_STR" | grep -oE '>>?[[:space:]]*[^[:space:];|&]+' |
|
||||
|
||||
# ── 2. Runtime rlimits + wall-clock timeout ─────────────────────────────────
|
||||
casan_log debug sandbox "SANDBOX_RUN workspace=$WS_ABS file_kb=$MAX_FILE_KB cpu=$CPU_SECONDS procs=$MAX_PROCS timeout=$TIMEOUT"
|
||||
record_sandbox "allow" "sandbox_static_policy_selected" "static_rlimit" '{"network_disabled":false,"read_only_root":false,"workspace_write_restricted":false,"environment_filtered":false,"non_root":false,"resource_limits":true}'
|
||||
(
|
||||
ulimit -f "$((MAX_FILE_KB * 2))" 2>/dev/null || true # ulimit -f is in 512-byte blocks
|
||||
ulimit -t "$CPU_SECONDS" 2>/dev/null || true
|
||||
|
||||
@@ -45,14 +45,24 @@ with open(path, encoding="utf-8") as f:
|
||||
if not line.strip():
|
||||
continue
|
||||
record = json.loads(line)
|
||||
core = "|".join([
|
||||
record.get("timestamp",""), record.get("trace_id",""),
|
||||
record.get("action",""), record.get("actor",""),
|
||||
record.get("risk_level",""), record.get("decision",""),
|
||||
record.get("approval_status",""), record.get("approver",""),
|
||||
record.get("input_hash",""), record.get("output_hash",""),
|
||||
previous,
|
||||
])
|
||||
if int(record.get("schema_version", 1)) >= 2:
|
||||
core = "|".join([
|
||||
record.get("timestamp",""), record.get("trace_id",""),
|
||||
record.get("action",""), record.get("action_class",""),
|
||||
record.get("actor",""), record.get("risk_level",""),
|
||||
json.dumps(record.get("risk_factors", {}), sort_keys=True, separators=(",", ":")),
|
||||
record.get("evidence_requirement",""), record.get("decision",""),
|
||||
record.get("approval_status",""), record.get("approver",""),
|
||||
record.get("input_hash",""), record.get("output_hash",""), previous,
|
||||
])
|
||||
else:
|
||||
core = "|".join([
|
||||
record.get("timestamp",""), record.get("trace_id",""),
|
||||
record.get("action",""), record.get("actor",""),
|
||||
record.get("risk_level",""), record.get("decision",""),
|
||||
record.get("approval_status",""), record.get("approver",""),
|
||||
record.get("input_hash",""), record.get("output_hash",""), previous,
|
||||
])
|
||||
previous = hashlib.sha256(core.encode()).hexdigest()
|
||||
print(previous)
|
||||
PY
|
||||
@@ -68,6 +78,10 @@ printf '%s' "$HEAD_HASH" > "$HEAD_FILE"
|
||||
# ── Sign the head file ────────────────────────────────────────────────────
|
||||
VAULT_KMS="$SCRIPT_DIR/vault-kms.sh"
|
||||
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
|
||||
[[ "${VAULT_ADDR:-}" == https://* ]] || { echo "SIGN_AUDIT_HEAD_FAIL reason=vault_https_required_in_prod" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
if [[ -n "${VAULT_ADDR:-}" && -n "${VAULT_TOKEN:-}" ]] && \
|
||||
curl -sf "$VAULT_ADDR/v1/sys/health" >/dev/null 2>&1; then
|
||||
# KMS path — sign via Vault Transit, export public key
|
||||
@@ -103,7 +117,7 @@ PY
|
||||
fi
|
||||
else
|
||||
# Fallback — local key (dev environment without Vault)
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" ]]; then
|
||||
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
|
||||
echo "SIGN_AUDIT_HEAD_FAIL reason=vault_kms_required_in_prod" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -35,21 +35,28 @@ with open(path, encoding="utf-8") as f:
|
||||
f"AUDIT_CHAIN_BROKEN line={line_no} expected_previous={previous} actual_previous={expected_previous}"
|
||||
)
|
||||
|
||||
core = "|".join(
|
||||
[
|
||||
record.get("timestamp", ""),
|
||||
record.get("trace_id", ""),
|
||||
record.get("action", ""),
|
||||
record.get("actor", ""),
|
||||
record.get("risk_level", ""),
|
||||
record.get("decision", ""),
|
||||
record.get("approval_status", ""),
|
||||
record.get("approver", ""),
|
||||
record.get("input_hash", ""),
|
||||
record.get("output_hash", ""),
|
||||
if int(record.get("schema_version", 1)) >= 2:
|
||||
core = "|".join([
|
||||
record.get("timestamp", ""), record.get("trace_id", ""),
|
||||
record.get("action", ""), record.get("action_class", ""),
|
||||
record.get("actor", ""), record.get("risk_level", ""),
|
||||
json.dumps(record.get("risk_factors", {}), sort_keys=True, separators=(",", ":")),
|
||||
record.get("evidence_requirement", ""), record.get("decision", ""),
|
||||
record.get("approval_status", ""), record.get("approver", ""),
|
||||
record.get("input_hash", ""), record.get("output_hash", ""),
|
||||
expected_previous,
|
||||
]
|
||||
)
|
||||
])
|
||||
else:
|
||||
core = "|".join(
|
||||
[
|
||||
record.get("timestamp", ""), record.get("trace_id", ""),
|
||||
record.get("action", ""), record.get("actor", ""),
|
||||
record.get("risk_level", ""), record.get("decision", ""),
|
||||
record.get("approval_status", ""), record.get("approver", ""),
|
||||
record.get("input_hash", ""), record.get("output_hash", ""),
|
||||
expected_previous,
|
||||
]
|
||||
)
|
||||
expected_hash = hashlib.sha256(core.encode()).hexdigest()
|
||||
actual_hash = record.get("record_hash", "")
|
||||
if expected_hash != actual_hash:
|
||||
|
||||
@@ -40,8 +40,23 @@ import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
SCHEMA_VERSION = "20.1"
|
||||
ADAPTER_DEFAULT_VERSION = "20.1.0"
|
||||
HARNESS_PACKAGE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if HARNESS_PACKAGE_ROOT not in sys.path:
|
||||
sys.path.insert(0, HARNESS_PACKAGE_ROOT)
|
||||
|
||||
from kernel.adapters import AgenticBridgeAdapter
|
||||
from kernel.contracts import validate_bundle
|
||||
from kernel.policy import (
|
||||
H2GateContext,
|
||||
evaluate_failure_policy,
|
||||
evaluate_h2_gate,
|
||||
evaluate_registry_configuration,
|
||||
evaluate_risk,
|
||||
evaluate_trust_capabilities,
|
||||
)
|
||||
|
||||
SCHEMA_VERSION = "20.2"
|
||||
ADAPTER_DEFAULT_VERSION = "20.2.0"
|
||||
|
||||
# ── Certification strength ranking (higher == stronger) ──────────────────────
|
||||
STRENGTH_RANK = {
|
||||
@@ -77,6 +92,20 @@ TOOL_REGISTRY_ACTION = {
|
||||
"str_replace_editor": "write_file",
|
||||
}
|
||||
|
||||
REGISTRY_ACTION_BY_CLASS = {
|
||||
"write": "write_file",
|
||||
"delete": "delete_file",
|
||||
"database_mutation": "db_write",
|
||||
"migration": "migration",
|
||||
"deployment": "deploy",
|
||||
"release": "deploy",
|
||||
"credential_access": "credential_access",
|
||||
"identity_permission_modification": "identity_change",
|
||||
"external_network_side_effect": "external_api",
|
||||
"infrastructure_modification": "deploy",
|
||||
"unknown": "unknown_tool",
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Path resolution (mirrors scripts/bash/casan-paths.sh so state lands in the
|
||||
@@ -145,6 +174,12 @@ def trace_event_dir():
|
||||
return d
|
||||
|
||||
|
||||
def kernel_trace_dir():
|
||||
d = os.path.join(state_root(), "logs", "kernel")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def metrics_log():
|
||||
override = os.environ.get("CASAN_TELEMETRY_METRICS_LOG") or os.environ.get(
|
||||
"CASAN_METRICS_LOG"
|
||||
@@ -612,28 +647,52 @@ def h4_scan(text, mode="input"):
|
||||
pass
|
||||
|
||||
|
||||
def h2_registry_gate(action, idempotency_key):
|
||||
"""H2 tool-registry gate for a mapped side-effect action."""
|
||||
script = os.path.join(gates_dir(), "tool-registry-gate.sh")
|
||||
if not os.path.exists(script) or not bash_available():
|
||||
return True, "h2_gate_missing"
|
||||
env = dict(os.environ)
|
||||
env["CASAN_IDEMPOTENCY_KEY"] = idempotency_key
|
||||
def h2_registry_gate(action, tool, actor, execution_id, idempotency_key):
|
||||
"""Return a structured H2 policy decision for every dependency outcome."""
|
||||
script = os.environ.get("CASAN_H2_GATE_PATH") or os.path.join(
|
||||
gates_dir(), "tool-registry-gate.sh")
|
||||
context = H2GateContext(
|
||||
mode=enforcement_mode(),
|
||||
actor=actor or "",
|
||||
action=action,
|
||||
tool=tool or action,
|
||||
execution_id=execution_id,
|
||||
enforcement_path="agentic_bridge.pre_tool.h2_registry",
|
||||
idempotency_key=idempotency_key,
|
||||
timeout_seconds=internal_timeout(),
|
||||
)
|
||||
return evaluate_h2_gate(
|
||||
script,
|
||||
context,
|
||||
env=os.environ,
|
||||
bash_path=bash_bin(),
|
||||
)
|
||||
|
||||
|
||||
def h5_governance_gate(action, content):
|
||||
"""Invoke the existing H5 gate for action-floor approval decisions."""
|
||||
script = os.path.join(gates_dir(), "governance-check.sh")
|
||||
if not os.path.isfile(script) or not bash_available():
|
||||
return {"approved": False, "reason_code": "h5_gate_unavailable", "detail": "governance gate unavailable"}
|
||||
tmpin = os.path.join(sessions_dir(), ".h5-in-%s" % uuid.uuid4().hex[:8])
|
||||
tmpout = os.path.join(sessions_dir(), ".h5-out-%s" % uuid.uuid4().hex[:8])
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bash_bin(), script, action],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=internal_timeout(),
|
||||
env=env,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
return True, "h2_ok"
|
||||
return False, "h2_denied"
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "h2_internal_timeout"
|
||||
except (OSError, ValueError) as exc:
|
||||
return False, "h2_error:%s" % exc
|
||||
with open(tmpin, "w", encoding="utf-8") as handle:
|
||||
handle.write(content or "")
|
||||
rc, stdout, stderr = _run_gate([bash_bin(), script, tmpin, tmpout, action])
|
||||
if rc == 0 and "GOVERNANCE_APPROVED" in stdout:
|
||||
return {"approved": True, "reason_code": "h5_approval_verified", "detail": stdout.strip()}
|
||||
if rc == 124:
|
||||
return {"approved": False, "reason_code": "h5_gate_timeout", "detail": "governance gate timed out"}
|
||||
if rc == 125:
|
||||
return {"approved": False, "reason_code": "h5_gate_internal_error", "detail": stderr}
|
||||
return {"approved": False, "reason_code": "h5_approval_required", "detail": stderr or stdout}
|
||||
finally:
|
||||
for path in (tmpin, tmpout):
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -692,6 +751,8 @@ def write_trace_events(rec, evidence):
|
||||
"kind": evidence.get("kind"),
|
||||
"decision": evidence.get("decision"),
|
||||
"detail": evidence.get("detail"),
|
||||
"category": evidence.get("category"),
|
||||
"facts": evidence.get("facts", {}),
|
||||
"certification_strength": rec.get(
|
||||
"certification_strength"),
|
||||
},
|
||||
@@ -700,13 +761,16 @@ def write_trace_events(rec, evidence):
|
||||
event, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
|
||||
|
||||
def add_evidence(rec, h, kind, decision, detail):
|
||||
def add_evidence(rec, h, kind, decision, detail, facts=None, category="runtime_control"):
|
||||
evidence = {
|
||||
"evidence_id": "%s:%s" % (rec.get("trace_id", "trace"), len(rec.get("evidence", [])) + 1),
|
||||
"h": h,
|
||||
"category": category,
|
||||
"kind": kind,
|
||||
"decision": decision,
|
||||
"at": now_iso(),
|
||||
"detail": redact(detail, 160),
|
||||
"facts": facts if isinstance(facts, dict) else {},
|
||||
}
|
||||
rec.setdefault("evidence", []).append(evidence)
|
||||
write_trace_events(rec, evidence)
|
||||
@@ -828,6 +892,27 @@ def write_h6_record(rec, status, quality, warnings, missing):
|
||||
return record
|
||||
|
||||
|
||||
def write_kernel_bundle(rec):
|
||||
"""Dual-emit the canonical contract without changing legacy trace readers."""
|
||||
event = dict(rec)
|
||||
event.update({
|
||||
"mode": enforcement_mode(),
|
||||
"execution_status": rec.get("execution_status", "unknown"),
|
||||
"assurance_status": rec.get("assurance_status", "unknown"),
|
||||
"environment": os.environ.get("CASAN_PROFILE", "development"),
|
||||
"completed_at": rec.get("finalized_at") or now_iso(),
|
||||
"policy_decisions": rec.get("policy_decisions", []),
|
||||
})
|
||||
bundle = AgenticBridgeAdapter().map_execution(event)
|
||||
errors = validate_bundle(bundle)
|
||||
if errors:
|
||||
raise ValueError("kernel_contract_invalid:%s" % ",".join(errors))
|
||||
path = os.path.join(kernel_trace_dir(), "%s.json" % rec.get("trace_id"))
|
||||
atomic_write_json(path, bundle)
|
||||
rec["kernel_contract_ref"] = path
|
||||
return path
|
||||
|
||||
|
||||
def write_trace_file(rec, certified, reasons):
|
||||
path = os.path.join(trace_dir(), "agentic-%s.json" % rec.get("trace_id"))
|
||||
payload = {
|
||||
@@ -851,6 +936,9 @@ def write_trace_file(rec, certified, reasons):
|
||||
"failures": rec.get("failures", 0),
|
||||
"certified": certified,
|
||||
"certification_reasons": reasons,
|
||||
"execution_outcome": rec.get("execution_status"),
|
||||
"assurance_outcome": rec.get("assurance_status"),
|
||||
"kernel_contract_ref": rec.get("kernel_contract_ref"),
|
||||
"evidence": rec.get("evidence", []),
|
||||
}
|
||||
atomic_write_json(path, payload)
|
||||
@@ -886,6 +974,8 @@ def op_begin(req):
|
||||
"client": client,
|
||||
"client_version": req.get("client_version"),
|
||||
"adapter_version": req.get("adapter_version") or ADAPTER_DEFAULT_VERSION,
|
||||
"actor": req.get("actor") or os.environ.get("CASAN_ACTOR") or os.environ.get("CASAN_AGENT") or "",
|
||||
"mode": enforcement_mode(),
|
||||
"project_root": project,
|
||||
"project_id": project_id(project),
|
||||
"session_id_hash": salted_hash(req.get("session")),
|
||||
@@ -900,8 +990,10 @@ def op_begin(req):
|
||||
"state": "Admitted",
|
||||
"tool_calls": 0,
|
||||
"failures": 0,
|
||||
"side_effect_failures": 0,
|
||||
"retries": 0,
|
||||
"evidence": [],
|
||||
"policy_decisions": [],
|
||||
"telemetry": {},
|
||||
"bypass_signal": False,
|
||||
"finalized": False,
|
||||
@@ -1012,30 +1104,109 @@ def op_pre_tool(req):
|
||||
# record the degradation but do not block the developer.
|
||||
add_evidence(rec, "H4", "pre-tool", "degraded", "%s:gate_unavailable_no_bash" % tool)
|
||||
|
||||
# H2 registry gate for mapped side-effect actions. Opt-in via
|
||||
# CASAN_AGENTIC_H2_REGISTRY=1: the tool-registry is keyed on NAMED CASAN
|
||||
# agent identities, which the transparent developer flow does not carry, so
|
||||
# enabling it unconditionally would deny every write. The always-on H2
|
||||
# equivalent for this flow is the admission gate above (a side effect
|
||||
# without a valid admission is denied). Managed deployments that define
|
||||
# agent identities can turn the registry gate on for defence in depth.
|
||||
registry_on = os.environ.get("CASAN_AGENTIC_H2_REGISTRY", "0") in ("1", "true", "yes")
|
||||
if side_effect and registry_on and enforcement_mode() == "enforce":
|
||||
action = TOOL_REGISTRY_ACTION.get((tool or "").strip().lower())
|
||||
if action:
|
||||
key = plain_hash("%s|%s|%s" % (rec["trace_id"], tool, redact(tool_input_text)))[:24]
|
||||
ok, reason = h2_registry_gate(action, key)
|
||||
if not ok:
|
||||
add_evidence(rec, "H2", "pre-tool", "deny", "%s:%s" % (tool, reason))
|
||||
save_admission(rec)
|
||||
return _base_response("pre-tool", "deny", admission_id=admission_id,
|
||||
trace_id=rec.get("trace_id"), reason=reason)
|
||||
tool_input_string = tool_input_text if isinstance(tool_input_text, str) else json.dumps(
|
||||
tool_input_text, ensure_ascii=False, sort_keys=True) if tool_input_text is not None else ""
|
||||
# Native hook payloads commonly wrap a shell command in {"command": ...}.
|
||||
# Classify the actual command while retaining the complete serialized input
|
||||
# for scanning, governance evidence and hashing.
|
||||
risk_command = tool_input_string
|
||||
if isinstance(tool_input_text, dict) and isinstance(tool_input_text.get("command"), str):
|
||||
risk_command = tool_input_text["command"]
|
||||
risk = evaluate_risk(
|
||||
action=str(req.get("action") or ""),
|
||||
tool=tool,
|
||||
resource=str(req.get("resource") or ""),
|
||||
command=risk_command,
|
||||
content=tool_input_string,
|
||||
actor=rec.get("actor", ""),
|
||||
environment=os.environ.get("CASAN_PROFILE", "development"),
|
||||
)
|
||||
# The canonical classifier can prove a shell command read-only; unknown
|
||||
# shell commands remain side-effecting/high-risk by construction.
|
||||
side_effect = bool(risk["side_effecting"])
|
||||
action = REGISTRY_ACTION_BY_CLASS.get(risk["action_class"], TOOL_REGISTRY_ACTION.get((tool or "").strip().lower(), "unknown_tool"))
|
||||
rec["last_tool"] = tool
|
||||
rec["last_action"] = action
|
||||
rec["last_risk"] = risk
|
||||
rec.setdefault("policy_decisions", []).append(risk)
|
||||
add_evidence(
|
||||
rec, "H5", "action-risk", risk["decision"],
|
||||
"class=%s effective=%s" % (risk["action_class"], risk["effective_risk"]),
|
||||
facts=risk,
|
||||
)
|
||||
|
||||
if side_effect and risk["decision"] == "deny":
|
||||
reason = risk["reason_codes"][-1]
|
||||
add_evidence(rec, "H5", "pre-tool", "deny", reason, facts=risk)
|
||||
save_admission(rec)
|
||||
return _base_response("pre-tool", "deny", admission_id=admission_id,
|
||||
trace_id=rec.get("trace_id"), reason=reason,
|
||||
policy_decision=risk)
|
||||
|
||||
if side_effect and risk["decision"] == "require_approval":
|
||||
approval = h5_governance_gate(action, tool_input_string)
|
||||
add_evidence(
|
||||
rec, "H5", "approval", "allow" if approval["approved"] else "require_approval",
|
||||
approval["reason_code"], facts={"risk": risk, "approval": approval},
|
||||
)
|
||||
if not approval["approved"]:
|
||||
save_admission(rec)
|
||||
return _base_response("pre-tool", "require_approval", admission_id=admission_id,
|
||||
trace_id=rec.get("trace_id"), reason=approval["reason_code"],
|
||||
policy_decision=risk)
|
||||
|
||||
registry = evaluate_registry_configuration(
|
||||
enforcement_mode(),
|
||||
os.environ.get("CASAN_PROFILE", "development"),
|
||||
os.environ.get("CASAN_AGENTIC_H2_REGISTRY") if "CASAN_AGENTIC_H2_REGISTRY" in os.environ else None,
|
||||
)
|
||||
if side_effect and not registry["valid"]:
|
||||
add_evidence(rec, "H2", "registry-configuration", "deny", registry["reason_code"], facts=registry)
|
||||
save_admission(rec)
|
||||
return _base_response("pre-tool", "deny", admission_id=admission_id,
|
||||
trace_id=rec.get("trace_id"), reason=registry["reason_code"])
|
||||
if side_effect and registry["unsafe_bypass"]:
|
||||
rec["integration_mode"] = "observed_only"
|
||||
rec["certification_strength"] = "observed_only"
|
||||
add_evidence(rec, "H2", "registry-configuration", "degraded", registry["reason_code"], facts=registry)
|
||||
|
||||
if side_effect and registry["enabled"]:
|
||||
key = plain_hash("%s|%s|%s" % (rec["trace_id"], tool, redact(tool_input_text)))[:24]
|
||||
h2_decision = h2_registry_gate(action, tool, rec.get("actor", ""), rec["trace_id"], key)
|
||||
rec.setdefault("policy_decisions", []).append(h2_decision)
|
||||
add_evidence(
|
||||
rec, "H2", "tool-registry", h2_decision["decision"],
|
||||
h2_decision["reason_code"], facts=h2_decision,
|
||||
)
|
||||
if not h2_decision["execution_allowed"]:
|
||||
save_admission(rec)
|
||||
return _base_response("pre-tool", "deny", admission_id=admission_id,
|
||||
trace_id=rec.get("trace_id"), reason=h2_decision["reason_code"],
|
||||
policy_decision=h2_decision)
|
||||
if not h2_decision["certifiable"]:
|
||||
rec["integration_mode"] = "observed_only"
|
||||
rec["certification_strength"] = "observed_only"
|
||||
|
||||
add_evidence(rec, "H2/H4", "pre-tool", "allow",
|
||||
"tool=%s side_effect=%s" % (tool, side_effect))
|
||||
"tool=%s side_effect=%s action=%s" % (tool, side_effect, action),
|
||||
facts={"action_risk": risk, "registry": registry})
|
||||
save_admission(rec)
|
||||
return _base_response("pre-tool", "allow", admission_id=admission_id,
|
||||
trace_id=rec.get("trace_id"), reason="allowed")
|
||||
response = _base_response("pre-tool", "allow", admission_id=admission_id,
|
||||
trace_id=rec.get("trace_id"), reason="allowed",
|
||||
policy_decision=risk)
|
||||
if registry["unsafe_bypass"]:
|
||||
response["warnings"].append("HIGH: H2 registry bypass active in development/test; execution is not certifiable")
|
||||
return response
|
||||
|
||||
|
||||
def single_step_failure_policy(side_effect):
|
||||
decision = evaluate_failure_policy(
|
||||
side_effecting=side_effect,
|
||||
mode=enforcement_mode(),
|
||||
profile=os.environ.get("CASAN_PROFILE", "development"),
|
||||
configured=os.environ.get("CASAN_SINGLE_STEP_FAILURE_POLICY"),
|
||||
)
|
||||
return decision["failure_policy"], decision["source"]
|
||||
|
||||
|
||||
def op_post_tool(req):
|
||||
@@ -1045,11 +1216,52 @@ def op_post_tool(req):
|
||||
reason="no_admission")
|
||||
rec["tool_calls"] = rec.get("tool_calls", 0) + 1
|
||||
status = req.get("status") or "success"
|
||||
if status in ("error", "timeout"):
|
||||
risk = evaluate_risk(
|
||||
action=str(rec.get("last_action") or ""),
|
||||
tool=str(req.get("tool") or rec.get("last_tool") or ""),
|
||||
command=str(req.get("command") or ""),
|
||||
actor=rec.get("actor", ""),
|
||||
environment=os.environ.get("CASAN_PROFILE", "development"),
|
||||
)
|
||||
side_effect = bool(risk["side_effecting"])
|
||||
if status in ("error", "timeout", "denied", "failed"):
|
||||
rec["failures"] = rec.get("failures", 0) + 1
|
||||
if side_effect:
|
||||
rec["side_effect_failures"] = rec.get("side_effect_failures", 0) + 1
|
||||
policy, source = single_step_failure_policy(side_effect)
|
||||
rec["failure_policy"] = policy
|
||||
rec["execution_status"] = "failed"
|
||||
rec["assurance_status"] = "failed"
|
||||
add_evidence(
|
||||
rec, "H6", "failure-policy", policy,
|
||||
"tool=%s status=%s policy=%s" % (req.get("tool"), status, policy),
|
||||
facts={
|
||||
"command_executed": status not in ("denied",),
|
||||
"command_outcome": status,
|
||||
"telemetry_recorded": True,
|
||||
"assurance_may_continue": policy == "record_only",
|
||||
"failure_policy": policy,
|
||||
"policy_source": source,
|
||||
"side_effecting": side_effect,
|
||||
},
|
||||
)
|
||||
add_evidence(rec, "H5", "post-tool", status,
|
||||
"tool=%s dur_ms=%s result=%s" % (req.get("tool"), req.get("duration_ms"),
|
||||
redact(req.get("result"), 80)),
|
||||
facts={"risk": risk, "failure_policy": policy})
|
||||
save_admission(rec)
|
||||
decision = "recorded" if policy == "record_only" else policy
|
||||
return _base_response(
|
||||
"post-tool", decision, admission_id=rec["admission_id"],
|
||||
trace_id=rec.get("trace_id"), reason="tool_failed:%s" % policy,
|
||||
execution_outcome="failed", assurance_outcome="failed",
|
||||
assurance_may_continue=policy == "record_only",
|
||||
)
|
||||
rec["execution_status"] = rec.get("execution_status") or "success"
|
||||
add_evidence(rec, "H5", "post-tool", status,
|
||||
"tool=%s dur_ms=%s result=%s" % (req.get("tool"), req.get("duration_ms"),
|
||||
redact(req.get("result"), 80)))
|
||||
redact(req.get("result"), 80)),
|
||||
facts={"risk": risk, "command_outcome": status, "telemetry_recorded": True})
|
||||
save_admission(rec)
|
||||
return _base_response("post-tool", "recorded", admission_id=rec["admission_id"],
|
||||
trace_id=rec.get("trace_id"), reason="evidence_appended")
|
||||
@@ -1092,6 +1304,10 @@ def op_finalize(req):
|
||||
rec["summary_hash"] = salted_hash(req.get("assistant_summary"))
|
||||
stop_reason = req.get("stop_reason") or "completed"
|
||||
status = "success" if stop_reason in ("completed", "max_turns") else "failed"
|
||||
if rec.get("failures", 0) > 0:
|
||||
# Telemetry success is not execution success. A failed tool remains a
|
||||
# failed execution even if the client later emits Stop(completed).
|
||||
status = "failed"
|
||||
|
||||
observed_harnesses = {
|
||||
harness
|
||||
@@ -1110,6 +1326,8 @@ def op_finalize(req):
|
||||
# H3/H5/H7 finalize controls: run the H4 output filter over the assistant
|
||||
# summary as the closing verification control.
|
||||
reasons = []
|
||||
if rec.get("failures", 0) > 0:
|
||||
reasons.append("tool_execution_failed")
|
||||
if req.get("assistant_summary"):
|
||||
h4 = h4_scan(req.get("assistant_summary"), "output")
|
||||
if h4 == "ok":
|
||||
@@ -1158,7 +1376,17 @@ def op_finalize(req):
|
||||
reasons.append("coverage_bypass")
|
||||
if status != "success":
|
||||
certified = False
|
||||
reasons.append("stop_%s" % stop_reason)
|
||||
failure_reason = "failed_tool_outcome" if rec.get("failures", 0) > 0 else "stop_%s" % stop_reason
|
||||
if failure_reason not in reasons:
|
||||
reasons.append(failure_reason)
|
||||
trust = evaluate_trust_capabilities()
|
||||
add_evidence(
|
||||
rec, "H7", "trust-capability", "pass" if trust["certifiable"] else "degraded",
|
||||
",".join(trust["reason_codes"]), facts=trust, category="readiness_check",
|
||||
)
|
||||
if trust["production"] and not trust["certifiable"]:
|
||||
certified = False
|
||||
reasons.extend(reason for reason in trust["reason_codes"] if reason not in reasons)
|
||||
if not certified and not reasons:
|
||||
reasons.append("unknown")
|
||||
if certified:
|
||||
@@ -1168,12 +1396,15 @@ def op_finalize(req):
|
||||
rec["certified"] = certified
|
||||
rec["finalized"] = True
|
||||
rec["finalized_at"] = now_iso()
|
||||
rec["execution_status"] = status
|
||||
rec["assurance_status"] = "passed" if certified else "failed" if rec.get("failures", 0) else "degraded"
|
||||
add_evidence(
|
||||
rec,
|
||||
"H7",
|
||||
"certification",
|
||||
"certified" if certified else "non_certified",
|
||||
",".join(reasons),
|
||||
category="certification_claim",
|
||||
)
|
||||
|
||||
quality, warnings, missing = classify_telemetry(rec)
|
||||
@@ -1185,13 +1416,26 @@ def op_finalize(req):
|
||||
",".join(warnings) if warnings else "provider_usage_complete",
|
||||
)
|
||||
h6_record = write_h6_record(rec, status, quality, warnings, missing)
|
||||
try:
|
||||
write_kernel_bundle(rec)
|
||||
except (OSError, ValueError, TypeError) as exc:
|
||||
certified = False
|
||||
rec["certified"] = False
|
||||
rec["state"] = "NonCertified"
|
||||
rec["assurance_status"] = "failed"
|
||||
if "kernel_contract_emission_failed" not in reasons:
|
||||
reasons.append("kernel_contract_emission_failed")
|
||||
add_evidence(rec, "H7", "kernel-contract", "failed",
|
||||
"kernel_contract_emission_failed:%s" % type(exc).__name__)
|
||||
trace_path = write_trace_file(rec, certified, reasons)
|
||||
save_admission(rec)
|
||||
|
||||
resp = _base_response("finalize", "certified" if certified else "non_certified",
|
||||
admission_id=rec["admission_id"], trace_id=rec.get("trace_id"),
|
||||
certification_strength=strength, telemetry_quality=quality,
|
||||
reason=",".join(reasons))
|
||||
reason=",".join(reasons), execution_outcome=status,
|
||||
assurance_outcome=rec.get("assurance_status"),
|
||||
certification_outcome="certified" if certified else "non_certified")
|
||||
resp["warnings"].extend(warnings)
|
||||
resp["context"] = "trace=%s certified=%s" % (os.path.basename(trace_path), certified)
|
||||
report_url = dashboard_url(rec.get("trace_id"))
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI boundary for shell/native runtimes to consume Assurance Kernel policy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HARNESS_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(HARNESS_ROOT))
|
||||
|
||||
from kernel.adapters import NativeHarnessAdapter # noqa: E402
|
||||
from kernel.contracts import validate_bundle # noqa: E402
|
||||
from kernel.policy import ( # noqa: E402
|
||||
H2GateContext,
|
||||
evaluate_h2_gate,
|
||||
evaluate_registry_configuration,
|
||||
evaluate_risk,
|
||||
evaluate_trust_capabilities,
|
||||
)
|
||||
|
||||
|
||||
def _print(payload: object) -> None:
|
||||
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
|
||||
|
||||
|
||||
def _read_content(path: str | None) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def _append_jsonl(path: str, payload: object) -> None:
|
||||
target = Path(path)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
fd = os.open(str(target), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
|
||||
try:
|
||||
try:
|
||||
import fcntl
|
||||
fcntl.flock(fd, fcntl.LOCK_EX)
|
||||
except (ImportError, OSError):
|
||||
pass
|
||||
os.write(fd, data)
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def cmd_risk(args: argparse.Namespace) -> int:
|
||||
payload = evaluate_risk(
|
||||
action=args.action,
|
||||
tool=args.tool,
|
||||
resource=args.resource,
|
||||
command=args.command,
|
||||
content=_read_content(args.content_file),
|
||||
actor=args.actor,
|
||||
environment=args.environment,
|
||||
)
|
||||
_print(payload)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_registry(args: argparse.Namespace) -> int:
|
||||
payload = evaluate_registry_configuration(args.mode, args.profile, args.explicit)
|
||||
if args.evidence_log:
|
||||
_append_jsonl(args.evidence_log, payload)
|
||||
_print(payload)
|
||||
return 0 if payload["valid"] else 2
|
||||
|
||||
|
||||
def cmd_h2(args: argparse.Namespace) -> int:
|
||||
context = H2GateContext(
|
||||
mode=args.mode,
|
||||
actor=args.actor,
|
||||
action=args.action,
|
||||
tool=args.tool,
|
||||
execution_id=args.execution_id,
|
||||
enforcement_path=args.enforcement_path,
|
||||
idempotency_key=args.idempotency_key,
|
||||
timeout_seconds=args.timeout,
|
||||
)
|
||||
payload = evaluate_h2_gate(args.gate, context, bash_path=args.bash)
|
||||
if args.evidence_log:
|
||||
_append_jsonl(args.evidence_log, payload)
|
||||
_print(payload)
|
||||
return 0 if payload["execution_allowed"] else 2
|
||||
|
||||
|
||||
def cmd_trust(_args: argparse.Namespace) -> int:
|
||||
payload = evaluate_trust_capabilities()
|
||||
_print(payload)
|
||||
return 0 if payload["ready"] else 2
|
||||
|
||||
|
||||
def cmd_native(args: argparse.Namespace) -> int:
|
||||
event = json.load(sys.stdin) if args.event_file == "-" else json.load(open(args.event_file, encoding="utf-8"))
|
||||
payload = NativeHarnessAdapter().map_execution(event)
|
||||
errors = validate_bundle(payload)
|
||||
if errors:
|
||||
_print({"valid": False, "errors": errors, "bundle": payload})
|
||||
return 2
|
||||
_print(payload)
|
||||
return 0
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
root = argparse.ArgumentParser()
|
||||
sub = root.add_subparsers(dest="command", required=True)
|
||||
risk = sub.add_parser("risk")
|
||||
for name in ("action", "tool", "resource", "command", "actor", "environment"):
|
||||
risk.add_argument(f"--{name}", default="")
|
||||
risk.add_argument("--content-file")
|
||||
risk.set_defaults(func=cmd_risk)
|
||||
|
||||
registry = sub.add_parser("registry-config")
|
||||
registry.add_argument("--mode", default="observe")
|
||||
registry.add_argument("--profile", default="development")
|
||||
registry.add_argument("--explicit")
|
||||
registry.add_argument("--evidence-log")
|
||||
registry.set_defaults(func=cmd_registry)
|
||||
|
||||
h2 = sub.add_parser("h2-gate")
|
||||
h2.add_argument("--gate", required=True)
|
||||
h2.add_argument("--mode", choices=("observe", "enforce"), required=True)
|
||||
h2.add_argument("--actor", default="")
|
||||
h2.add_argument("--action", required=True)
|
||||
h2.add_argument("--tool", required=True)
|
||||
h2.add_argument("--execution-id", required=True)
|
||||
h2.add_argument("--enforcement-path", required=True)
|
||||
h2.add_argument("--idempotency-key", required=True)
|
||||
h2.add_argument("--timeout", type=float, default=8.0)
|
||||
h2.add_argument("--bash", default="bash")
|
||||
h2.add_argument("--evidence-log")
|
||||
h2.set_defaults(func=cmd_h2)
|
||||
|
||||
trust = sub.add_parser("trust-capabilities")
|
||||
trust.set_defaults(func=cmd_trust)
|
||||
|
||||
native = sub.add_parser("adapt-native")
|
||||
native.add_argument("event_file")
|
||||
native.set_defaults(func=cmd_native)
|
||||
return root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
return int(args.func(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -199,6 +199,9 @@ def _check(
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"gate": gate,
|
||||
"legacy_gate": gate,
|
||||
"category": "readiness_check",
|
||||
"check_id": "ReadinessCheck.%s" % gate,
|
||||
"title": title,
|
||||
"status": status,
|
||||
"summary": summary,
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic unit and cross-runtime conformance tests for the kernel."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
HARNESS_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(HARNESS_ROOT))
|
||||
|
||||
from kernel.adapters import AgenticBridgeAdapter, NativeHarnessAdapter
|
||||
from kernel.contracts import validate_bundle
|
||||
from kernel.policy import (
|
||||
H2GateContext,
|
||||
classify_action,
|
||||
evaluate_failure_policy,
|
||||
evaluate_h2_gate,
|
||||
evaluate_registry_configuration,
|
||||
evaluate_risk,
|
||||
evaluate_trust_capabilities,
|
||||
)
|
||||
from kernel.taxonomy import CertificationClaim, ReadinessCheck, ReportDimension, RuntimeControl, same_legacy_label
|
||||
from kernel.supervision import RuntimeCapabilities, negotiate_intervention
|
||||
|
||||
|
||||
class RiskFloorTests(unittest.TestCase):
|
||||
def test_benign_deploy_has_high_floor_and_requires_approval(self) -> None:
|
||||
decision = evaluate_risk(action="deploy", content="publish a harmless documentation update", actor="alice")
|
||||
self.assertEqual(decision["action_class"], "deployment")
|
||||
self.assertEqual(decision["risk_factors"]["content_risk"], "low")
|
||||
self.assertEqual(decision["effective_risk"], "high")
|
||||
self.assertEqual(decision["decision"], "require_approval")
|
||||
|
||||
def test_effective_risk_is_maximum_of_all_factors(self) -> None:
|
||||
decision = evaluate_risk(action="read", resource="/.ssh/id_rsa", actor="alice")
|
||||
self.assertEqual(decision["risk_factors"]["action_risk"], "high")
|
||||
self.assertEqual(decision["risk_factors"]["resource_risk"], "high")
|
||||
self.assertEqual(decision["effective_risk"], "high")
|
||||
|
||||
def test_missing_actor_denies_high_impact_action(self) -> None:
|
||||
decision = evaluate_risk(action="migration", content="apply schema", actor="")
|
||||
self.assertEqual(decision["decision"], "deny")
|
||||
self.assertIn("actor_identity_required", decision["reason_codes"])
|
||||
|
||||
def test_read_only_shell_command_is_not_side_effecting(self) -> None:
|
||||
decision = classify_action(tool="Bash", command="git status --short")
|
||||
self.assertEqual(decision["action_class"], "read_only")
|
||||
self.assertFalse(decision["side_effecting"])
|
||||
|
||||
|
||||
class RegistryConfigurationTests(unittest.TestCase):
|
||||
def test_enforce_defaults_registry_on(self) -> None:
|
||||
result = evaluate_registry_configuration("enforce", "development", None)
|
||||
self.assertTrue(result["enabled"])
|
||||
self.assertEqual(result["reason_code"], "h2_registry_default_enabled")
|
||||
|
||||
def test_development_bypass_is_visible_and_non_silent(self) -> None:
|
||||
result = evaluate_registry_configuration("enforce", "test", "0")
|
||||
self.assertFalse(result["enabled"])
|
||||
self.assertTrue(result["unsafe_bypass"])
|
||||
self.assertEqual(result["severity"], "high")
|
||||
|
||||
def test_production_bypass_is_rejected(self) -> None:
|
||||
result = evaluate_registry_configuration("enforce", "production", "0")
|
||||
self.assertFalse(result["valid"])
|
||||
self.assertEqual(result["reason_code"], "h2_registry_bypass_forbidden")
|
||||
|
||||
|
||||
class FailurePolicyTests(unittest.TestCase):
|
||||
def test_all_supported_policies_are_explicit(self) -> None:
|
||||
for configured in ("halt", "quarantine", "require_approval", "record_only"):
|
||||
result = evaluate_failure_policy(
|
||||
side_effecting=False, mode="observe", profile="test", configured=configured,
|
||||
)
|
||||
self.assertEqual(result["failure_policy"], configured)
|
||||
self.assertEqual(result["execution_result"], "failed")
|
||||
self.assertEqual(result["telemetry_result"], "recorded")
|
||||
|
||||
def test_production_side_effect_cannot_be_record_only(self) -> None:
|
||||
result = evaluate_failure_policy(
|
||||
side_effecting=True, mode="enforce", profile="production", configured="record_only",
|
||||
)
|
||||
self.assertEqual(result["failure_policy"], "halt")
|
||||
self.assertFalse(result["valid"])
|
||||
self.assertEqual(result["source"], "production_record_only_forbidden")
|
||||
|
||||
def test_invalid_policy_fails_closed(self) -> None:
|
||||
result = evaluate_failure_policy(
|
||||
side_effecting=True, mode="enforce", profile="test", configured="continue_anyway",
|
||||
)
|
||||
self.assertEqual(result["failure_policy"], "halt")
|
||||
self.assertFalse(result["valid"])
|
||||
|
||||
|
||||
class TaxonomyTests(unittest.TestCase):
|
||||
def test_same_h_label_cannot_be_interpreted_as_same_category(self) -> None:
|
||||
self.assertTrue(same_legacy_label(RuntimeControl.H4, ReadinessCheck.H4))
|
||||
self.assertNotEqual(RuntimeControl.H4.value, ReadinessCheck.H4.value)
|
||||
self.assertNotEqual(ReportDimension.H4.value, CertificationClaim.H4.value)
|
||||
|
||||
def test_unsupported_runtime_intervention_is_explicit(self) -> None:
|
||||
result = negotiate_intervention("i-1", "roll_back", "operator", RuntimeCapabilities())
|
||||
self.assertEqual(result["status"], "unsupported")
|
||||
self.assertEqual(result["reason_code"], "runtime_intervention_unsupported")
|
||||
|
||||
|
||||
class H2DependencyTests(unittest.TestCase):
|
||||
def context(self, mode: str) -> H2GateContext:
|
||||
return H2GateContext(mode, "alice", "write_file", "Edit", "run-1", "test.h2", "idem", 0.5)
|
||||
|
||||
def script(self, directory: str, body: str) -> str:
|
||||
path = Path(directory) / "gate.sh"
|
||||
path.write_text("#!/usr/bin/env bash\n" + body + "\n", encoding="utf-8")
|
||||
path.chmod(0o700)
|
||||
return str(path)
|
||||
|
||||
def test_missing_gate_denies_enforce_and_degrades_observe(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
missing = str(Path(temp) / "missing.sh")
|
||||
enforced = evaluate_h2_gate(missing, self.context("enforce"))
|
||||
observed = evaluate_h2_gate(missing, self.context("observe"))
|
||||
self.assertFalse(enforced["execution_allowed"])
|
||||
self.assertEqual(enforced["reason_code"], "h2_gate_unavailable")
|
||||
self.assertTrue(observed["execution_allowed"])
|
||||
self.assertEqual(observed["decision"], "observe_only")
|
||||
self.assertFalse(observed["certifiable"])
|
||||
|
||||
def test_permission_denied_fails_closed(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = self.script(temp, "echo 'TOOL_APPROVED tool=x reason=test'")
|
||||
os.chmod(path, 0)
|
||||
result = evaluate_h2_gate(path, self.context("enforce"))
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
self.assertFalse(result["execution_allowed"])
|
||||
self.assertEqual(result["reason_code"], "h2_gate_permission_denied")
|
||||
|
||||
def test_timeout_malformed_and_internal_exception_fail_closed(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
timeout = self.script(temp, "sleep 1")
|
||||
timed = evaluate_h2_gate(timeout, self.context("enforce"))
|
||||
malformed = self.script(temp, "echo nonsense")
|
||||
malformed_result = evaluate_h2_gate(malformed, self.context("enforce"))
|
||||
with patch("kernel.policy.subprocess.run", side_effect=OSError("boom")):
|
||||
internal = evaluate_h2_gate(malformed, self.context("enforce"))
|
||||
self.assertEqual(timed["reason_code"], "h2_gate_timeout")
|
||||
self.assertEqual(malformed_result["reason_code"], "h2_gate_malformed_response")
|
||||
self.assertEqual(internal["reason_code"], "h2_gate_internal_error")
|
||||
self.assertTrue(all(not item["execution_allowed"] for item in (timed, malformed_result, internal)))
|
||||
|
||||
def test_explicit_approval_and_denial_protocols(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
allowed = self.script(temp, "echo 'TOOL_APPROVED tool=write_file reason=registered'")
|
||||
allow_result = evaluate_h2_gate(allowed, self.context("enforce"))
|
||||
denied = self.script(temp, "echo 'TOOL_DENIED tool=write_file reason=missing_agent_identity' >&2; exit 2")
|
||||
deny_result = evaluate_h2_gate(denied, self.context("enforce"))
|
||||
self.assertTrue(allow_result["execution_allowed"])
|
||||
self.assertFalse(deny_result["execution_allowed"])
|
||||
self.assertEqual(deny_result["reason_code"], "h2_denied:missing_agent_identity")
|
||||
|
||||
|
||||
class TrustRootTests(unittest.TestCase):
|
||||
def test_production_refuses_local_fallback(self) -> None:
|
||||
result = evaluate_trust_capabilities({"CASAN_PROFILE": "production"})
|
||||
self.assertFalse(result["ready"])
|
||||
self.assertFalse(result["certifiable"])
|
||||
self.assertIn("external_signing_trust_root_required", result["reason_codes"])
|
||||
|
||||
def test_external_configuration_is_capable_but_not_claimed_provisioned(self) -> None:
|
||||
result = evaluate_trust_capabilities({
|
||||
"CASAN_PROFILE": "production",
|
||||
"CASAN_SIGNING_PROVIDER": "vault_kms",
|
||||
"VAULT_ADDR": "https://vault.example",
|
||||
"VAULT_TOKEN": "redacted-runtime-token",
|
||||
"CASAN_IMMUTABLE_ANCHOR_PROVIDER": "s3_object_lock",
|
||||
"CASAN_S3_BUCKET": "audit-lock",
|
||||
"CASAN_S3_KMS_KEY_ID": "kms-key",
|
||||
})
|
||||
self.assertTrue(result["ready"])
|
||||
self.assertTrue(result["certifiable"])
|
||||
|
||||
def test_emergency_override_never_certifies(self) -> None:
|
||||
result = evaluate_trust_capabilities({
|
||||
"CASAN_PROFILE": "production",
|
||||
"CASAN_TRUST_EMERGENCY_OVERRIDE": "1",
|
||||
})
|
||||
self.assertFalse(result["certifiable"])
|
||||
self.assertEqual(result["severity"], "critical")
|
||||
|
||||
|
||||
class CrossRuntimeConformanceTests(unittest.TestCase):
|
||||
def events(self, **overrides: object) -> tuple[dict[str, object], dict[str, object]]:
|
||||
common: dict[str, object] = {
|
||||
"run_id": "run-1", "trace_id": "run-1", "action": "deploy", "tool": "Bash",
|
||||
"command": "deploy harmless docs", "actor": "alice", "mode": "enforce",
|
||||
"execution_status": "failed", "assurance_status": "failed", "certification_status": "non_certified",
|
||||
"certified": False, "correlation_id": "corr-1", "parent_run_id": "parent-1",
|
||||
"parent_step_id": "parent-step-1", "evidence": [
|
||||
{"evidence_id": "e-1", "decision": "deny", "category": "runtime_control"},
|
||||
],
|
||||
}
|
||||
common.update(overrides)
|
||||
return dict(common), dict(common, adapter_version="20.2.0")
|
||||
|
||||
def map_both(self, **overrides: object) -> tuple[dict[str, object], dict[str, object]]:
|
||||
native_event, agentic_event = self.events(**overrides)
|
||||
native = NativeHarnessAdapter().map_execution(native_event)
|
||||
agentic = AgenticBridgeAdapter().map_execution(agentic_event)
|
||||
return native, agentic
|
||||
|
||||
def test_01_same_dangerous_action_classification(self) -> None:
|
||||
native, agentic = self.map_both()
|
||||
self.assertEqual(validate_bundle(native), [])
|
||||
self.assertEqual(validate_bundle(agentic), [])
|
||||
self.assertEqual(native["run"]["risk_summary"]["action_class"], "deployment")
|
||||
self.assertEqual(agentic["run"]["risk_summary"]["action_class"], "deployment")
|
||||
self.assertEqual(native["run"]["risk_summary"]["effective_risk"], "high")
|
||||
|
||||
def test_02_same_missing_actor_denial(self) -> None:
|
||||
native, agentic = self.map_both(actor="")
|
||||
for bundle in (native, agentic):
|
||||
self.assertEqual(bundle["run"]["risk_summary"]["decision"], "deny")
|
||||
self.assertIn("actor_identity_required", bundle["run"]["risk_summary"]["reason_codes"])
|
||||
|
||||
def test_03_same_missing_h2_gate_denial(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
missing = str(Path(temp) / "missing.sh")
|
||||
decisions = [
|
||||
evaluate_h2_gate(missing, H2GateContext("enforce", "alice", "write_file", "Edit", "run-1", point, "idem"))
|
||||
for point in ("native-harness.h2", "agentic-bridge.h2")
|
||||
]
|
||||
native, agentic = self.map_both(
|
||||
action="write_file", tool="Edit", command="safe edit",
|
||||
policy_decisions=[decisions[0]], evidence=[{"evidence_id": "e-1", "decision": "deny"}],
|
||||
)
|
||||
# The second adapter receives the same kernel decision with only its
|
||||
# enforcement-point extension changed.
|
||||
agentic_event = self.events(
|
||||
action="write_file", tool="Edit", command="safe edit",
|
||||
policy_decisions=[decisions[1]], evidence=[{"evidence_id": "e-1", "decision": "deny"}],
|
||||
)[1]
|
||||
agentic = AgenticBridgeAdapter().map_execution(agentic_event)
|
||||
for bundle in (native, agentic):
|
||||
decision = bundle["steps"][0]["policy_decisions"][0]
|
||||
self.assertEqual(decision["decision"], "deny")
|
||||
self.assertIn("h2_gate_unavailable", decision["reason_codes"])
|
||||
|
||||
def test_04_same_approval_requirement(self) -> None:
|
||||
native, agentic = self.map_both(actor="alice")
|
||||
for bundle in (native, agentic):
|
||||
self.assertEqual(bundle["run"]["risk_summary"]["decision"], "require_approval")
|
||||
self.assertEqual(bundle["run"]["risk_summary"]["evidence_requirement"], "required")
|
||||
|
||||
def test_05_same_claim_evidence_relationship(self) -> None:
|
||||
native, agentic = self.map_both()
|
||||
for bundle in (native, agentic):
|
||||
self.assertEqual(bundle["claims"][0]["evidence_refs"], ["e-1"])
|
||||
self.assertEqual(bundle["evidence"][0]["claim_refs"], [bundle["claims"][0]["claim_id"]])
|
||||
|
||||
def test_06_observed_only_execution_is_never_certified(self) -> None:
|
||||
native, agentic = self.map_both(
|
||||
mode="observe", certification_status="certified", certified=True,
|
||||
)
|
||||
for bundle in (native, agentic):
|
||||
self.assertIn("observed_only_cannot_be_certified", validate_bundle(bundle))
|
||||
|
||||
def test_07_execution_and_assurance_outcomes_are_distinct(self) -> None:
|
||||
native, agentic = self.map_both(
|
||||
execution_status="failed", assurance_status="passed", certification_status="non_certified",
|
||||
)
|
||||
for bundle in (native, agentic):
|
||||
outcome = bundle["run"]["outcome"]
|
||||
self.assertEqual(outcome["execution_result"], "failed")
|
||||
self.assertEqual(outcome["assurance_result"], "passed")
|
||||
self.assertEqual(outcome["certification_result"], "non_certified")
|
||||
|
||||
def test_08_correlation_and_causation_are_preserved(self) -> None:
|
||||
native, agentic = self.map_both()
|
||||
for bundle in (native, agentic):
|
||||
self.assertEqual(bundle["run"]["correlation_id"], "corr-1")
|
||||
self.assertEqual(bundle["run"]["parent_run_id"], "parent-1")
|
||||
self.assertEqual(bundle["steps"][0]["parent_step_id"], "parent-step-1")
|
||||
self.assertEqual(bundle["trace_links"][0]["from"], "run-1")
|
||||
|
||||
def test_09_same_verifier_detects_evidence_tampering(self) -> None:
|
||||
native, agentic = self.map_both()
|
||||
for original in (native, agentic):
|
||||
self.assertEqual(validate_bundle(original), [])
|
||||
tampered = deepcopy(original)
|
||||
tampered["evidence"][0]["artifact_ref"] = "inline:tampered"
|
||||
self.assertIn("evidence.0.integrity_invalid", validate_bundle(tampered))
|
||||
|
||||
def test_10_runtime_details_are_namespaced_extensions(self) -> None:
|
||||
native, agentic = self.map_both()
|
||||
self.assertNotEqual(native["run"]["runtime"]["type"], agentic["run"]["runtime"]["type"])
|
||||
self.assertIn("casan.runtime.casan-native-harness", native["extensions"])
|
||||
self.assertIn("casan.runtime.agentic-bridge", agentic["extensions"])
|
||||
|
||||
def test_delegation_depth_is_enforced(self) -> None:
|
||||
native, _ = self.map_both()
|
||||
native["actors"].append({
|
||||
"actor_id": "child", "actor_type": "child_agent", "issuer": "alice",
|
||||
"trust_level": "verified", "authentication_evidence_ref": None,
|
||||
})
|
||||
native["delegations"] = [
|
||||
{"delegation_id": "d1", "delegator_ref": "alice", "delegate_ref": "child", "allowed_authority": ["read"], "resource_scope": ["project"], "expiry": "2030-01-01T00:00:00Z", "maximum_depth": 3, "parent_delegation_ref": None, "revoked": False, "approval_ref": None},
|
||||
{"delegation_id": "d2", "delegator_ref": "alice", "delegate_ref": "child", "allowed_authority": ["read"], "resource_scope": ["project"], "expiry": "2030-01-01T00:00:00Z", "maximum_depth": 3, "parent_delegation_ref": "d1", "revoked": False, "approval_ref": None},
|
||||
{"delegation_id": "d3", "delegator_ref": "alice", "delegate_ref": "child", "allowed_authority": ["read"], "resource_scope": ["project"], "expiry": "2030-01-01T00:00:00Z", "maximum_depth": 1, "parent_delegation_ref": "d2", "revoked": False, "approval_ref": None},
|
||||
]
|
||||
self.assertIn("delegations.2.maximum_depth_exceeded", validate_bundle(native))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Focused bridge integration regressions for the Assurance Kernel upgrade."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
HARNESS_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(HARNESS_ROOT / "scripts" / "python"))
|
||||
|
||||
import agentic_bridge as bridge
|
||||
|
||||
|
||||
class BridgeUpgradeIntegrationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.state = str(Path(self.temp.name) / "state")
|
||||
self.project = str(HARNESS_ROOT.parent.parent)
|
||||
self.base_env = {
|
||||
"CASAN_STATE_ROOT": self.state,
|
||||
"CASAN_AGENTIC_ENFORCEMENT_MODE": "enforce",
|
||||
"CASAN_AGENT": "boss",
|
||||
"CASAN_ACTOR": "boss",
|
||||
"CASAN_PROFILE": "test",
|
||||
}
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp.cleanup()
|
||||
|
||||
def begin(self, actor: str = "boss") -> dict[str, object]:
|
||||
return bridge.op_begin({
|
||||
"op": "begin", "client": "codex", "project": self.project,
|
||||
"session": "integration", "prompt": "edit a source file safely",
|
||||
"integration_mode": "project_hook", "actor": actor,
|
||||
})
|
||||
|
||||
def fixture_gate(self, body: str) -> str:
|
||||
path = Path(self.temp.name) / "h2-gate.sh"
|
||||
path.write_text("#!/usr/bin/env bash\n" + body + "\n", encoding="utf-8")
|
||||
path.chmod(0o700)
|
||||
return str(path)
|
||||
|
||||
def pre_edit(self, admission_id: str) -> dict[str, object]:
|
||||
return bridge.op_pre_tool({
|
||||
"op": "pre-tool", "admission_id": admission_id, "tool": "Edit",
|
||||
"tool_input": "update a source file", "project": self.project,
|
||||
})
|
||||
|
||||
def test_missing_h2_gate_denies_enforce_with_structured_evidence(self) -> None:
|
||||
missing = str(Path(self.temp.name) / "missing.sh")
|
||||
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": missing}, clear=False):
|
||||
begin = self.begin()
|
||||
result = self.pre_edit(str(begin["admission_id"]))
|
||||
record = bridge.load_admission(str(begin["admission_id"]))
|
||||
self.assertEqual(result["decision"], "deny")
|
||||
self.assertEqual(result["reason"], "h2_gate_unavailable")
|
||||
evidence = next(item for item in record["evidence"] if item["kind"] == "tool-registry")
|
||||
self.assertEqual(evidence["facts"]["mode"], "enforce")
|
||||
self.assertEqual(evidence["facts"]["actor"], "boss")
|
||||
self.assertEqual(evidence["facts"]["tool"], "Edit")
|
||||
self.assertEqual(evidence["facts"]["execution_id"], begin["trace_id"])
|
||||
self.assertEqual(evidence["facts"]["enforcement_path"], "agentic_bridge.pre_tool.h2_registry")
|
||||
|
||||
def test_missing_h2_gate_observe_allows_only_degraded_non_certifiable_execution(self) -> None:
|
||||
missing = str(Path(self.temp.name) / "missing.sh")
|
||||
env = {**self.base_env, "CASAN_AGENTIC_ENFORCEMENT_MODE": "observe", "CASAN_H2_GATE_PATH": missing}
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
begin = self.begin()
|
||||
result = self.pre_edit(str(begin["admission_id"]))
|
||||
finalized = bridge.op_finalize({"op": "finalize", "admission_id": begin["admission_id"], "stop_reason": "completed"})
|
||||
self.assertEqual(result["decision"], "allow")
|
||||
self.assertEqual(finalized["decision"], "non_certified")
|
||||
self.assertEqual(finalized["certification_strength"], "observed_only")
|
||||
|
||||
def test_registry_defaults_on_and_development_bypass_is_visible(self) -> None:
|
||||
missing = str(Path(self.temp.name) / "missing.sh")
|
||||
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": missing}, clear=False):
|
||||
begin = self.begin()
|
||||
default_result = self.pre_edit(str(begin["admission_id"]))
|
||||
self.assertEqual(default_result["reason"], "h2_gate_unavailable")
|
||||
|
||||
with patch.dict(os.environ, {**self.base_env, "CASAN_AGENTIC_H2_REGISTRY": "0", "CASAN_H2_GATE_PATH": missing}, clear=False):
|
||||
begin = self.begin()
|
||||
bypass = self.pre_edit(str(begin["admission_id"]))
|
||||
record = bridge.load_admission(str(begin["admission_id"]))
|
||||
self.assertEqual(bypass["decision"], "allow")
|
||||
self.assertTrue(any("HIGH" in warning for warning in bypass["warnings"]))
|
||||
self.assertEqual(record["certification_strength"], "observed_only")
|
||||
|
||||
def test_benign_deploy_requires_approval_from_action_floor(self) -> None:
|
||||
gate = self.fixture_gate("echo 'TOOL_APPROVED tool=deploy reason=registered'")
|
||||
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": gate}, clear=False):
|
||||
begin = self.begin()
|
||||
result = bridge.op_pre_tool({
|
||||
"op": "pre-tool", "admission_id": begin["admission_id"], "tool": "Bash",
|
||||
"tool_input": "deploy harmless documentation", "project": self.project,
|
||||
})
|
||||
self.assertEqual(result["decision"], "require_approval")
|
||||
risk = result["policy_decision"]
|
||||
self.assertEqual(risk["action_class"], "deployment")
|
||||
self.assertEqual(risk["risk_factors"]["content_risk"], "medium")
|
||||
self.assertEqual(risk["effective_risk"], "high")
|
||||
|
||||
def test_structured_shell_payload_classifies_the_inner_read_only_command(self) -> None:
|
||||
with patch.dict(os.environ, self.base_env, clear=False):
|
||||
begin = self.begin()
|
||||
result = bridge.op_pre_tool({
|
||||
"op": "pre-tool", "admission_id": begin["admission_id"], "tool": "Bash",
|
||||
"tool_input": {"command": "ls"}, "project": self.project,
|
||||
})
|
||||
self.assertEqual(result["decision"], "allow")
|
||||
self.assertEqual(result["policy_decision"]["action_class"], "read_only")
|
||||
|
||||
def test_failed_side_effect_halts_and_cannot_finalize_successfully(self) -> None:
|
||||
gate = self.fixture_gate("echo 'TOOL_APPROVED tool=write_file reason=registered'")
|
||||
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": gate}, clear=False):
|
||||
begin = self.begin()
|
||||
admitted = self.pre_edit(str(begin["admission_id"]))
|
||||
post = bridge.op_post_tool({
|
||||
"op": "post-tool", "admission_id": begin["admission_id"], "tool": "Edit",
|
||||
"status": "error", "duration_ms": 2, "result": "write failed",
|
||||
})
|
||||
finalized = bridge.op_finalize({
|
||||
"op": "finalize", "admission_id": begin["admission_id"],
|
||||
"stop_reason": "completed", "assistant_summary": "completed",
|
||||
})
|
||||
self.assertEqual(admitted["decision"], "allow")
|
||||
self.assertEqual(post["decision"], "halt")
|
||||
self.assertFalse(post["assurance_may_continue"])
|
||||
self.assertEqual(finalized["decision"], "non_certified")
|
||||
self.assertEqual(finalized["execution_outcome"], "failed")
|
||||
self.assertEqual(finalized["assurance_outcome"], "failed")
|
||||
self.assertIn("failed_tool_outcome", finalized["reason"])
|
||||
kernel_files = list((Path(self.state) / "logs" / "kernel").glob("*.json"))
|
||||
self.assertEqual(len(kernel_files), 1)
|
||||
kernel = json.loads(kernel_files[0].read_text(encoding="utf-8"))
|
||||
self.assertEqual(kernel["run"]["outcome"]["execution_result"], "failed")
|
||||
self.assertEqual(kernel["run"]["outcome"]["certification_result"], "non_certified")
|
||||
|
||||
def test_production_without_external_trust_root_never_certifies(self) -> None:
|
||||
env = {**self.base_env, "CASAN_PROFILE": "production", "CASAN_AGENTIC_ENFORCEMENT_MODE": "enforce"}
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
begin = self.begin()
|
||||
finalized = bridge.op_finalize({"op": "finalize", "admission_id": begin["admission_id"], "stop_reason": "completed"})
|
||||
self.assertEqual(finalized["decision"], "non_certified")
|
||||
self.assertIn("external_signing_trust_root_required", finalized["reason"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -40,6 +40,8 @@ print(d.get(sys.argv[1],""))'
|
||||
echo "===== C1: normal turn = one admission + one trace + one metric (single model) ====="
|
||||
newstate
|
||||
export CASAN_AGENTIC_ENFORCEMENT_MODE=enforce
|
||||
export CASAN_AGENT=boss
|
||||
export CASAN_ACTOR=boss
|
||||
B=$(bridge '{"op":"begin","client":"claude-code","project":"'"$PROJ"'","session":"c1","prompt":"add a helper","integration_mode":"project_hook"}')
|
||||
DEC=$(printf '%s' "$B" | field decision)
|
||||
AID=$(printf '%s' "$B" | field admission_id)
|
||||
@@ -76,7 +78,8 @@ B=$(bridge '{"op":"begin","client":"claude-code","project":"'"$PROJ"'","session"
|
||||
AID=$(printf '%s' "$B" | field admission_id); TID=$(printf '%s' "$B" | field trace_id)
|
||||
OK3=1
|
||||
for tool in Bash Edit Write; do
|
||||
R=$(bridge '{"op":"pre-tool","admission_id":"'"$AID"'","tool":"'"$tool"'","tool_input":"x","project":"'"$PROJ"'"}')
|
||||
INPUT_VALUE="x"; [[ "$tool" == "Bash" ]] && INPUT_VALUE="ls"
|
||||
R=$(bridge '{"op":"pre-tool","admission_id":"'"$AID"'","tool":"'"$tool"'","tool_input":"'"$INPUT_VALUE"'","project":"'"$PROJ"'"}')
|
||||
[[ "$(printf '%s' "$R" | field decision)" == "allow" ]] || OK3=0
|
||||
bridge '{"op":"post-tool","admission_id":"'"$AID"'","tool":"'"$tool"'","status":"success"}' >/dev/null
|
||||
done
|
||||
@@ -245,7 +248,7 @@ AID=$(printf '%s' "$B" | field admission_id)
|
||||
|| fail "no-bash begin did not degrade gracefully ($B)"
|
||||
R=$(bridge '{"op":"pre-tool","admission_id":"'"$AID"'","tool":"Bash","tool_input":"ls","project":"'"$PROJ"'"}')
|
||||
[[ "$(printf '%s' "$R" | field decision)" == "allow" ]] \
|
||||
&& pass "no-bash: side-effect tool allowed (admission gate still governs)" \
|
||||
&& pass "no-bash: classified read-only shell command remains available" \
|
||||
|| fail "no-bash pre-tool blocked the developer ($R)"
|
||||
F=$(bridge '{"op":"finalize","admission_id":"'"$AID"'","stop_reason":"completed"}')
|
||||
[[ "$(printf '%s' "$F" | field decision)" == "non_certified" ]] \
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||
S="$CASAN_HARNESS_ROOT/scripts/bash"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
PASS=0; FAIL=0
|
||||
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
echo "===== Assurance Kernel unit + cross-runtime conformance ====="
|
||||
if CASAN_STATE_ROOT="$WORK/unit-state" python3 "$SCRIPT_DIR/assurance-kernel-tests.py" >/dev/null; then
|
||||
pass "kernel policy and two-adapter conformance tests"
|
||||
else
|
||||
fail "kernel policy/conformance tests"
|
||||
fi
|
||||
|
||||
echo "===== Agentic bridge integration ====="
|
||||
if CASAN_STATE_ROOT="$WORK/bridge-state" python3 "$SCRIPT_DIR/assurance-upgrade-integration-tests.py" >/dev/null; then
|
||||
pass "agentic H2, risk floor, failure outcome, trust-root integration"
|
||||
else
|
||||
fail "agentic upgrade integration tests"
|
||||
fi
|
||||
|
||||
echo "===== Sandbox fail-closed selection ====="
|
||||
SBX_STATE="$WORK/sandbox-state"
|
||||
SBX_OUT="$(CASAN_STATE_ROOT="$SBX_STATE" CASAN_ENFORCEMENT_MODE=enforce \
|
||||
CASAN_SANDBOX_MODE=container CASAN_SANDBOX_TEST_FORCE_UNAVAILABLE=1 \
|
||||
bash "$S/sandbox-run.sh" --workspace "$WORK" -- sh -c 'printf should-not-run' 2>&1)"
|
||||
SBX_RC=$?
|
||||
if [[ "$SBX_RC" -eq 2 && "$SBX_OUT" == *"SANDBOX_ISOLATION_REQUIRED"* ]] \
|
||||
&& grep -q '"reason_code":"sandbox_isolation_backend_unavailable"' "$SBX_STATE/logs/sandbox/decisions.jsonl"; then
|
||||
pass "enforce mode denies unavailable isolation backend with structured evidence"
|
||||
else
|
||||
fail "strict sandbox unavailable behavior rc=$SBX_RC out=$SBX_OUT"
|
||||
fi
|
||||
|
||||
DEV_OUT="$(CASAN_STATE_ROOT="$SBX_STATE" CASAN_PROFILE=test CASAN_SANDBOX_MODE=container \
|
||||
CASAN_SANDBOX_TEST_FORCE_UNAVAILABLE=1 CASAN_SANDBOX_ALLOW_STATIC_FALLBACK=1 \
|
||||
bash "$S/sandbox-run.sh" --workspace "$WORK" -- sh -c 'printf dev-ok' 2>/dev/null)"
|
||||
[[ "$DEV_OUT" == "dev-ok" ]] && pass "explicit test-only static fallback remains available" || fail "explicit test fallback failed"
|
||||
|
||||
echo "===== H5 action-class risk floor ====="
|
||||
GOV_STATE="$WORK/governance-state"
|
||||
mkdir -p "$GOV_STATE" "$WORK/keys"
|
||||
printf 'deploy a harmless documentation-only change\n' > "$WORK/deploy.txt"
|
||||
GOV_OUT="$(CASAN_STATE_ROOT="$GOV_STATE" CASAN_AUDIT_KEY_DIR="$WORK/keys" CASAN_ACTOR=alice \
|
||||
bash "$S/governance-check.sh" "$WORK/deploy.txt" "$WORK/deploy.out" deploy 2>&1)"
|
||||
GOV_RC=$?
|
||||
TRACE_FILE="$(find "$GOV_STATE/logs/trace" -name 'governance-*.json' -print -quit 2>/dev/null)"
|
||||
if [[ "$GOV_RC" -eq 2 && -n "$TRACE_FILE" ]] \
|
||||
&& python3 - "$TRACE_FILE" <<'PY'
|
||||
import json, sys
|
||||
r=json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
raise SystemExit(0 if r.get("action_class") == "deployment" and r.get("effective_risk") == "high" and r.get("decision") == "denied" else 1)
|
||||
PY
|
||||
then
|
||||
pass "benign deploy text cannot lower deployment risk or bypass approval"
|
||||
else
|
||||
fail "deploy risk-floor regression rc=$GOV_RC out=$GOV_OUT"
|
||||
fi
|
||||
|
||||
MISSING_ACTOR_OUT="$(CASAN_STATE_ROOT="$WORK/missing-actor-state" CASAN_AUDIT_KEY_DIR="$WORK/missing-actor-keys" \
|
||||
CASAN_APPROVAL_DECISION=approve CASAN_APPROVER=bob \
|
||||
bash "$S/governance-check.sh" "$WORK/deploy.txt" "$WORK/missing-actor.out" deploy 2>&1)"
|
||||
MISSING_ACTOR_RC=$?
|
||||
if [[ "$MISSING_ACTOR_RC" -eq 2 && "$MISSING_ACTOR_OUT" == *"actor_identity_required"* ]]; then
|
||||
pass "approval cannot replace required actor identity for a high-impact action"
|
||||
else
|
||||
fail "missing actor was not denied rc=$MISSING_ACTOR_RC out=$MISSING_ACTOR_OUT"
|
||||
fi
|
||||
|
||||
echo "===== Native failed side-effect completion semantics ====="
|
||||
NATIVE_STATE="$WORK/native-failure-state"
|
||||
printf 'safe write request\n' > "$WORK/native-input.txt"
|
||||
|
||||
OBSERVE_STATE="$WORK/native-observe-state"
|
||||
OBSERVE_OUT="$(CASAN_STATE_ROOT="$OBSERVE_STATE" CASAN_GOVERNANCE_ROOT="$WORK/observe-governance" \
|
||||
CASAN_AUDIT_KEY_DIR="$WORK/observe-keys" CASAN_ENFORCEMENT_MODE=observe CASAN_ACTOR=alice \
|
||||
bash "$S/casan-harness.sh" "$WORK/native-input.txt" "$WORK/native-observe-output.txt" agent_step 2>&1)"
|
||||
OBSERVE_RC=$?
|
||||
if [[ "$OBSERVE_RC" -eq 0 && "$OBSERVE_OUT" == *"assurance=degraded certification=non_certified"* ]] \
|
||||
&& grep -q '"assurance_result":"degraded"' "$OBSERVE_STATE/logs/kernel/"*.json; then
|
||||
pass "native observe-only success is explicitly degraded and non-certified"
|
||||
else
|
||||
fail "native observe-only outcome was overstated rc=$OBSERVE_RC out=$OBSERVE_OUT"
|
||||
fi
|
||||
|
||||
NATIVE_OUT="$(CASAN_STATE_ROOT="$NATIVE_STATE" CASAN_AUDIT_KEY_DIR="$WORK/native-keys" \
|
||||
CASAN_ENFORCEMENT_MODE=observe CASAN_AGENT=boss CASAN_ACTOR=boss \
|
||||
bash "$S/casan-harness.sh" "$WORK/native-input.txt" "$WORK/native-output.txt" write_file -- \
|
||||
bash -c 'exit 7' 2>&1)"
|
||||
NATIVE_RC=$?
|
||||
if [[ "$NATIVE_RC" -eq 7 && "$NATIVE_OUT" != *"CASAN_HARNESS_COMPLETE"* ]] \
|
||||
&& grep -q '"status":"failed"' "$NATIVE_STATE/logs/cost/metrics.jsonl" \
|
||||
&& grep -q '"execution_result":"failed"' "$NATIVE_STATE/logs/kernel/"*.json; then
|
||||
pass "failed native side effect records telemetry but cannot emit successful completion"
|
||||
else
|
||||
fail "native failed-outcome semantics rc=$NATIVE_RC out=$NATIVE_OUT"
|
||||
fi
|
||||
|
||||
NO_OUTPUT_STATE="$WORK/native-no-output-state"
|
||||
NO_OUTPUT_OUT="$(CASAN_STATE_ROOT="$NO_OUTPUT_STATE" CASAN_GOVERNANCE_ROOT="$WORK/no-output-governance" \
|
||||
CASAN_AUDIT_KEY_DIR="$WORK/no-output-keys" CASAN_ENFORCEMENT_MODE=observe CASAN_ACTOR=alice \
|
||||
bash "$S/casan-harness.sh" "$WORK/native-input.txt" "$WORK/native-no-output.txt" agent_step -- \
|
||||
bash -c 'true' 2>&1)"
|
||||
NO_OUTPUT_RC=$?
|
||||
if [[ "$NO_OUTPUT_RC" -eq 1 && "$NO_OUTPUT_OUT" != *"CASAN_HARNESS_COMPLETE"* ]] \
|
||||
&& grep -q '"status":"failed"' "$NO_OUTPUT_STATE/logs/cost/metrics.jsonl" \
|
||||
&& grep -q '"execution_result":"failed"' "$NO_OUTPUT_STATE/logs/kernel/"*.json; then
|
||||
pass "zero-exit command missing its required output remains a failed execution"
|
||||
else
|
||||
fail "missing required output was presented as success rc=$NO_OUTPUT_RC out=$NO_OUTPUT_OUT"
|
||||
fi
|
||||
|
||||
echo "===== Native H2 missing dependency ====="
|
||||
NATIVE_H2_STATE="$WORK/native-h2-state"
|
||||
NATIVE_H2_OUT="$(CASAN_STATE_ROOT="$NATIVE_H2_STATE" CASAN_GOVERNANCE_ROOT="$WORK/native-h2-governance" \
|
||||
CASAN_AUDIT_KEY_DIR="$WORK/native-h2-keys" CASAN_ENFORCEMENT_MODE=enforce CASAN_PROFILE=test \
|
||||
CASAN_ACTOR=alice CASAN_APPROVAL_DECISION=approve CASAN_APPROVER=bob \
|
||||
CASAN_H2_GATE_PATH="$WORK/missing-native-h2.sh" \
|
||||
bash "$S/casan-harness.sh" "$WORK/native-input.txt" "$WORK/native-h2-output.txt" write_file -- \
|
||||
bash -c 'printf should-not-run' 2>&1)"
|
||||
NATIVE_H2_RC=$?
|
||||
if [[ "$NATIVE_H2_RC" -eq 2 ]] \
|
||||
&& grep -q '"reason_code":"h2_gate_unavailable"' "$NATIVE_H2_STATE/logs/policy/h2-decisions.jsonl" \
|
||||
&& grep -q '"execution_result":"failed"' "$NATIVE_H2_STATE/logs/kernel/"*.json; then
|
||||
pass "native enforce mode denies a missing H2 gate and emits failed canonical evidence"
|
||||
else
|
||||
fail "native missing H2 gate did not fail closed rc=$NATIVE_H2_RC out=$NATIVE_H2_OUT"
|
||||
fi
|
||||
|
||||
echo "===== Production trust-root refusal ====="
|
||||
printf 'read status\n' > "$WORK/read.txt"
|
||||
TRUST_OUT="$(CASAN_STATE_ROOT="$WORK/trust-state" CASAN_AUDIT_KEY_DIR="$WORK/trust-keys" \
|
||||
CASAN_PROFILE=production bash "$S/governance-check.sh" "$WORK/read.txt" "$WORK/read.out" agent_step 2>&1)"
|
||||
TRUST_RC=$?
|
||||
if [[ "$TRUST_RC" -eq 2 && "$TRUST_OUT" == *"production_trust_root_unavailable"* ]]; then
|
||||
pass "production refuses local signing and local hash-chain fallback"
|
||||
else
|
||||
fail "production trust-root refusal rc=$TRUST_RC out=$TRUST_OUT"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "===== ASSURANCE UPGRADE SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
@@ -6,6 +6,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||
SHIP="$CASAN_HARNESS_ROOT/scripts/bash/audit-ship-s3.sh"
|
||||
PREFLIGHT="$CASAN_HARNESS_ROOT/scripts/bash/production-preflight.sh"
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
PASS=0; FAIL=0
|
||||
pass() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
||||
@@ -52,5 +53,109 @@ printf 'not-a-digest\n' > "$WORK/bad-head.txt"
|
||||
[[ "$(rc env PATH="$WORK/bin:$PATH" CASAN_PROFILE=prod CASAN_S3_BUCKET=casan-production-audit CASAN_S3_REGION=ap-northeast-1 CASAN_S3_KMS_KEY_ID=alias/casan-audit bash "$SHIP" "$WORK/bad-head.txt")" -ne 0 ]] \
|
||||
&& pass "malformed audit head is refused" || fail "malformed audit head accepted"
|
||||
|
||||
echo "===== Production configuration boundary ====="
|
||||
mkdir -p "$WORK/tls" "$WORK/state/logs/audit" "$WORK/output"
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 60 \
|
||||
-subj '/CN=control.casan.internal' -addext 'subjectAltName=DNS:control.casan.internal' \
|
||||
-keyout "$WORK/tls/tls.key" -out "$WORK/tls/tls.crt" >/dev/null 2>&1
|
||||
openssl genrsa -out "$WORK/idp-private.pem" 2048 >/dev/null 2>&1
|
||||
openssl rsa -in "$WORK/idp-private.pem" -pubout -out "$WORK/idp-public.pem" >/dev/null 2>&1
|
||||
printf 'test-ca\n' > "$WORK/vault-ca.pem"
|
||||
printf '%064d\n' 0 > "$WORK/state/logs/audit/audit-head.txt"
|
||||
|
||||
cat > "$WORK/oauth.env" <<'EOF'
|
||||
OAUTH2_PROXY_PROVIDER=oidc
|
||||
OAUTH2_PROXY_OIDC_ISSUER_URL=https://id.casan.internal/realms/casan
|
||||
OAUTH2_PROXY_CLIENT_ID=casan-control-plane
|
||||
OAUTH2_PROXY_CLIENT_SECRET=secret-manager-injected
|
||||
OAUTH2_PROXY_COOKIE_SECRET=base64-cookie-secret-value
|
||||
OAUTH2_PROXY_COOKIE_SECURE=true
|
||||
OAUTH2_PROXY_REDIRECT_URL=https://control.casan.internal/oauth2/callback
|
||||
OAUTH2_PROXY_OIDC_GROUPS_CLAIM=groups
|
||||
OAUTH2_PROXY_SET_XAUTHREQUEST=true
|
||||
OAUTH2_PROXY_PASS_ACCESS_TOKEN=true
|
||||
OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER=true
|
||||
EOF
|
||||
cat > "$WORK/vault.env" <<EOF
|
||||
VAULT_ADDR=https://vault.casan.internal
|
||||
VAULT_TOKEN=short-lived-workload-token
|
||||
VAULT_CACERT=$WORK/vault-ca.pem
|
||||
EOF
|
||||
|
||||
write_runtime() {
|
||||
local audience="$1" signing="$2" anchor="$3"
|
||||
cat > "$WORK/runtime.env" <<EOF
|
||||
CASAN_PROFILE=prod
|
||||
CASAN_CP_AUTH_MODE=jwt
|
||||
CASAN_CP_JWT_ISSUER=https://id.casan.internal/realms/casan
|
||||
CASAN_CP_JWT_AUDIENCE=$audience
|
||||
CASAN_CP_JWT_PUBLIC_KEY_FILE=/run/casan-idp/idp-public.pem
|
||||
CASAN_CP_JWT_CLOCK_SKEW_SECONDS=60
|
||||
CASAN_SIGNING_PROVIDER=$signing
|
||||
CASAN_IMMUTABLE_ANCHOR_PROVIDER=$anchor
|
||||
EOF
|
||||
}
|
||||
write_prod_env() {
|
||||
local public_key="$1"
|
||||
cat > "$WORK/casan-prod.env" <<EOF
|
||||
CASAN_PUBLIC_FQDN=control.casan.internal
|
||||
CASAN_CP_HTTPS_PORT=443
|
||||
CASAN_CP_TLS_DIR=$WORK/tls
|
||||
CASAN_CP_OAUTH_ENV=$WORK/oauth.env
|
||||
CASAN_CP_RUNTIME_ENV=$WORK/runtime.env
|
||||
CASAN_CP_VAULT_ENV=$WORK/vault.env
|
||||
CASAN_CP_STATE_DIR=$WORK/state
|
||||
CASAN_CP_OUTPUT_DIR=$WORK/output
|
||||
CASAN_CP_IDP_PUBLIC_KEY=$public_key
|
||||
CASAN_CP_API_IMAGE=registry.casan.internal/api@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
CASAN_CP_UI_IMAGE=registry.casan.internal/ui@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
CASAN_CP_OAUTH2_PROXY_IMAGE=registry.casan.internal/oauth@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
|
||||
CASAN_S3_BUCKET=casan-production-audit
|
||||
CASAN_S3_PREFIX=audit-anchors
|
||||
CASAN_S3_REGION=ap-northeast-1
|
||||
CASAN_S3_RETENTION_DAYS=365
|
||||
CASAN_S3_KMS_KEY_ID=alias/casan-audit
|
||||
EOF
|
||||
}
|
||||
|
||||
cat > "$WORK/bin/curl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' '{"data":{"ttl":300,"policies":["casan-audit-sign"]}}'
|
||||
EOF
|
||||
cat > "$WORK/bin/docker" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
EOF
|
||||
cat > "$WORK/bin/aws" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "$1 $2" == "s3api get-object-lock-configuration" ]]; then
|
||||
printf '%s\n' '{"ObjectLockConfiguration":{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":365}}}}'
|
||||
elif [[ "$1 $2" == "s3api head-object" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$WORK/bin/curl" "$WORK/bin/docker" "$WORK/bin/aws"
|
||||
|
||||
write_runtime casan-control-plane vault_kms s3_object_lock
|
||||
write_prod_env "$WORK/idp-public.pem"
|
||||
[[ "$(rc env PATH="$WORK/bin:$PATH" bash "$PREFLIGHT" "$WORK/casan-prod.env")" -eq 0 ]] \
|
||||
&& pass "complete JWT/Vault/Object-Lock production contract passes with local service stubs" \
|
||||
|| fail "valid production configuration rejected"
|
||||
|
||||
write_runtime wrong-audience vault_kms s3_object_lock
|
||||
[[ "$(rc env PATH="$WORK/bin:$PATH" bash "$PREFLIGHT" "$WORK/casan-prod.env")" -ne 0 ]] \
|
||||
&& pass "OIDC audience mismatch is refused" || fail "OIDC audience mismatch accepted"
|
||||
|
||||
write_runtime casan-control-plane local_openssl local_hash_chain
|
||||
[[ "$(rc env PATH="$WORK/bin:$PATH" bash "$PREFLIGHT" "$WORK/casan-prod.env")" -ne 0 ]] \
|
||||
&& pass "production local signing and local ledger configuration is refused" \
|
||||
|| fail "production local trust fallback accepted"
|
||||
|
||||
write_runtime casan-control-plane vault_kms s3_object_lock
|
||||
write_prod_env "$WORK/missing-idp-public.pem"
|
||||
[[ "$(rc env PATH="$WORK/bin:$PATH" bash "$PREFLIGHT" "$WORK/casan-prod.env")" -ne 0 ]] \
|
||||
&& pass "missing IdP verification key is refused" || fail "missing IdP key accepted"
|
||||
|
||||
echo "===== PRODUCTION HANDOFF SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
|
||||
@@ -117,7 +117,7 @@ set -e
|
||||
|
||||
# H5: high-risk action approved with explicit approver
|
||||
APPROVED_OUT="$EVIDENCE_DIR/04-high-risk-approved-output.txt"
|
||||
CASAN_APPROVAL_DECISION=approve CASAN_APPROVER=architect@example.local \
|
||||
CASAN_ACTOR=developer CASAN_APPROVAL_DECISION=approve CASAN_APPROVER=architect@example.local \
|
||||
"$SCRIPTS/governance-check.sh" "$RISK_IN" "$APPROVED_OUT" deploy > "$EVIDENCE_DIR/04-governance-approve.stdout"
|
||||
assert_contains "$APPROVED_OUT" "Deploy"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user