Declare layer machine-readably (§11); adopt v0.6 corrections
The standard moved v0.4 -> v0.6. All four findings from our v0.4 review were adopted in v0.5, and v0.6 went further on two of them. §11 now requires a machine-readable declaration — prose cannot distinguish a declaration from a transcribed review. We had none. Added layer.yaml (form adapted from ops-warden's reference implementation), scripts/check_layer_conformance.py, and tests/test_layer_conformance.py. The check makes our central claim mechanical rather than asserted: no direct Tooling client in src/. The test exercises the negative case on a synthetic tree, so it fails if the checker goes blind. pyyaml is added as a DEV dependency only — `dependencies = []` is load-bearing for the §5 claim and stays empty. Adopted from v0.6: - containment is no longer ours (§9.2). Actuation is an Engine concept, unowned and held at zero; kings-guard proposes containment and never performs it. The register row is now a dependency, not our gap. - observation is scoped to Staff-reachable sources, with identity and secret observation pending — our finding 1, adopted near-verbatim. - access-engine DECLINED the authentication-evidence gap; owner is now the identity layer plus audit-core, reproposed and unassented. - §11 blocked-clean recorded, with the rule that it must not rank below conforming — our finding 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEtvmYUBP2fDtirJGWn5MW Assistant: claude-code Assistant-Model: opus Assistant-Process: 4014379@bnt-lap001 Assistant-Session: 4af9e20f-1768-4afc-951b-b507784e382b
This commit is contained in:
parent
d99f395aa1
commit
72c2a42d67
9 changed files with 389 additions and 29 deletions
143
scripts/check_layer_conformance.py
Normal file
143
scripts/check_layer_conformance.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue