VALIDATED_AGAINST now names the accepted security-layer-model v0.7 (net-kingdom@66dc491) as amended by GH-DEC-2026-017, -020, -021 (gate-house@39d9287), not the held v0.8 (021 §2). The A12 detector converges on ops-warden's playbook reference plus the 021 §3 addition (a version in a standard:/companion: value is a pin). A prose citation such as 'the v0.7 scope rule' is no longer failed (021 §1); tests updated. The layer.yaml note rewording stays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
270 lines
12 KiB
Python
270 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Check tenant-engine against the NetKingdom security layer model (§5, §11).
|
|
|
|
Read-only. Makes two mechanical checks:
|
|
|
|
1. INTENT.md frontmatter declares the layer and governs; layer.yaml is a
|
|
derived artifact (derived: true, derived_from: INTENT.md) that must agree
|
|
with it after ASCII case-folding. The layer is one of §3's closed four
|
|
tokens (Taxonomy, Tooling, Engine, Staff). Neither form carries a
|
|
version of the standard or its companion as a pin (GH-DEC-2026-017
|
|
§1-§5, GH-DEC-2026-020, GH-DEC-2026-021 §1, amendments A9, A11, A12 r2).
|
|
2. No catalogued Tooling client (OpenBao, key-cape) appears in src/
|
|
unless it maps to a declared §5.1 / §5.2 / §5.3 entry.
|
|
|
|
PostgreSQL / SQLite / httpx-to-flex-auth / httpx-to-audit-core are not
|
|
Tooling contacts. They are listed in layer.yaml non_tooling_clients so
|
|
the inventory is total.
|
|
|
|
A12 reaches a pin, not a citation (GH-DEC-2026-021 §1). The detector is
|
|
ops-warden's estate reference (wiki/playbooks/netkingdom-layer-declaration.md,
|
|
021 §3): a key naming a standard or companion version, a version in a path or
|
|
file-name token (`_v0.7`, `-v0.8.md`, `@0.7`), and, the one addition, any
|
|
version token in the value of a `standard:` or `companion:` key. A revision
|
|
cited in prose (a space-preceded `v0.7`) is provenance and is not reached.
|
|
`schema_version` and YAML comments are not reached. Stance, claims and classification maps
|
|
(pep-stance.yaml, pip-claims.yaml) are not declarations; this check does not
|
|
read them for A12 and must not (A12 r2, GH-DEC-2026-020 §3).
|
|
|
|
Every run prints the standard text it checks against (VALIDATED_AGAINST) and
|
|
the scope it ranged over, on success and on failure (GH-DEC-2026-020 §4).
|
|
There is no emitted conformance record; the version lives in the run.
|
|
|
|
Exit 0 clean, 1 undeclared contact, 2 declaration malformed.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError: # pragma: no cover - dev extra
|
|
print("FAIL: PyYAML is required (pip install pyyaml)", file=sys.stderr)
|
|
raise SystemExit(2) from None
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src" / "tenant_engine"
|
|
DECL = ROOT / "layer.yaml"
|
|
INTENT = ROOT / "INTENT.md"
|
|
|
|
# The text in force this checker enforces (GH-DEC-2026-020 §4, GH-DEC-2026-021
|
|
# §2): the ACCEPTED v0.7 plus the gate-house decisions enforced beyond it. v0.8
|
|
# is held under GH-DEC-2026-019 and does not govern, so it is not named here.
|
|
# Re-point after the flip (GH-WP-0004-T11).
|
|
VALIDATED_AGAINST = ("net-kingdom/canon/standards/security-layer-model_v0.7.md "
|
|
"(net-kingdom@66dc491) as amended by GH-DEC-2026-017, "
|
|
"GH-DEC-2026-020, GH-DEC-2026-021 (gate-house@39d9287)")
|
|
SCOPE = ("declaration = INTENT.md frontmatter + layer.yaml (every key and value); "
|
|
"tooling scan = src/tenant_engine/**/*.py; "
|
|
"not reached: pep-stance.yaml, pip-claims.yaml, comments, schema_version")
|
|
|
|
# A12 pin detector, converged on ops-warden's estate reference (GH-DEC-2026-021
|
|
# §3). The path pattern deliberately flags a versioned path of any document.
|
|
_VERSION_KEY = re.compile(r"(standard|companion).*version|version.*(standard|companion)",
|
|
re.IGNORECASE)
|
|
_VERSION_IN_VALUE = re.compile(r"[_\-.]v\d+(\.\d+)*(\.md)?\b|@v?\d+\.\d+", re.IGNORECASE)
|
|
_EXEMPT_KEYS = {"schema_version"} # the declaration file's own schema, not reached
|
|
# 021 §3 addition: an identity-bearing standard:/companion: value carries no version.
|
|
_IDENTITY_KEYS = {"standard", "companion"}
|
|
_ANY_VERSION = re.compile(r"v?\d+\.\d+", re.IGNORECASE)
|
|
|
|
# Catalogued Tooling in statute §4 today: key-cape and OpenBao.
|
|
# Import roots that would constitute a direct client of those.
|
|
TOOLING_IMPORTS = {
|
|
"hvac": "OpenBao / Vault client",
|
|
"bao": "OpenBao client",
|
|
"keycloak": "key-cape / Keycloak client",
|
|
"ldap3": "direct LDAP client (key-cape tooling)",
|
|
"python_ldap": "direct LDAP client (key-cape tooling)",
|
|
}
|
|
|
|
TOOLING_ARGV = re.compile(r"""\[\s*(?:["']bao["']|bao_bin\b|bao_binary\b)\s*,""")
|
|
OPENBAO_ADDR = re.compile(r"\b(?:VAULT_ADDR|BAO_ADDR|X-Vault-Token)\b")
|
|
|
|
# §3 as amended (A9): closed, four tokens, compared after an ASCII case-fold.
|
|
LAYER_VOCABULARY = {"taxonomy", "tooling", "engine", "staff"}
|
|
|
|
|
|
def _fold(value: object) -> str:
|
|
"""ASCII case-fold only; two spellings of a token are one token."""
|
|
return str(value).translate(str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
"abcdefghijklmnopqrstuvwxyz"))
|
|
|
|
|
|
def version_findings(data: object, where: str) -> list[str]:
|
|
"""Every pin in a parsed declaration (A12, GH-DEC-2026-021 §1, §3)."""
|
|
found: list[str] = []
|
|
|
|
def walk(node: object, path: str) -> None:
|
|
if isinstance(node, dict):
|
|
for key, value in node.items():
|
|
sub = f"{path}.{key}" if path else str(key)
|
|
if str(key) in _EXEMPT_KEYS:
|
|
continue
|
|
if _VERSION_KEY.search(str(key)):
|
|
found.append(f"{where}: key '{sub}' is a version key")
|
|
continue
|
|
if (_fold(key) in _IDENTITY_KEYS and isinstance(value, str)
|
|
and _ANY_VERSION.search(value)):
|
|
found.append(f"{where}: '{sub}' carries a version: {value.strip()[:80]!r}")
|
|
continue
|
|
walk(value, sub)
|
|
elif isinstance(node, list):
|
|
for i, item in enumerate(node):
|
|
walk(item, f"{path}[{i}]")
|
|
elif isinstance(node, str):
|
|
if _VERSION_IN_VALUE.search(node):
|
|
found.append(f"{where}: '{path}' carries a version: {node.strip()[:80]!r}")
|
|
|
|
walk(data, "")
|
|
return found
|
|
|
|
|
|
def _reject_versions(data: object, where: str) -> None:
|
|
findings = version_findings(data, where)
|
|
if findings:
|
|
print("FAIL: a layer declaration must not carry a version of the standard "
|
|
"or its companion, as a pin (A12, GH-DEC-2026-020, GH-DEC-2026-021 §1)",
|
|
file=sys.stderr)
|
|
for line in findings:
|
|
print(f" {line}", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
|
|
|
|
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", "role", "repository", "derived", "derived_from", "tooling_contacts"):
|
|
if key not in data:
|
|
print(f"FAIL: {DECL.name} missing required key '{key}'", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
if data["derived"] is not True or data["derived_from"] != "INTENT.md":
|
|
print(f"FAIL: {DECL.name} must be marked derived: true, derived_from: INTENT.md "
|
|
"(§11, GH-DEC-2026-017 §1)", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
_reject_versions(data, DECL.name)
|
|
if _fold(data["layer"]) not in LAYER_VOCABULARY:
|
|
print(f"FAIL: {DECL.name} layer {data['layer']!r} is outside the closed "
|
|
f"vocabulary {sorted(LAYER_VOCABULARY)}", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
if _fold(data["layer"]) != "engine":
|
|
print(f"FAIL: declared layer is {data['layer']!r}, expected engine", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
if _fold(data["role"]) != "pip":
|
|
print(f"FAIL: declared role is {data['role']!r}, expected pip", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
if data["repository"] != "tenant-engine":
|
|
print(f"FAIL: repository is {data['repository']!r}", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
return data
|
|
|
|
|
|
def intent_frontmatter() -> dict:
|
|
text = INTENT.read_text()
|
|
if not text.startswith("---"):
|
|
print("FAIL: INTENT.md has no YAML frontmatter", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
block = text.split("---", 2)[1]
|
|
data = yaml.safe_load(block) or {}
|
|
if "layer" not in data:
|
|
print("FAIL: INTENT.md frontmatter has no layer: key (§11)", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
_reject_versions(data, "INTENT.md frontmatter")
|
|
if _fold(data["layer"]) not in LAYER_VOCABULARY:
|
|
print(f"FAIL: INTENT.md layer {data['layer']!r} is outside the closed "
|
|
f"vocabulary {sorted(LAYER_VOCABULARY)}", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
if _fold(data["layer"]) != "engine":
|
|
print(f"FAIL: INTENT.md layer is {data.get('layer')!r}, expected Engine", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
if _fold(data.get("role", "")) != "pip":
|
|
print(f"FAIL: INTENT.md role is {data.get('role')!r}, expected PIP", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
return data
|
|
|
|
|
|
def imported_modules(path: Path) -> set[str]:
|
|
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) and 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")):
|
|
text = path.read_text()
|
|
for module in sorted(imported_modules(path)):
|
|
if module in TOOLING_IMPORTS:
|
|
hits.append((path, module, TOOLING_IMPORTS[module]))
|
|
if TOOLING_ARGV.search(text) or OPENBAO_ADDR.search(text):
|
|
hits.append((path, "openbao-invocation", "OpenBao argv or address"))
|
|
return hits
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--report", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
# GH-DEC-2026-020 §4: every run states what it checked against and its scope.
|
|
print(f"checking against: {VALIDATED_AGAINST}")
|
|
print(f"scope: {SCOPE}")
|
|
|
|
decl = load_declaration()
|
|
intent = intent_frontmatter()
|
|
|
|
# A11: a disagreement between the governing and derived forms is a finding
|
|
# in its own right, reported rather than resolved by precedence. Case is
|
|
# folded first (A9), so what survives is a real disagreement.
|
|
for key in ("layer", "role"):
|
|
if _fold(decl[key]) != _fold(intent.get(key, "")):
|
|
print(f"FAIL: {key} disagrees after case-fold — INTENT.md (governing): "
|
|
f"{intent.get(key)!r}, layer.yaml (derived): {decl[key]!r}",
|
|
file=sys.stderr)
|
|
return 2
|
|
hits = scan()
|
|
|
|
if args.report:
|
|
print(f"tenant-engine — layer {intent['layer']}, role {intent['role']} "
|
|
f"(INTENT.md governs; layer.yaml derived from {decl['derived_from']})")
|
|
print(f" tooling contacts declared: {len(decl.get('tooling_contacts') or [])}")
|
|
print(f" non-tooling clients: {len(decl.get('non_tooling_clients') or [])}")
|
|
|
|
if hits:
|
|
print("", file=sys.stderr)
|
|
print("FAIL: undeclared Tooling-layer client (§11 undeclared violation)", file=sys.stderr)
|
|
for path, module, what in hits:
|
|
print(f" {path.relative_to(ROOT)}: {module} — {what}", file=sys.stderr)
|
|
return 1
|
|
|
|
if decl.get("tooling_contacts"):
|
|
print("FAIL: tooling_contacts is not empty; this engine claimed none", file=sys.stderr)
|
|
return 1
|
|
|
|
if not args.report:
|
|
print(f"OK: Engine/PIP declaration matches INTENT.md; no Tooling client in "
|
|
f"{SRC.relative_to(ROOT)}; no version in the declaration; "
|
|
f"validated against {VALIDATED_AGAINST}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|