Some checks failed
PR Checklist / pr-checklist (pull_request) Failing after 1m27s
Implements the Ansible Infrastructure as Code story from KT 2026-04-08. One canonical Ansible playbook defines: - Deadman switch (snapshot good config on health, rollback+restart on death) - Golden state config deployment (Anthropic BANNED, Kimi→Gemini→Ollama) - Cron schedule (source-controlled, no manual crontab edits) - Agent startup sequence (pull→validate→start→verify) - request_log telemetry table (every inference call logged) - Thin config pattern (immutable local pointer to upstream) - Gitea webhook handler (deploy on merge) - Config validator (rejects banned providers) Fleet inventory: Timmy (Mac), Allegro (VPS), Bezalel (VPS), Ezra (VPS) Roles: wizard_base, golden_state, deadman_switch, request_log, cron_manager Addresses: timmy-config #442, #443, #444, #445, #446 References: KT Final 2026-04-08 P2, KT Bezalel 2026-04-08 #1-#5
156 lines
4.6 KiB
Python
156 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Config Validator — The Timmy Foundation
|
|
Validates wizard configs against golden state rules.
|
|
Run before any config deploy to catch violations early.
|
|
|
|
Usage:
|
|
python3 validate_config.py <config_file>
|
|
python3 validate_config.py --all # Validate all wizard configs
|
|
|
|
Exit codes:
|
|
0 — All validations passed
|
|
1 — Validation errors found
|
|
2 — File not found or parse error
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import yaml
|
|
import fnmatch
|
|
from pathlib import Path
|
|
|
|
# === BANNED PROVIDERS — HARD POLICY ===
|
|
BANNED_PROVIDERS = {"anthropic", "claude"}
|
|
BANNED_MODEL_PATTERNS = [
|
|
"claude-*",
|
|
"anthropic/*",
|
|
"*sonnet*",
|
|
"*opus*",
|
|
"*haiku*",
|
|
]
|
|
|
|
# === REQUIRED FIELDS ===
|
|
REQUIRED_FIELDS = {
|
|
"model": ["default", "provider"],
|
|
"fallback_providers": None, # Must exist as a list
|
|
}
|
|
|
|
|
|
def is_banned_model(model_name: str) -> bool:
|
|
"""Check if a model name matches any banned pattern."""
|
|
model_lower = model_name.lower()
|
|
for pattern in BANNED_MODEL_PATTERNS:
|
|
if fnmatch.fnmatch(model_lower, pattern):
|
|
return True
|
|
return False
|
|
|
|
|
|
def validate_config(config_path: str) -> list[str]:
|
|
"""Validate a wizard config file. Returns list of error strings."""
|
|
errors = []
|
|
|
|
try:
|
|
with open(config_path) as f:
|
|
cfg = yaml.safe_load(f)
|
|
except FileNotFoundError:
|
|
return [f"File not found: {config_path}"]
|
|
except yaml.YAMLError as e:
|
|
return [f"YAML parse error: {e}"]
|
|
|
|
if not cfg:
|
|
return ["Config file is empty"]
|
|
|
|
# Check required fields
|
|
for section, fields in REQUIRED_FIELDS.items():
|
|
if section not in cfg:
|
|
errors.append(f"Missing required section: {section}")
|
|
elif fields:
|
|
for field in fields:
|
|
if field not in cfg[section]:
|
|
errors.append(f"Missing required field: {section}.{field}")
|
|
|
|
# Check default provider
|
|
default_provider = cfg.get("model", {}).get("provider", "")
|
|
if default_provider.lower() in BANNED_PROVIDERS:
|
|
errors.append(f"BANNED default provider: {default_provider}")
|
|
|
|
default_model = cfg.get("model", {}).get("default", "")
|
|
if is_banned_model(default_model):
|
|
errors.append(f"BANNED default model: {default_model}")
|
|
|
|
# Check fallback providers
|
|
for i, fb in enumerate(cfg.get("fallback_providers", [])):
|
|
provider = fb.get("provider", "")
|
|
model = fb.get("model", "")
|
|
|
|
if provider.lower() in BANNED_PROVIDERS:
|
|
errors.append(f"BANNED fallback provider [{i}]: {provider}")
|
|
|
|
if is_banned_model(model):
|
|
errors.append(f"BANNED fallback model [{i}]: {model}")
|
|
|
|
# Check providers section
|
|
for name, provider_cfg in cfg.get("providers", {}).items():
|
|
if name.lower() in BANNED_PROVIDERS:
|
|
errors.append(f"BANNED provider in providers section: {name}")
|
|
|
|
base_url = str(provider_cfg.get("base_url", ""))
|
|
if "anthropic" in base_url.lower():
|
|
errors.append(f"BANNED URL in provider {name}: {base_url}")
|
|
|
|
# Check system prompt for banned references
|
|
prompt = cfg.get("system_prompt_suffix", "")
|
|
if isinstance(prompt, str):
|
|
for banned in BANNED_PROVIDERS:
|
|
if banned in prompt.lower():
|
|
errors.append(f"BANNED provider referenced in system_prompt_suffix: {banned}")
|
|
|
|
return errors
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} <config_file> [--all]")
|
|
sys.exit(2)
|
|
|
|
if sys.argv[1] == "--all":
|
|
# Validate all wizard configs in the repo
|
|
repo_root = Path(__file__).parent.parent.parent
|
|
wizard_dir = repo_root / "wizards"
|
|
all_errors = {}
|
|
|
|
for wizard_path in sorted(wizard_dir.iterdir()):
|
|
config_file = wizard_path / "config.yaml"
|
|
if config_file.exists():
|
|
errors = validate_config(str(config_file))
|
|
if errors:
|
|
all_errors[wizard_path.name] = errors
|
|
|
|
if all_errors:
|
|
print("VALIDATION FAILED:")
|
|
for wizard, errors in all_errors.items():
|
|
print(f"\n {wizard}:")
|
|
for err in errors:
|
|
print(f" - {err}")
|
|
sys.exit(1)
|
|
else:
|
|
print("All wizard configs passed validation.")
|
|
sys.exit(0)
|
|
else:
|
|
config_path = sys.argv[1]
|
|
errors = validate_config(config_path)
|
|
|
|
if errors:
|
|
print(f"VALIDATION FAILED for {config_path}:")
|
|
for err in errors:
|
|
print(f" - {err}")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"PASSED: {config_path}")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|