#!/usr/bin/env python3 """Check kings-guard against the NetKingdom security layer model (§5, §11). Read-only. kings-guard's whole position under the standard rests on one claim: it holds no direct client for any Tooling-layer system, and the capabilities that would need one sit at zero instead (§11 blocked-clean) That claim has been asserted in prose since KG-DEC-2026-001. §11 (v0.6) requires a machine-readable declaration because prose cannot distinguish a declaration from a transcribed review. This script is what makes the claim checkable: it fails if a Tooling client appears in src/ without a matching layer.yaml entry. The failure it exists to catch is a *convenience* — someone reaching for an OpenBao or cluster client during an incident because the engine surface still does not exist (§9.2). That is precisely the "small convenience" §6 warns about, and it would arrive as a one-line import. Review dates are reported, never enforced: a date-triggered failure breaks the build on a calendar day with no code change. Exit 0 clean, 1 undeclared contact found, 2 declaration malformed. """ from __future__ import annotations import argparse import ast import sys from datetime import date from pathlib import Path import yaml ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src" / "kings_guard" DECL = ROOT / "layer.yaml" # Import roots that would constitute a direct Tooling-layer client under §4. # Matched against the top-level module of every import in src/. TOOLING_IMPORTS = { "hvac": "OpenBao / Vault client", "bao": "OpenBao client", "kubernetes": "cluster client", "kubernetes_asyncio": "cluster client", "psycopg": "direct database connection", "psycopg2": "direct database connection", "asyncpg": "direct database connection", "sqlalchemy": "direct database connection", "pymysql": "direct database connection", "redis": "direct datastore connection", "ldap3": "direct LDAP client (key-cape tooling)", "python_ldap": "direct LDAP client (key-cape tooling)", "docker": "container runtime client", } def load_declaration() -> dict: if not DECL.exists(): print(f"FAIL: no declaration at {DECL.relative_to(ROOT)} (§11)", file=sys.stderr) raise SystemExit(2) try: data = yaml.safe_load(DECL.read_text()) except yaml.YAMLError as exc: print(f"FAIL: {DECL.name} is not parseable: {exc}", file=sys.stderr) raise SystemExit(2) from exc for key in ("layer", "repository", "tooling_contacts", "standard_version"): if key not in data: print(f"FAIL: {DECL.name} missing required key '{key}' (§11)", file=sys.stderr) raise SystemExit(2) if data["layer"] != "staff": print(f"FAIL: declared layer is '{data['layer']}', expected 'staff'", file=sys.stderr) raise SystemExit(2) return data def imported_modules(path: Path) -> set[str]: """Top-level module name of every import in one file.""" try: tree = ast.parse(path.read_text()) except SyntaxError: return set() found: set[str] = set() for node in ast.walk(tree): if isinstance(node, ast.Import): found.update(alias.name.split(".")[0] for alias in node.names) elif isinstance(node, ast.ImportFrom): if node.level == 0 and node.module: found.add(node.module.split(".")[0]) return found def scan() -> list[tuple[Path, str, str]]: hits: list[tuple[Path, str, str]] = [] for path in sorted(SRC.rglob("*.py")): for module in sorted(imported_modules(path)): if module in TOOLING_IMPORTS: hits.append((path, module, TOOLING_IMPORTS[module])) return hits def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--report", action="store_true", help="print the declaration summary") args = parser.parse_args() decl = load_declaration() declared = {c.get("id") for c in decl.get("tooling_contacts") or []} hits = scan() undeclared = [h for h in hits if h[1] not in declared] if args.report: print(f"kings-guard — layer {decl['layer']}, standard v{decl['standard_version']}") print(f" tooling contacts declared: {len(declared)}") print(f" unowned capabilities (§11 blocked-clean): " f"{len(decl.get('unowned_capabilities') or [])}") today = date.today() for cap in decl.get("unowned_capabilities") or []: review = cap.get("review") stale = "" if review and date.fromisoformat(str(review)) < today: stale = " [REVIEW OVERDUE]" print(f" - {cap['id']}: {cap.get('owner_status', '?')}{stale}") if undeclared: print("", file=sys.stderr) print("FAIL: undeclared Tooling-layer client (§11 undeclared violation)", file=sys.stderr) for path, module, what in undeclared: rel = path.relative_to(ROOT) print(f" {rel}: imports '{module}' — {what}", file=sys.stderr) print("", file=sys.stderr) print(" A Staff repository may not hold a direct Tooling client (§5).", file=sys.stderr) print(" Route it through the owning engine, or if none exists, raise an", file=sys.stderr) print(" engine gap — do not declare this to make the check pass.", file=sys.stderr) return 1 if not args.report: print(f"OK: no direct Tooling client in {SRC.relative_to(ROOT)} (§5, §11)") return 0 if __name__ == "__main__": raise SystemExit(main())