feat(casan): establish assurance kernel and harden control plane
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user