#!/usr/bin/env bash
# v0.39 — CI guard against bypassing the localOnly filter on the HTTP MCP
# surface. Lives alongside check-jsonb-pattern.sh / check-progress-to-stdout.sh
# in the bun run verify chain.
#
# Background: serve-http.ts builds the HTTP MCP tools/list response from
# `operations.filter(op => !op.localOnly)`. That filter is the only thing
# keeping localOnly ops (sync_brain, file_upload, file_list, file_url —
# any admin op the user EXPLICITLY marked as CLI-only) off the wire.
#
# If a future HTTP-exposing module imports `operations` from
# core/operations.ts WITHOUT applying the filter, the localOnly contract
# silently breaks: a write-scoped OAuth client could submit `sync_brain`
# or `file_upload` over HTTP. Codex outside-voice review of the e2e-test-
# wave (CMT-3) flagged this exact bypass class.
#
# The guard works by:
#   1. Listing every file that imports `operations` from core/operations.ts.
#   2. Comparing against an explicit ALLOWLIST of known-safe importers
#      (each with a rationale below).
#   3. Failing if a new file is missing from the allowlist — forces the
#      author to either (a) join the allowlist with an explicit rationale,
#      or (b) apply the canonical filter at import.
#
# This is a structural defense. The runtime defense lives in
# test/operations-trust-boundary.test.ts (the canonical filter contract +
# handler-invocation cases for the historically-broken classes).
#
# To allow a new importer: add the relative path to ALLOWED below with a
# one-line comment explaining why localOnly isn't a concern there.
#
# Exit: 0 when no unknown importers, 1 when at least one is missing from
# the allowlist.
set -euo pipefail

ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"

# Files allowed to import `operations` directly. Each entry must be
# accompanied by a one-line rationale (the comment on the same line).
ALLOWED=(
  "src/cli.ts"                                  # local CLI; user owns the machine, no trust boundary
  "src/mcp/dispatch.ts"                         # shared dispatch; sets ctx.remote from caller, handlers self-gate
  "src/mcp/server.ts"                           # stdio MCP; local-trusted (binary on user's box)
  "src/mcp/http-transport.ts"                   # superseded by serve-http.ts; kept for back-compat tests
  "src/mcp/tool-defs.ts"                        # pure helper; takes ops as parameter, never exposes them
  "src/core/minions/tools/brain-allowlist.ts"   # subagent registry; has its own opt-in allowlist (separate from localOnly)
  "src/commands/capture.ts"                     # local CLI tool; not network-exposed
  "src/commands/enrich.ts"                       # local CLI tool; calls put_page handler with remote=false, not network-exposed
  "src/commands/book-mirror.ts"                 # local CLI tool; not network-exposed
  "src/commands/tools-json.ts"                  # gbrain --tools-json introspection; full op list IS the purpose
  "src/commands/serve-http.ts"                  # MUST APPLY .filter(op => !op.localOnly) — verified by grep below
)

# Pattern: any import that brings the `operations` VALUE in from core/operations.ts.
# Three shapes the value can enter through; each must be caught:
#   - destructured:    import { operations } from '...core/operations.ts'
#   - aliased:         import { operations as ops } from '...core/operations.ts'
#   - namespace:       import * as opsModule from '...core/operations.ts'
# The original narrow regex only matched the destructured form — codex caught
# the bypass class during /ship adversarial review (aliased + namespace forms
# slipped through). The broadened regex below specifically requires `operations`
# inside the destructured clause OR a namespace import (`* as X`); type-only
# imports of sibling exports like `sourceScopeOpts` / `OperationContext` are
# left alone (those don't expose the op list to a transport surface).
PATTERN='import[[:space:]]+(\*[[:space:]]+as[[:space:]]+[a-zA-Z_$][a-zA-Z0-9_$]*|\{[^}]*\boperations\b[^}]*\})[[:space:]]+from[[:space:]]*['\''"][^'\''"]*core/operations\.ts['\''"]'

# Collect files that import `operations`. Use a while-loop over grep output
# instead of `mapfile` to stay compatible with macOS's default bash 3.2.
FOUND_FILES=""
while IFS= read -r f; do
  [ -n "$f" ] && FOUND_FILES="$FOUND_FILES$f"$'\n'
done < <(grep -rlE --include='*.ts' "$PATTERN" src 2>/dev/null | sort -u || true)

FAIL=0

# Check 1: every found file is in ALLOWED.
while IFS= read -r file; do
  [ -z "$file" ] && continue
  rel="${file#"$ROOT/"}"
  ok=0
  for allowed in "${ALLOWED[@]}"; do
    if [ "$rel" = "$allowed" ]; then
      ok=1
      break
    fi
  done
  if [ "$ok" -eq 0 ]; then
    echo "FAIL: $rel imports operations but is not in scripts/check-operations-filter-bypass.sh ALLOWED list."
    echo "      Either apply .filter(op => !op.localOnly) at the import boundary,"
    echo "      or add this file to ALLOWED with a one-line rationale."
    FAIL=1
  fi
done <<< "$FOUND_FILES"

# Check 2: serve-http.ts MUST contain the canonical filter expression near
# its operations import. Without the filter, the entire HTTP MCP surface
# leaks localOnly ops.
SERVE_HTTP="src/commands/serve-http.ts"
if [ -f "$SERVE_HTTP" ]; then
  if ! grep -qE 'operations\.filter\(\s*op\s*=>\s*!op\.localOnly\s*\)' "$SERVE_HTTP"; then
    echo "FAIL: $SERVE_HTTP no longer contains the canonical"
    echo "      operations.filter(op => !op.localOnly) expression. The HTTP MCP"
    echo "      surface depends on this filter to enforce localOnly. Restore"
    echo "      the filter or refactor the trust boundary explicitly."
    FAIL=1
  fi
fi

if [ "$FAIL" -eq 1 ]; then
  echo ""
  echo "Hint: see test/operations-trust-boundary.test.ts for the runtime contract."
  exit 1
fi

exit 0
