Files
CASAN/AINative_OKR_CASAN5/.specify/scripts/bash/casan-harness.sh
T
thanhnvandClaude Opus 4.8 ac40c0b281 feat(h4-hardening): Plan-07 Track A — A1 strict semantic, A2 unicode/encoding, A3 tool-output scan
A1 (V1): CASAN_SECURITY_STRICT=1 makes semantic classification REQUIRED and
  fail-closed — model unavailable/no-verdict → BLOCK, never a silent SKIP.
  Non-strict CASAN_SEMANTIC_CLASSIFY=1 keeps regex verdict but logs
  SEMANTIC_SKIPPED loudly (sourced casan-log.sh). Default (no flags) unchanged.
A2 (V3/V4): unicode-normalize.py (NFKC + zero-width strip + Cyrillic/Greek
  homoglyph fold) and decode-suspicious.py (base64/hex decode + rescan, printable
  filter to avoid false positives) feed new match_either/secret_match haystacks.
  Blocks homoglyph, zero-width, fullwidth, base64/hex-smuggled injection & secrets.
A3 (V7): tool-output-scan.sh scans tool output for injection/secret before it
  re-enters model context; wrapper runs it after H6-exec (mode off|warn|block,
  strict→block). warn is default to preserve benign-draft behaviour.

Baseline preserved: run-casan4 35/35, adversarial 44/44.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:20:50 +09:00

162 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# CASAN Level 5 unified harness wrapper.
# Usage:
# casan-harness.sh <input-file> <output-file> [action-name] [-- <command> ...]
#
# Flow:
# H4 input security -> H5 governance -> H6 metrics around execution/cache -> H4 output filter
INPUT_FILE="${1:-}"
FINAL_OUTPUT="${2:-}"
ACTION_NAME="${3:-agent_step}"
shift 3 || true
if [[ "${1:-}" == "--" ]]; then
shift
fi
if [[ -z "$INPUT_FILE" || -z "$FINAL_OUTPUT" ]]; then
echo "Usage: casan-harness.sh <input-file> <output-file> [action-name] [-- <command> ...]" >&2
exit 64
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
TMP_DIR="$PROJECT_ROOT/.specify/logs/tmp"
CACHE_DIR="$PROJECT_ROOT/.specify/logs/idempotency"
mkdir -p "$TMP_DIR" "$CACHE_DIR" "$(dirname "$FINAL_OUTPUT")"
# Shared log taxonomy (error<warn<info<debug<trace via CASAN_LOG_LEVEL).
# debug shows each harness phase with its rc; stderr only, stdout untouched.
# shellcheck source=casan-log.sh
source "$SCRIPT_DIR/casan-log.sh"
# Optional machine-readable phase report (one JSON object per invocation).
# The Boss orchestrator sets CASAN_PHASE_REPORT per step so it can log per-
# harness rc at debug level and persist them into pipeline-run.jsonl.
PHASE_REPORT="${CASAN_PHASE_REPORT:-}"
PHASE_LOG=""
CACHE_STATUS="none"
record_phase() { # <phase-name> <rc>
casan_log debug harness "action=$ACTION_NAME phase=$1 rc=$2"
PHASE_LOG="${PHASE_LOG:+$PHASE_LOG,}{\"phase\":\"$1\",\"rc\":$2}"
}
write_phase_report() {
[[ -n "$PHASE_REPORT" ]] || return 0
printf '{"action":"%s","cache":"%s","phases":[%s]}\n' \
"$ACTION_NAME" "$CACHE_STATUS" "$PHASE_LOG" > "$PHASE_REPORT" 2>/dev/null || true
}
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
write_phase_report
exit "$rc"
fi
}
hash_text() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum | awk '{print $1}'
else
shasum -a 256 | awk '{print $1}'
fi
}
CMD_STR="${*:-no_cmd}"
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)"
CACHE_META="$CACHE_DIR/$IDEMPOTENCY_KEY.json"
CACHE_OUT="$CACHE_DIR/$IDEMPOTENCY_KEY.output"
TRACE_SUFFIX="$(date +%s)-$$"
SAFE_INPUT="$TMP_DIR/security-input-$TRACE_SUFFIX.txt"
APPROVED_INPUT="$TMP_DIR/governance-approved-$TRACE_SUFFIX.txt"
RAW_OUTPUT="$TMP_DIR/raw-output-$TRACE_SUFFIX.txt"
casan_log debug harness "action=$ACTION_NAME input=$INPUT_FILE output=$FINAL_OUTPUT key=${IDEMPOTENCY_KEY:0:12}…"
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
# 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.
export CASAN_STEP_NAME="${CASAN_STEP_NAME:-$ACTION_NAME}"
# WP-S5: wrap command execution under a hard wall-clock timeout (tool-exec.sh).
# Prevents runaway or hung tool calls from blocking the pipeline indefinitely.
TOOL_TIMEOUT="${CASAN_TOOL_TIMEOUT_SECONDS:-30}"
if [[ -f "$CACHE_META" && -f "$CACHE_OUT" ]]; then
CACHE_STATUS="cached"
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" -- "$@"
else
CACHE_STATUS="stored"
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT"
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.
# warn keeps existing behaviour (logged, non-blocking) so benign drafts are not
# broken; block enforces (fail-closed) for production/strict runs.
TOOL_OUTPUT_SCAN_MODE="${CASAN_TOOL_OUTPUT_SCAN:-}"
if [[ -z "$TOOL_OUTPUT_SCAN_MODE" ]]; then
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" ]]; then TOOL_OUTPUT_SCAN_MODE="block"; else TOOL_OUTPUT_SCAN_MODE="warn"; fi
fi
if [[ "$TOOL_OUTPUT_SCAN_MODE" != "off" ]]; then
TOS_RC=0
"$SCRIPT_DIR/tool-output-scan.sh" "$RAW_OUTPUT" "$ACTION_NAME" >/dev/null 2>&1 || TOS_RC=$?
record_phase "H4-tool-output" "$TOS_RC"
if [[ "$TOS_RC" -eq 2 ]]; then
if [[ "$TOOL_OUTPUT_SCAN_MODE" == "block" ]]; then
casan_log error harness "TOOL_OUTPUT_INJECTION_BLOCKED action=$ACTION_NAME"
: > "$FINAL_OUTPUT"
write_phase_report
echo "TOOL_OUTPUT_INJECTION_BLOCKED action=$ACTION_NAME" >&2
exit 2
else
casan_log warn harness "TOOL_OUTPUT_INJECTION_SUSPECTED action=$ACTION_NAME mode=warn hint=set_CASAN_TOOL_OUTPUT_SCAN=block_or_CASAN_SECURITY_STRICT=1_to_enforce"
fi
fi
fi
run_phase "H4-out" "$SCRIPT_DIR/security-check.sh" "$RAW_OUTPUT" "$FINAL_OUTPUT" output
if [[ "$CACHE_STATUS" == "stored" ]]; then
cat <<EOF > "$CACHE_META"
{
"idempotency_key": "$IDEMPOTENCY_KEY",
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"action": "$ACTION_NAME",
"command": "$(printf '%s' "$CMD_STR" | sed 's/"/\\"/g')",
"output_hash": "$(cat "$FINAL_OUTPUT" | hash_text)"
}
EOF
cp "$FINAL_OUTPUT" "$CACHE_OUT"
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"