#!/usr/bin/env bash
# web-access-gate.sh -- PreToolUse hook
# Blocks WebSearch/WebFetch/Playwright for agents without web access authorization.
# Exit code 2 = block the tool call. Exit code 0 = allow.
#
# Reads agent-web-permissions.md to determine access level per agent.
# Part of @connexum/ai-governance

set -euo pipefail

# Read hook input from stdin
INPUT=$(cat)

# AIAG-018: enforce 1MB input size limit
if [ "${#INPUT}" -gt 1048576 ]; then
  echo "BLOCKED: Hook input exceeds 1MB. Possible injection attempt." >&2
  exit 2
fi

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)

# F-NEW-CRIT-38: Claude Code's hook payload does NOT include .agent_name.
# Resolve subagent identity through a documented fallback chain so the
# operator can pin identity per-session (CXNI_AGENT_NAME) or set a
# session-wide default access level (CXNI_DEFAULT_WEB_ACCESS).
AGENT_NAME=$(echo "$INPUT" | jq -r '.agent_name // empty' 2>/dev/null)
if [ -z "$AGENT_NAME" ] && [ -n "${CXNI_AGENT_NAME:-}" ]; then
  AGENT_NAME="$CXNI_AGENT_NAME"
fi

# Only gate web-related tools
WEB_TOOLS="WebSearch WebFetch mcp__playwright"
IS_WEB_TOOL=false
for wt in $WEB_TOOLS; do
  case "$TOOL_NAME" in
    $wt*) IS_WEB_TOOL=true ;;
  esac
done

if [ "$IS_WEB_TOOL" = false ]; then
  exit 0
fi

# Find the permissions manifest
MANIFEST=""
for candidate in \
  ".claude/rules/agent-web-permissions.md" \
  ".claude/governance/web-permissions.md" \
  "GOVERNANCE/web-permissions.md"; do
  if [ -f "$candidate" ]; then
    MANIFEST="$candidate"
    break
  fi
done

# If no manifest exists, block all web access by default (deny-by-default)
if [ -z "$MANIFEST" ]; then
  echo "BLOCKED: No web permissions manifest found. All web access denied by default." >&2
  exit 2
fi

# F-NEW-CRIT-38: When subagent identity is unresolved, allow the operator
# to set CXNI_DEFAULT_WEB_ACCESS as the resolved access level for the
# whole session. If neither identity nor default is set, fail-secure
# (block) but emit an actionable advisory naming the env vars to set.
if [ -z "$AGENT_NAME" ]; then
  if [ -n "${CXNI_DEFAULT_WEB_ACCESS:-}" ]; then
    ACCESS_LEVEL="$CXNI_DEFAULT_WEB_ACCESS"
  else
    echo "BLOCKED: subagent identity unresolved (Claude Code does not pass .agent_name). Set CXNI_AGENT_NAME=<agent-id> per session, or set CXNI_DEFAULT_WEB_ACCESS=<NONE|SEARCH_ONLY|READ_ONLY|FULL> as the session default." >&2
    exit 2
  fi
fi

# Parse agent's web access level from manifest.
# Format expected: | agent-file.md | Name | Role | LEVEL | justification |
#
# AIAG-007: use grep -F (fixed-string) to prevent regex injection via
# attacker-controlled AGENT_NAME. `|| true` prevents `set -e`/pipefail
# from aborting the script when grep finds no match — we need the empty
# string to flow into the NONE default below.
#
# F-NEW-CRIT-38: only run manifest lookup when we have a real agent id;
# otherwise ACCESS_LEVEL was already set above from CXNI_DEFAULT_WEB_ACCESS.
if [ -n "$AGENT_NAME" ]; then
  ACCESS_LEVEL=$(grep -iF "$AGENT_NAME" "$MANIFEST" 2>/dev/null | grep -oP '(NONE|SEARCH_ONLY|READ_ONLY|FULL)' | head -1 || true)

  # Default to NONE if agent not found in manifest
  if [ -z "$ACCESS_LEVEL" ]; then
    ACCESS_LEVEL="NONE"
  fi
fi

# Enforce access levels
case "$ACCESS_LEVEL" in
  NONE)
    echo "BLOCKED: Agent '$AGENT_NAME' has NONE web access level. Tool '$TOOL_NAME' denied." >&2
    exit 2
    ;;
  SEARCH_ONLY)
    if [ "$TOOL_NAME" != "WebSearch" ]; then
      echo "BLOCKED: Agent '$AGENT_NAME' has SEARCH_ONLY access. Tool '$TOOL_NAME' denied (only WebSearch allowed)." >&2
      exit 2
    fi
    ;;
  READ_ONLY)
    case "$TOOL_NAME" in
      mcp__playwright*)
        echo "BLOCKED: Agent '$AGENT_NAME' has READ_ONLY access. Playwright tools denied." >&2
        exit 2
        ;;
    esac
    ;;
  FULL)
    # All web tools allowed
    ;;
  *)
    echo "BLOCKED: Unknown access level '$ACCESS_LEVEL' for agent '$AGENT_NAME'." >&2
    exit 2
    ;;
esac

exit 0
