KeiSeiKit-1.0/hooks/agent-stub-scan.sh
Parfii-bot 71f17337fe fix(security): cortex /term env_clear + bind guard, agent-stub-scan stdin, magiclink revoke
Three independent security hardenings from cross-cutting audits.

1. cortex /term PTY env leak + bind guard (HIGH — Sonnet Cross-cutting + Opus)
   - kei-cortex/src/handlers/term_pty.rs: PTY spawn was inheriting daemon's
     full process env (KEI_AUTH_KEY, ANTHROPIC_API_KEY, FAL_KEY, etc.) into
     every authenticated /term shell. Combined with default cors_origin =
     https://keisei.app, one stored XSS on keisei.app + one bearer token =
     full local shell with all daemon secrets.
     Added apply_safe_env() helper: env_clear() + re-set only HOME, PATH,
     USER, LANG, TERM. Spawn helper invokes it before spawn_command.
   - kei-cortex/src/main.rs: extracted build_config() helper; added
     enforce_loopback_or_local_cors() guard called before serve.bind. Refuses
     to start if bind addr is non-loopback AND cors_origin is a public
     domain — prevents the XSS-to-shell scenario in production.

2. agent-stub-scan.sh stdin parsing (HIGH — multiple audits)
   - hooks/agent-stub-scan.sh: previously read $CLAUDE_AGENT_TRANSCRIPT env
     var which Claude Code does NOT set on PostToolUse:Agent. Hook silently
     exited 0 — RULE 0.16 enforcement was dead-code in production.
     Rewrote to read stdin JSON via jq, flatten .tool_response recursively
     (string|array|object via the same pattern as agent-event-done.sh),
     guard on .tool_name == "Agent" and command -v jq. Maintained WARN-tier
     exit-0 with TODO marker for ENFORCE flip on 2026-05-05 (per RULE 0.16
     §2 ladder).

3. magiclink revoke() silent no-op (HIGH — Opus Rust + Sonnet Cross-cutting)
   - kei-auth-magiclink/src/{error,provider}.rs: revoke() previously returned
     Ok(()) without doing anything. Operators expecting "revoke a session"
     semantics from the AuthProvider trait got false success. Stolen magic-
     link URLs remained valid until the 15-minute TTL.
     Added Error::Unsupported variant. revoke() now returns
     Err(Unsupported(...)) with explicit guidance: "rotate KEI_MAGICLINK_HMAC_
     KEY to invalidate all live tokens, or maintain a deny-list at the caller
     layer". Test provider_revoke_returns_unsupported_error confirms the
     error variant is wired.

Tests: cargo check + cargo test both PASS. 444 functional tests across
kei-cortex (428 lib) + kei-auth-magiclink (16 lib + smoke). Pre-existing
openai_loop_wiring.rs 502 failures in routes/openai/{chat,responses}.rs are
NOT introduced by these fixes — separate unrelated triage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:38:23 +08:00

67 lines
2.8 KiB
Bash
Executable file

#!/usr/bin/env bash
# agent-stub-scan.sh — RULE 0.16 enforcement hook (PostToolUse:Agent).
# Scans agent response for STATUS-TRUTH MARKER and validates internal
# consistency. Severity per RULE 0.10 ladder; bypass via STATUS_TRUTH_BYPASS=1.
#
# Wave 47 fix: reads stdin JSON (Claude Code's actual hook payload format)
# instead of the never-set CLAUDE_AGENT_TRANSCRIPT env var. The previous
# env-var path silently exited 0 on every invocation, leaving RULE 0.16 as
# dead-code in production. Mirrors the flatten pattern from
# `agent-event-done.sh` so both hooks share one shape.
#
# TODO(2026-05-05): flip WARN -> ENFORCE per RULE 0.16 §2 ladder.
# Until then, every inconsistency exits 0 with stderr only.
set -u
log_block() {
printf '\n=== agent-stub-scan (RULE 0.16) ===\n%s\n===\n' "$1" >&2
}
if [ "${STATUS_TRUTH_BYPASS:-0}" = "1" ]; then
log_block "BYPASS active (STATUS_TRUTH_BYPASS=1) — skipping scan."
exit 0
fi
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat 2>/dev/null || true)
[ -n "$INPUT" ] || exit 0
TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
[ "$TOOL" = "Agent" ] || exit 0
# Flatten tool_response.content (string OR array OR object) to plain text.
# Same recursive shape as agent-event-done.sh so the two hooks parse the
# same payload identically.
RESPONSE=$(printf '%s' "$INPUT" | jq -r '
(.tool_response // "") as $r | def f:
if type=="string" then . elif type=="array" then map(f)|join("\n")
elif type=="object" then (if has("text") then .text elif has("content") then .content|f else tostring end)
else "" end; $r|f' 2>/dev/null || true)
[ -n "$RESPONSE" ] || exit 0
if ! printf '%s' "$RESPONSE" | grep -q '=== STATUS-TRUTH MARKER ==='; then
log_block "MISSING STATUS-TRUTH MARKER block in agent response.
RULE 0.16 §1 requires every code-implementer agent to emit the marker.
Add the block to the agent's final report; see ~/.claude/rules/shipped-vs-functional.md"
# WARN tier (until 2026-05-05): exit 0 with stderr. After: exit 1.
exit 0
fi
SHIPPED=$(printf '%s' "$RESPONSE" | grep -m1 '^shipped:' \
| sed 's/^shipped:[[:space:]]*//' | awk '{print $1}')
STUB_COUNT=$(printf '%s' "$RESPONSE" | grep -cE '\b(todo!\(\)|unimplemented!\(\)|placeholder|echo-stub|NOT-RUN|stub_|stub agent)\b' || true)
if [ "$SHIPPED" = "functional" ] && [ "${STUB_COUNT:-0}" -gt 0 ]; then
LOCS=$(printf '%s' "$RESPONSE" | grep -nE '\b(todo!\(\)|unimplemented!\(\)|placeholder|echo-stub|stub_)\b' | head -20)
log_block "INCONSISTENCY: shipped=functional but $STUB_COUNT stub-markers found.
First locations:
$LOCS
Either downgrade shipped to 'partial'/'scaffolding' or remove the stubs."
# WARN tier (until 2026-05-05): exit 0. After: exit 1.
exit 0
fi
log_block "OK: shipped=$SHIPPED, stubs=${STUB_COUNT:-0} (consistent)."
exit 0