diff --git a/bin/validate_config.py b/bin/validate_config.py index 508f14c9..90c2dbfd 100644 --- a/bin/validate_config.py +++ b/bin/validate_config.py @@ -1,2 +1,83 @@ #!/usr/bin/env python3 -print('test') +""" +Config Validator -- pre-deploy YAML validation for timmy-config sidecar. + +Validates YAML syntax, required keys (model.default, model.provider, +toolsets), and provider names before deploy.sh writes to ~/.hermes/. + +Usage: + python3 bin/validate_config.py [path/to/config.yaml] + python3 bin/validate_config.py --strict (fail on warnings too) +""" +import json, os, sys, yaml +from pathlib import Path + +REQUIRED = { + "model": {"type": dict, "keys": {"default": str, "provider": str}}, + "toolsets": {"type": list}, +} +ALLOWED_PROVIDERS = [ + "anthropic", "openai", "nous", "ollama", "openrouter", "openai-codex" +] + +def validate(path): + errors = [] + try: + with open(path) as f: + data = yaml.safe_load(f) + except Exception as e: + return [f"YAML parse error: {e}"] + if not isinstance(data, dict): + return [f"Expected mapping, got {type(data).__name__}"] + + for key, spec in REQUIRED.items(): + if key not in data: + errors.append(f"Required key missing: {key}") + continue + if spec["type"] == dict and not isinstance(data[key], dict): + errors.append(f"{key}: expected dict") + continue + if spec["type"] == list and not isinstance(data[key], list): + errors.append(f"{key}: expected list") + continue + if "keys" in spec: + for sub, sub_type in spec["keys"].items(): + if sub not in data[key]: + errors.append(f"{key}.{sub}: required") + elif not isinstance(data[key][sub], sub_type): + errors.append(f"{key}.{sub}: expected {sub_type.__name__}") + + provider = data.get("model", {}).get("provider") + if provider and provider not in ALLOWED_PROVIDERS: + errors.append(f"model.provider: unknown provider '{provider}'") + + # Check JSON files + for jf in ["channel_directory.json"]: + jp = Path(path).parent / jf + if jp.exists(): + try: + json.loads(jp.read_text()) + except Exception as e: + errors.append(f"{jf}: invalid JSON: {e}") + + return errors + +def main(): + strict = "--strict" in sys.argv + args = [a for a in sys.argv[1:] if not a.startswith("--")] + path = args[0] if args else str(Path(__file__).parent.parent / "config.yaml") + + if not os.path.exists(path): + print(f"ERROR: {path} not found") + sys.exit(1) + + errs = validate(path) + if errs: + for e in errs: + print(f"ERROR: {e}") + print(f"Validation FAILED: {len(errs)} issue(s)") + sys.exit(1) + print(f"OK: {path} is valid") + +if __name__ == "__main__": + main()