#!/usr/bin/env python3 """Compute guard: Windy Mind is the ONLY door to AI compute (Grant, 2026-09-23). Flags code that talks to an AI provider directly instead of through Windy Mind: a provider API host, a provider SDK import or dependency, or a raw provider key name. Direct calls skip Mind's metering, caps and live-model routing, and they spend whichever key happens to be lying around (the audit found Grant's personal Max OAuth token inside a platform container). WARN-ONLY for now: the bridge posts `windy-git/compute-guard` as success with a "⚠ WARN" description, so nothing turns red. `COMPUTE_GUARD_MODE=block` flips findings to failure once the repos are clean (orchestrator's call). - PR heads: only lines the PR ADDS (vs its merge-base with the default branch). - Default-branch head: the whole tree (the baseline, and what `report` prints). Exceptions live in ONE file, ci/compute-guard-allow.yml, each with a reason. Tests, docs, lockfiles, vendored code and CI config are never scanned. Reads the sync's bare GitHub clones on Veron (no docker exec: IO-stall lesson). python3 scripts/compute_guard.py report [repo ...] # whole-tree findings on each default branch """ from __future__ import annotations import fnmatch import hashlib import json import os import re import subprocess import sys from dataclasses import dataclass from pathlib import Path import yaml ROOT = Path(__file__).resolve().parents[1] ALLOW_FILE = Path(os.environ.get("COMPUTE_GUARD_ALLOW", ROOT / "ci" / "compute-guard-allow.yml")) WORK = Path(os.environ.get("SYNC_WORK", "/srv/windygit/sync")) CACHE = Path(os.environ.get("COMPUTE_GUARD_CACHE", "/var/lib/windy-git/compute-guard-cache.json")) MODE = os.environ.get("COMPUTE_GUARD_MODE", "warn") # warn | block HOSTS = [ "api.anthropic.com", "api.openai.com", "api.groq.com", "generativelanguage.googleapis.com", "api.mistral.ai", "api.perplexity.ai", "openrouter.ai", "api.together.xyz", "api.together.ai", "api.cerebras.ai", "api.sambanova.ai", "api.deepseek.com", "api.x.ai", "api.cohere.ai", "api.cohere.com", "api.fireworks.ai", "api.replicate.com", "api-inference.huggingface.co", ] KEYS = [ "ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN", "OPENAI_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_AI_API_KEY", "MISTRAL_API_KEY", "PERPLEXITY_API_KEY", "PPLX_API_KEY", "OPENROUTER_API_KEY", "TOGETHER_API_KEY", "CEREBRAS_API_KEY", "SAMBANOVA_API_KEY", "DEEPSEEK_API_KEY", "XAI_API_KEY", "COHERE_API_KEY", "FIREWORKS_API_KEY", "REPLICATE_API_TOKEN", ] PY_SDKS = r"anthropic|openai|groq|mistralai|cohere|google\.generativeai|google\.genai|together|cerebras|litellm" JS_SDKS = (r"@anthropic-ai/sdk|openai|groq-sdk|@google/generative-ai|@google/genai|@mistralai/mistralai" r"|cohere-ai|together-ai|@ai-sdk/(?:anthropic|openai|groq|google|mistral)") RULES: list[tuple[str, re.Pattern]] = [ ("provider host", re.compile("|".join(re.escape(h) for h in HOSTS))), ("provider key", re.compile(r"\b(?:" + "|".join(KEYS) + r")\b")), ("provider SDK", re.compile(rf"^\s*(?:from|import)\s+(?:{PY_SDKS})(?:\s|\.|$|,)")), ("provider SDK", re.compile(rf"""(?:from\s+|require\(\s*|import\(\s*)['"](?:{JS_SDKS})(?:/[^'"]*)?['"]""")), # dependency manifests: package.json keys, requirements / pyproject lines ("provider SDK dep", re.compile(rf'''^\s*"(?:{JS_SDKS})"\s*:''')), ("provider SDK dep", re.compile(rf'''^\s*["']?(?:{PY_SDKS.replace(chr(92) + ".", "-")})(?:\[[^\]]*\])?\s*(?:[<>=~!]=?|["',]|$)''')), ] DEP_FILES = re.compile(r"(^|/)(package\.json|requirements[^/]*\.txt|pyproject\.toml|setup\.cfg|Pipfile)$") # Never scanned: tests, docs, lockfiles, vendored/built code, CI config. SKIP = re.compile( r"(^|/)(tests?|__tests__|spec|docs?|node_modules|vendor|dist|build|\.github|\.gitea)/" r"|(^|/)(test_[^/]*|[^/]*_test\.py|conftest\.py|[^/]*\.(test|spec)\.[cm]?[jt]sx?)$" r"|\.(md|mdx|rst|txt|lock|snap|svg|png|jpg|pdf)$" r"|(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|uv\.lock|poetry\.lock|Cargo\.lock)$" ) @dataclass(frozen=True) class Finding: path: str line: int kind: str match: str def load_allow(path: Path = ALLOW_FILE) -> list[dict]: data = yaml.safe_load(path.read_text()) or {} entries = data.get("allow") or [] for e in entries: # a reason per entry is the whole point of the file if not (e.get("repo") and e.get("paths") and str(e.get("reason", "")).strip()): raise ValueError(f"allow entry needs repo, paths and a reason: {e}") return entries def allowed(repo: str, path: str, allow: list[dict]) -> bool: for e in allow: if e["repo"] == repo and any(fnmatch.fnmatch(path, g) for g in e["paths"]): return True return False COMMENT = re.compile(r"^\s*(?:#|//|/\*|\*|