feat(casan): establish assurance kernel and harden control plane

This commit is contained in:
thanhnv
2026-08-02 23:24:51 +07:00
parent 8b477f3800
commit 5745519126
51 changed files with 4076 additions and 180 deletions
@@ -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"))