T1 (H7): casan-step.mjs now calls rollback-manager.sh checkpoint before overwriting plan.md at attempt-2, writes tx-id to plan.checkpoint.txid sidecar, and executes rollback on REJECTED verdict. rollback-transactions.jsonl records a real cp restore command. Adversarial test: checkpoint exists, real cp command recorded, plan hash matches pre-overwrite content. T4 (H6): casan-harness.sh exports CASAN_STEP_NAME=$ACTION_NAME before agent-metrics.sh so nested model calls (model-call.py) and the provider- cost-lookup.py query share the same step label. metrics.jsonl now writes cost_source=provider_telemetry instead of word_count_estimate when a real Ollama call is made within the same step. Adversarial test: verified with CASAN_STEP_NAME=t4-telemetry-test end-to-end. adversarial-harness-tests.sh: 40 → 44 PASS / 0 FAIL (+3 T1, +1 T4) security-gate.sh: PASS=10 FAIL=0 SKIP=0 (verified, local ornith:9b) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
3.5 KiB
Bash
Executable File
99 lines
3.5 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")"
|
|
|
|
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"
|
|
|
|
"$SCRIPT_DIR/security-check.sh" "$INPUT_FILE" "$SAFE_INPUT" input
|
|
"$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)
|
|
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
|
|
"$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- bash -c 'cp "$1" "$CASAN_OUTPUT"' _ "$CACHE_OUT"
|
|
CACHE_STATUS="cached"
|
|
elif [[ "$#" -gt 0 ]]; then
|
|
"$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
|
|
"$SCRIPT_DIR/tool-exec.sh" "$TOOL_TIMEOUT" -- "$@"
|
|
CACHE_STATUS="stored"
|
|
else
|
|
"$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT"
|
|
CACHE_STATUS="stored"
|
|
fi
|
|
|
|
"$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
|
|
|
|
echo "CASAN_HARNESS_COMPLETE cache=$CACHE_STATUS key=$IDEMPOTENCY_KEY output=$FINAL_OUTPUT"
|