enforce: Anthropic ban — linter, pre-commit, tests, and policy doc
Some checks failed
PR Checklist / pr-checklist (pull_request) Failing after 1m20s
Some checks failed
PR Checklist / pr-checklist (pull_request) Failing after 1m20s
Anthropic is not just removed — it is banned. This commit adds enforcement at every gate to prevent re-introduction. 1. architecture_linter.py — 9 BANNED rules for Anthropic patterns (provider, model slugs, API endpoints, keys, model names). Scans all yaml/py/sh/json/md. Skips training data and historical docs. 2. pre-commit.py — scan_banned_providers() runs on every staged file. Blocks any commit that introduces Anthropic references. Exempt: training/, evaluations/, changelogs, historical cost data. 3. test_sovereignty_enforcement.py — TestAnthropicBan class with 4 tests: - No Anthropic in wizard configs - No Anthropic in playbooks - No Anthropic in fallback chain - No Anthropic API key in bootstrap 4. BANNED_PROVIDERS.md — Hard policy document. Golden state config. Replacement table. Exception list. Not advisory — mandatory.
This commit is contained in:
63
BANNED_PROVIDERS.md
Normal file
63
BANNED_PROVIDERS.md
Normal file
@@ -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.*
|
||||
@@ -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:], "<diff>", 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:")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user