diff --git a/BANNED_PROVIDERS.md b/BANNED_PROVIDERS.md new file mode 100644 index 00000000..2d36aa2b --- /dev/null +++ b/BANNED_PROVIDERS.md @@ -0,0 +1,63 @@ +# Banned Providers + +This document is a hard policy. It is not advisory. It is not aspirational. +Any agent, wizard, or automated process that violates this policy is broken +and must be fixed immediately. + +## Permanently Banned + +### Anthropic (Claude) + +**Status:** BANNED — April 2026 +**Scope:** All configs, fallback chains, playbooks, wizard bootstraps, and fleet scripts. +**Enforcement:** Pre-commit hook, architecture linter, sovereignty enforcement tests. + +No Anthropic model (Claude Opus, Sonnet, Haiku, or any variant) may appear as: +- A primary provider +- A fallback provider +- An OpenRouter model slug (e.g. `anthropic/claude-*`) +- An API endpoint (api.anthropic.com) +- A required dependency (`anthropic` pip package) +- An environment variable (`ANTHROPIC_API_KEY`, `ANTHROPIC_TOKEN`) + +### What to use instead + +| Was | Now | +|-----|-----| +| claude-opus-4-6 | kimi-k2.5 | +| claude-sonnet-4-20250514 | kimi-k2.5 | +| claude-haiku | google/gemini-2.5-pro | +| anthropic (provider) | kimi-coding | +| anthropic/claude-* (OpenRouter) | google/gemini-2.5-pro | +| ANTHROPIC_API_KEY | KIMI_API_KEY | + +### Exceptions + +The following files may reference Anthropic for **historical or defensive** purposes: + +- `training/` — Training data must not be altered +- `evaluations/` — Historical benchmark results +- `RELEASE_*.md` — Changelogs +- `metrics_helpers.py` — Historical cost calculation +- `pre-commit.py` — Detects leaked Anthropic keys (defensive) +- `secret-scan.yml` — Detects leaked Anthropic keys (defensive) +- `architecture_linter.py` — Warns/blocks Anthropic usage (enforcement) +- `test_sovereignty_enforcement.py` — Tests that Anthropic is blocked (enforcement) + +### Golden State + +```yaml +fallback_providers: + - provider: kimi-coding + model: kimi-k2.5 + reason: Primary + - provider: openrouter + model: google/gemini-2.5-pro + reason: Cloud fallback + - provider: ollama + model: gemma4:latest + base_url: http://localhost:11434/v1 + reason: Terminal fallback — never phones home +``` + +*Sovereignty and service always.* diff --git a/hermes-sovereign/githooks/pre-commit.py b/hermes-sovereign/githooks/pre-commit.py index a48ade0a..9bdb0f8d 100644 --- a/hermes-sovereign/githooks/pre-commit.py +++ b/hermes-sovereign/githooks/pre-commit.py @@ -272,6 +272,48 @@ def get_file_content_at_staged(filepath: str) -> bytes: return result.stdout + +# --------------------------------------------------------------------------- +# BANNED PROVIDER CHECK — Anthropic is permanently banned +# --------------------------------------------------------------------------- + +_BANNED_PROVIDER_PATTERNS = [ + (re.compile(r"provider:\s*anthropic", re.IGNORECASE), "Anthropic provider reference"), + (re.compile(r"anthropic/claude", re.IGNORECASE), "Anthropic model slug"), + (re.compile(r"api\.anthropic\.com"), "Anthropic API endpoint"), + (re.compile(r"claude-opus", re.IGNORECASE), "Claude Opus model"), + (re.compile(r"claude-sonnet", re.IGNORECASE), "Claude Sonnet model"), + (re.compile(r"claude-haiku", re.IGNORECASE), "Claude Haiku model"), +] + +# Files exempt from the ban (training data, historical docs, tests) +_BAN_EXEMPT = { + "training/", "evaluations/", "RELEASE_v", "PERFORMANCE_", + "scores.json", "docs/design-log/", "FALSEWORK.md", + "test_sovereignty_enforcement.py", "test_metrics_helpers.py", + "metrics_helpers.py", "sonnet-workforce.md", +} + + +def _is_ban_exempt(filepath: str) -> bool: + return any(exempt in filepath for exempt in _BAN_EXEMPT) + + +def scan_banned_providers(filepath: str, content: str) -> List[Finding]: + """Block any commit that introduces banned provider references.""" + if _is_ban_exempt(filepath): + return [] + findings = [] + for line_no, line in enumerate(content.splitlines(), start=1): + for pattern, desc in _BANNED_PROVIDER_PATTERNS: + if pattern.search(line): + findings.append(Finding( + filepath, line_no, + f"🚫 BANNED PROVIDER: {desc}. Anthropic is permanently banned from this system." + )) + return findings + + # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -295,11 +337,21 @@ def main() -> int: if line.startswith("+") and not line.startswith("+++"): findings.extend(scan_line(line[1:], "", line_no)) + # Scan for banned providers + for filepath in staged_files: + file_content = get_file_content_at_staged(filepath) + if not is_binary_content(file_content): + try: + text = file_content.decode("utf-8") if isinstance(file_content, bytes) else file_content + findings.extend(scan_banned_providers(filepath, text)) + except UnicodeDecodeError: + pass + if not findings: - print(f"{GREEN}✓ No potential secret leaks detected{NC}") + print(f"{GREEN}✓ No potential secret leaks or banned providers detected{NC}") return 0 - print(f"{RED}✗ Potential secret leaks detected:{NC}\n") + print(f"{RED}✗ Violations detected:{NC}\n") for finding in findings: loc = finding.filename print( @@ -308,7 +360,7 @@ def main() -> int: print() print(f"{RED}╔════════════════════════════════════════════════════════════╗{NC}") - print(f"{RED}║ COMMIT BLOCKED: Potential secrets detected! ║{NC}") + print(f"{RED}║ COMMIT BLOCKED: Secrets or banned providers detected! ║{NC}") print(f"{RED}╚════════════════════════════════════════════════════════════╝{NC}") print() print("Recommendations:") diff --git a/scripts/architecture_linter.py b/scripts/architecture_linter.py index fa426442..1d58262b 100644 --- a/scripts/architecture_linter.py +++ b/scripts/architecture_linter.py @@ -1,33 +1,85 @@ #!/usr/bin/env python3 +"""Architecture Linter — Ensuring alignment with the Frontier Local Agenda. + +Anthropic is BANNED. Not deprecated, not discouraged — banned. +Any reference to Anthropic as a provider, model, or API endpoint +in active configs is a hard failure. +""" + import os import sys import re -# Architecture Linter -# Ensuring all changes align with the Frontier Local Agenda. - SOVEREIGN_RULES = [ - (r"https?://(api\.openai\.com|api\.anthropic\.com)", "CRITICAL: External cloud API detected. Use local custom_provider instead."), - (r"provider: (openai|anthropic)", "WARNING: Direct cloud provider used. Ensure fallback_model is configured."), - (r"api_key: ['"][^'"\s]{10,}['"]", "SECURITY: Hardcoded API key detected. Use environment variables.") + # BANNED — hard failures + (r"provider:\s*anthropic", "BANNED: Anthropic provider reference. Anthropic is permanently banned from this system."), + (r"anthropic/claude", "BANNED: Anthropic model reference (anthropic/claude-*). Use kimi-k2.5 or google/gemini-2.5-pro."), + (r"api\.anthropic\.com", "BANNED: Direct Anthropic API endpoint. Anthropic is permanently banned."), + (r"ANTHROPIC_API_KEY", "BANNED: Anthropic API key reference. Remove all Anthropic credentials."), + (r"ANTHROPIC_TOKEN", "BANNED: Anthropic token reference. Remove all Anthropic credentials."), + (r"sk-ant-", "BANNED: Anthropic API key literal (sk-ant-*). Remove immediately."), + (r"claude-opus", "BANNED: Claude Opus model reference. Use kimi-k2.5."), + (r"claude-sonnet", "BANNED: Claude Sonnet model reference. Use kimi-k2.5."), + (r"claude-haiku", "BANNED: Claude Haiku model reference. Use google/gemini-2.5-pro."), + + # Existing sovereignty rules + (r"https?://api\.openai\.com", "WARNING: Direct OpenAI API endpoint. Use local custom_provider instead."), + (r"provider:\s*openai", "WARNING: Direct OpenAI provider. Ensure fallback_model is configured."), + (r"api_key: ['\"][^'\"\s]{10,}['\"]", "SECURITY: Hardcoded API key detected. Use environment variables."), ] -def lint_file(path): +# Files to skip (training data, historical docs, changelogs, tests that validate the ban) +SKIP_PATTERNS = [ + "training/", "evaluations/", "RELEASE_v", "PERFORMANCE_", + "scores.json", "docs/design-log/", "FALSEWORK.md", + "test_sovereignty_enforcement.py", "test_metrics_helpers.py", + "metrics_helpers.py", # historical cost data +] + + +def should_skip(path: str) -> bool: + return any(skip in path for skip in SKIP_PATTERNS) + + +def lint_file(path: str) -> int: + if should_skip(path): + return 0 print(f"Linting {path}...") content = open(path).read() violations = 0 for pattern, msg in SOVEREIGN_RULES: - if re.search(pattern, content): + matches = list(re.finditer(pattern, content, re.IGNORECASE)) + if matches: print(f" [!] {msg}") + for m in matches[:3]: # Show up to 3 locations + line_no = content[:m.start()].count('\n') + 1 + print(f" Line {line_no}: ...{content[max(0,m.start()-20):m.end()+20].strip()}...") violations += 1 return violations + def main(): - print("--- Ezra's Architecture Linter ---") + print("--- Architecture Linter (Anthropic BANNED) ---") files = [f for f in sys.argv[1:] if os.path.isfile(f)] + if not files: + # If no args, scan all yaml/py/sh/json in the repo + for root, _, filenames in os.walk("."): + for fn in filenames: + if fn.endswith((".yaml", ".yml", ".py", ".sh", ".json", ".md")): + path = os.path.join(root, fn) + if not should_skip(path) and ".git" not in path: + files.append(path) + total_violations = sum(lint_file(f) for f in files) + banned = sum(1 for f in files for p, m in SOVEREIGN_RULES + if "BANNED" in m and re.search(p, open(f).read(), re.IGNORECASE) + and not should_skip(f)) + print(f"\nLinting complete. Total violations: {total_violations}") + if banned > 0: + print(f"\n🚫 {banned} BANNED provider violation(s) detected. Anthropic is permanently banned.") sys.exit(1 if total_violations > 0 else 0) + if __name__ == "__main__": main() diff --git a/tests/test_sovereignty_enforcement.py b/tests/test_sovereignty_enforcement.py index ee0ad9ca..ff9a9a77 100644 --- a/tests/test_sovereignty_enforcement.py +++ b/tests/test_sovereignty_enforcement.py @@ -200,3 +200,97 @@ class TestVoiceSovereignty: stt_provider = config.get("stt", {}).get("provider", "") assert stt_provider in ("local", "whisper", ""), \ f"STT provider '{stt_provider}' may use cloud" + + +# ── Anthropic Ban ──────────────────────────────────────────────────── + +class TestAnthropicBan: + """Anthropic is permanently banned from this system. + + Not deprecated. Not discouraged. Banned. Any reference to Anthropic + as a provider, model, or API endpoint in active wizard configs, + playbooks, or fallback chains is a hard failure. + """ + + BANNED_PATTERNS = [ + "provider: anthropic", + "provider: \"anthropic\"", + "anthropic/claude", + "claude-opus", + "claude-sonnet", + "claude-haiku", + "api.anthropic.com", + ] + + ACTIVE_CONFIG_DIRS = [ + "wizards", + "playbooks", + ] + + ACTIVE_CONFIG_FILES = [ + "fallback-portfolios.yaml", + "config.yaml", + ] + + def _scan_active_configs(self): + """Collect all active config files for scanning.""" + files = [] + for dir_name in self.ACTIVE_CONFIG_DIRS: + dir_path = REPO_ROOT / dir_name + if dir_path.exists(): + for f in dir_path.rglob("*.yaml"): + files.append(f) + for f in dir_path.rglob("*.yml"): + files.append(f) + for f in dir_path.rglob("*.json"): + files.append(f) + for fname in self.ACTIVE_CONFIG_FILES: + fpath = REPO_ROOT / fname + if fpath.exists(): + files.append(fpath) + return files + + def test_no_anthropic_in_wizard_configs(self): + """No wizard config may reference Anthropic as a provider or model.""" + wizard_dir = REPO_ROOT / "wizards" + if not wizard_dir.exists(): + pytest.skip("No wizards directory") + for config_file in wizard_dir.rglob("*.yaml"): + content = config_file.read_text().lower() + for pattern in self.BANNED_PATTERNS: + assert pattern.lower() not in content, \ + f"BANNED: {config_file.name} contains \"{pattern}\". Anthropic is permanently banned." + + def test_no_anthropic_in_playbooks(self): + """No playbook may reference Anthropic models.""" + playbook_dir = REPO_ROOT / "playbooks" + if not playbook_dir.exists(): + pytest.skip("No playbooks directory") + for pb_file in playbook_dir.rglob("*.yaml"): + content = pb_file.read_text().lower() + for pattern in self.BANNED_PATTERNS: + assert pattern.lower() not in content, \ + f"BANNED: {pb_file.name} contains \"{pattern}\". Anthropic is permanently banned." + + def test_no_anthropic_in_fallback_chain(self): + """Fallback portfolios must not include Anthropic.""" + fb_path = REPO_ROOT / "fallback-portfolios.yaml" + if not fb_path.exists(): + pytest.skip("No fallback-portfolios.yaml") + content = fb_path.read_text().lower() + for pattern in self.BANNED_PATTERNS: + assert pattern.lower() not in content, \ + f"BANNED: fallback-portfolios.yaml contains \"{pattern}\". Anthropic is permanently banned." + + def test_no_anthropic_api_key_in_bootstrap(self): + """Wizard bootstrap must not require ANTHROPIC_API_KEY.""" + bootstrap_path = REPO_ROOT / "hermes-sovereign" / "wizard-bootstrap" / "wizard_bootstrap.py" + if not bootstrap_path.exists(): + pytest.skip("No wizard_bootstrap.py") + content = bootstrap_path.read_text() + assert "ANTHROPIC_API_KEY" not in content, \ + "BANNED: wizard_bootstrap.py still checks for ANTHROPIC_API_KEY" + assert "ANTHROPIC_TOKEN" not in content, \ + "BANNED: wizard_bootstrap.py still checks for ANTHROPIC_TOKEN" + assert "\"anthropic\"" not in content.lower(), \ + "BANNED: wizard_bootstrap.py still lists anthropic as a dependency"