#!/usr/bin/env bash
# Dredd MCP — Pre-Flight Security hook for Claude Code
#
# Drop-in PreToolUse hook. Calls Dredd's signed-verdict endpoint before any
# MCP tool invocation; blocks if the verdict is BLOCK, advisories pass through
# with a warning, ALLOW is silent.
#
# Install:   curl -fsSL https://analytics.dugganusa.com/install/dredd-mcp.sh | bash
# Manual:    save this file, chmod +x, point Claude Code's hooks config at it.
#
# Bypass:    set DREDD_BYPASS=1 (logged to ~/.dredd-mcp/audit.log)
# Fail-open: if our endpoint is unreachable, the hook ALLOWS by default and
#            logs the failure. Set DREDD_FAIL_CLOSED=1 to override.
#
# License:   MIT — github.com/pduggusa/dredd-mcp
# Repo:      https://github.com/pduggusa/dredd-mcp

set -u

DREDD_ENDPOINT="${DREDD_ENDPOINT:-https://analytics.dugganusa.com/api/v1/dredd/preflight}"
DREDD_HOME="${DREDD_HOME:-$HOME/.dredd-mcp}"
DREDD_CACHE="$DREDD_HOME/cache"
DREDD_AUDIT="$DREDD_HOME/audit.log"
CACHE_TTL_SECONDS="${DREDD_CACHE_TTL:-300}"  # match server-side TTL
TIMEOUT_SECONDS="${DREDD_TIMEOUT:-3}"

mkdir -p "$DREDD_CACHE" 2>/dev/null || true
touch "$DREDD_AUDIT" 2>/dev/null || true

log_audit() {
  local kind="$1"; shift
  printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$kind" "$*" >> "$DREDD_AUDIT" 2>/dev/null || true
}

# ---- INPUT ----
# Claude Code passes the pending tool-use envelope on stdin as JSON. We extract
# server name + tool name. If we can't parse it, fail-open with a warning.
PAYLOAD="$(cat 2>/dev/null || true)"

# Bypass short-circuit
if [ -n "${DREDD_BYPASS:-}" ]; then
  log_audit BYPASS "DREDD_BYPASS=$DREDD_BYPASS payload_size=${#PAYLOAD}"
  exit 0
fi

# Extract MCP-shaped server + tool. Claude Code's hook envelope varies by version;
# we handle both top-level and tool_input nesting. jq is preferred when present.
extract_field() {
  local key="$1"
  if command -v jq >/dev/null 2>&1; then
    printf '%s' "$PAYLOAD" | jq -r --arg k "$key" 'if has($k) then .[$k] else (.tool_use? // .tool_input? // {} | .[$k] // empty) end' 2>/dev/null
  else
    # Crude grep fallback — string-shaped values only
    printf '%s' "$PAYLOAD" | grep -oE "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/"
  fi
}

SERVER="$(extract_field server_name)"
[ -z "$SERVER" ] && SERVER="$(extract_field server)"
[ -z "$SERVER" ] && SERVER="$(extract_field mcp_server)"
TOOL_NAME="$(extract_field tool_name)"
[ -z "$TOOL_NAME" ] && TOOL_NAME="$(extract_field name)"
SERVER_VERSION="$(extract_field server_version)"

# If this isn't an MCP invocation (no server name), allow silently.
if [ -z "$SERVER" ]; then
  exit 0
fi

# Only check MCP-shaped server names — skip non-MCP tools.
case "$SERVER" in
  io.*|io_*|*/*-mcp|mcp-*|*mcp*) : ;;
  *) exit 0 ;;
esac

# ---- CACHE ----
# Cache key: server + version + tool. Fresh-within-TTL cached verdicts skip the network call.
CACHE_KEY="$(printf '%s|%s|%s' "$SERVER" "$SERVER_VERSION" "$TOOL_NAME" | shasum -a 256 2>/dev/null | awk '{print $1}')"
CACHE_FILE="$DREDD_CACHE/$CACHE_KEY"
if [ -f "$CACHE_FILE" ]; then
  CACHE_AGE=$(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0)))
  if [ "$CACHE_AGE" -lt "$CACHE_TTL_SECONDS" ]; then
    CACHED_VERDICT="$(cat "$CACHE_FILE" 2>/dev/null)"
    case "$CACHED_VERDICT" in
      ALLOW) exit 0 ;;
      ADVISORY)
        printf 'Dredd: ADVISORY for %s (cached) — proceeding\n' "$SERVER" >&2
        exit 0 ;;
      BLOCK)
        printf 'Dredd: BLOCK for %s (cached) — invocation denied\n' "$SERVER" >&2
        log_audit BLOCK_CACHED "$SERVER tool=$TOOL_NAME"
        exit 1 ;;
    esac
  fi
fi

# ---- NETWORK CALL ----
URL="$DREDD_ENDPOINT?server=$(printf %s "$SERVER" | sed 's/ /%20/g')"
[ -n "$SERVER_VERSION" ] && URL="$URL&version=$SERVER_VERSION"
[ -n "$TOOL_NAME" ] && URL="$URL&tool=$TOOL_NAME"

RESPONSE="$(curl -fsS --max-time "$TIMEOUT_SECONDS" "$URL" 2>&1)"
CURL_RC=$?

if [ "$CURL_RC" -ne 0 ] || [ -z "$RESPONSE" ]; then
  log_audit NETWORK_FAIL "rc=$CURL_RC server=$SERVER"
  if [ -n "${DREDD_FAIL_CLOSED:-}" ]; then
    printf 'Dredd: backend unreachable, FAIL_CLOSED set — invocation denied\n' >&2
    exit 1
  fi
  printf 'Dredd: backend unreachable — fail-open, allowing\n' >&2
  exit 0
fi

# ---- PARSE VERDICT ----
VERDICT="$(printf '%s' "$RESPONSE" | grep -oE '"verdict"[[:space:]]*:[[:space:]]*"[A-Z]+"' | head -1 | sed -E 's/.*"([A-Z]+)".*/\1/')"
SEVERITY="$(printf '%s' "$RESPONSE" | grep -oE '"severity"[[:space:]]*:[[:space:]]*"[a-z]+"' | head -1 | sed -E 's/.*"([a-z]+)".*/\1/')"

# Cache it
printf '%s' "$VERDICT" > "$CACHE_FILE" 2>/dev/null || true

case "$VERDICT" in
  ALLOW)
    exit 0
    ;;
  ADVISORY)
    printf 'Dredd: ADVISORY (%s) for %s — proceeding\n' "${SEVERITY:-?}" "$SERVER" >&2
    log_audit ADVISORY "$SERVER tool=$TOOL_NAME severity=${SEVERITY:-?}"
    exit 0
    ;;
  BLOCK)
    printf 'Dredd: BLOCK (%s) for %s — invocation denied\n' "${SEVERITY:-?}" "$SERVER" >&2
    printf 'Override: DREDD_BYPASS=<reason> %s ...\n' "$0" >&2
    log_audit BLOCK "$SERVER tool=$TOOL_NAME severity=${SEVERITY:-?}"
    exit 1
    ;;
  *)
    log_audit UNPARSED "verdict=$VERDICT response_size=${#RESPONSE}"
    if [ -n "${DREDD_FAIL_CLOSED:-}" ]; then
      printf 'Dredd: unparseable verdict, FAIL_CLOSED set — invocation denied\n' >&2
      exit 1
    fi
    exit 0
    ;;
esac
