The security layer model moved v0.1 -> v0.4 (accepted) after ops-warden's assent. Both §5 asks from ADR-0010 were adopted: §5.2 now sanctions the conduit shape on the supplied-authority property, and §5.3 is the declared engine gap amendment, carrying the four fields verbatim and crediting ops-warden's delegation machinery as prior art. Which creates an obligation. §5.3 requires those fields MACHINE-READABLY, and §11 makes "every direct Tooling client maps to a declared §5.1/§5.2/§5.3 entry" a mechanical check. ops-warden's declaration was prose in INTENT.md — the repo that proposed the shape was not implementing it. layer.yaml is the map: 5 contacts (2 declared gaps, 1 read-only observation, 2 conduits) plus the non-Tooling clients recorded explicitly so the check is total rather than silently selective. scripts/check_layer_conformance.py enforces it and found three undeclared modules on its first run — all false positives (help text, a docstring, and the doubles library that SIMULATES bao rather than calling it), which is why the scan now matches invocation shapes instead of the word: an httpx call built against the configured OpenBao address, or an argv whose first element is the bao binary. tests/test_layer_conformance.py adds the §5.2 test the standard says SHOULD exist: _caller_env() returns the caller's environment unchanged, and proxy.py is asserted not to reference X-Vault-Token, approle login, or token create — a conduit that presents its own token is not a conduit. No assertion on review dates, deliberately: a date-triggered failure breaks the build on a calendar day with no code change, the same reasoning WP-0033-T05 recorded for blocker staleness. 398 tests pass, ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWBMovyFoy9RRrfL7zKvPJ Assistant: claude-code Assistant-Model: opus Assistant-Process: 4014535@bnt-lap001 Assistant-Session: d0036016-73e8-4da1-8e47-563e3ab39a3c
164 lines
6.9 KiB
Python
164 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Check ops-warden against the NetKingdom security layer model (§5, §11).
|
|
|
|
Read-only. Makes §11's second mechanical check real:
|
|
|
|
every direct Tooling client in a Staff repository maps to a declared
|
|
§5.1, §5.2, or §5.3 entry
|
|
|
|
The failure this catches is a *new* direct OpenBao contact appearing in
|
|
src/warden/ without an entry in layer.yaml — an undeclared violation (§11),
|
|
which is a finding rather than a tracked gap. It deliberately does NOT check
|
|
the review dates: a date-triggered failure breaks the build on a calendar day
|
|
with no code change (the reasoning recorded in WARDEN-WP-0033-T05), so
|
|
staleness is reported and left to `--report`, never to CI.
|
|
|
|
Exit 0 clean, 1 undeclared contact found, 2 declaration malformed.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src" / "warden"
|
|
DECL = ROOT / "layer.yaml"
|
|
|
|
VALID_SHAPES = {"5.1", "5.2", "5.3"}
|
|
|
|
# A direct Tooling contact is an *invocation*, not a mention. Matching the word
|
|
# "bao" caught help text, a docstring, and the dev-tier doubles library that
|
|
# simulates bao rather than calling it — three false positives on first run.
|
|
# So match the two shapes that actually execute:
|
|
# 1. an HTTP request built against the OpenBao address
|
|
# 2. an argv list whose first element is the bao binary
|
|
TOOLING_PATTERNS = (
|
|
# httpx call whose URL is built from the configured OpenBao/Vault address
|
|
re.compile(r"""\bhttpx\.\w+\(|url\s*=\s*f?["'].*\{self\._cfg\.addr\}"""),
|
|
# argv construction: [bao_bin, ...] / ["bao", ...] / [bao_binary, ...]
|
|
re.compile(r"""\[\s*(?:["']bao["']|bao_bin\b|bao_binary\b)\s*,"""),
|
|
)
|
|
|
|
# httpx alone is not a Tooling contact — policy.py calls an Engine and worker.py
|
|
# calls the State Hub. A module matching only the httpx pattern counts as a
|
|
# contact only if it also references the OpenBao address configuration.
|
|
ADDR_HINT = re.compile(r"""_cfg\.addr|VAULT_ADDR|BAO_ADDR""")
|
|
|
|
# Modules that talk to an Engine or to something outside the §4 catalog. Listed
|
|
# in layer.yaml under non_tooling_clients and excluded from the scan with it.
|
|
def _excluded(decl: dict) -> set[str]:
|
|
return {e["module"].split("/")[-1] for e in decl.get("non_tooling_clients", [])}
|
|
|
|
|
|
def load_declaration() -> dict:
|
|
if not DECL.exists():
|
|
print(f"MISSING: {DECL} — ops-warden must declare in its own voice (§11)")
|
|
raise SystemExit(2)
|
|
decl = yaml.safe_load(DECL.read_text())
|
|
for key in ("layer", "repository", "standard_version", "tooling_contacts"):
|
|
if key not in decl:
|
|
print(f"MALFORMED: layer.yaml has no {key!r}")
|
|
raise SystemExit(2)
|
|
for c in decl["tooling_contacts"]:
|
|
if c.get("shape") not in VALID_SHAPES:
|
|
print(f"MALFORMED: {c.get('id')} has shape {c.get('shape')!r}, not one of {sorted(VALID_SHAPES)}")
|
|
raise SystemExit(2)
|
|
# §5.3 carries four fields, machine-readably. That is the whole point of
|
|
# the shape; a gap missing them is prose wearing a schema.
|
|
if c["shape"] == "5.3":
|
|
for field in ("capability", "intended_owner", "blocked_on", "review"):
|
|
if not c.get(field):
|
|
print(f"MALFORMED: §5.3 entry {c['id']!r} is missing {field!r}")
|
|
raise SystemExit(2)
|
|
# §5.2's test is the supplied-authority property.
|
|
if c["shape"] == "5.2" and c.get("supplied_authority") != "none":
|
|
print(f"MALFORMED: §5.2 conduit {c['id']!r} must declare supplied_authority: none")
|
|
raise SystemExit(2)
|
|
return decl
|
|
|
|
|
|
def scan_modules() -> dict[str, list[int]]:
|
|
"""Return {module_name: [line numbers]} for direct Tooling contacts."""
|
|
found: dict[str, list[int]] = {}
|
|
for path in sorted(SRC.rglob("*.py")):
|
|
if path.name.startswith("test_"):
|
|
continue
|
|
text = path.read_text()
|
|
hits: list[int] = []
|
|
for n, line in enumerate(text.splitlines(), 1):
|
|
stripped = line.strip()
|
|
if stripped.startswith("#") or stripped.startswith('"'):
|
|
continue
|
|
if any(p.search(line) for p in TOOLING_PATTERNS):
|
|
hits.append(n)
|
|
if hits:
|
|
# An httpx-only match needs the OpenBao address to be a Tooling
|
|
# contact; otherwise it is an Engine or non-catalogued call.
|
|
argv_shape = any(TOOLING_PATTERNS[1].search(ln) for ln in text.splitlines())
|
|
if argv_shape or ADDR_HINT.search(text):
|
|
found[path.name] = hits
|
|
return found
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--report", action="store_true", help="also print the declaration and gap review dates")
|
|
args = ap.parse_args()
|
|
|
|
decl = load_declaration()
|
|
declared = {c["module"].split("/")[-1] for c in decl["tooling_contacts"]}
|
|
excluded = _excluded(decl)
|
|
found = scan_modules()
|
|
|
|
undeclared = {m: lines for m, lines in found.items() if m not in declared and m not in excluded}
|
|
# A voluntary declaration has no fixed argv shape to detect (an
|
|
# operator-configured command). Over-declaring is safe; not reporting it as
|
|
# stale keeps the signal meaningful.
|
|
voluntary = {
|
|
c["module"].split("/")[-1]
|
|
for c in decl["tooling_contacts"]
|
|
if c.get("detection") == "voluntary"
|
|
}
|
|
stale_decls = declared - set(found) - voluntary
|
|
|
|
if args.report:
|
|
print(f"{decl['repository']} — layer: {decl['layer']} (model v{decl['standard_version']})")
|
|
print(f"declared by {decl['declared_by']}\n")
|
|
for c in decl["tooling_contacts"]:
|
|
line = f" §{c['shape']} {c['id']:<28} {c['module']}"
|
|
if c["shape"] == "5.3":
|
|
overdue = str(c["review"]) < date.today().isoformat()
|
|
line += f" -> {c['intended_owner']} review {c['review']}"
|
|
if overdue:
|
|
line += " [REVIEW OVERDUE]"
|
|
print(line)
|
|
gaps = [c for c in decl["tooling_contacts"] if c["shape"] == "5.3"]
|
|
print(f"\n{len(gaps)} declared gap(s) — tracked non-conformance, not conformance (§11).")
|
|
|
|
ok = True
|
|
if undeclared:
|
|
ok = False
|
|
print("\nUNDECLARED TOOLING CONTACT — a finding under §11, not a tracked gap:")
|
|
for m, lines in sorted(undeclared.items()):
|
|
print(f" src/warden/{m}: line(s) {', '.join(map(str, lines[:6]))}")
|
|
print("\nAdd a §5.1/§5.2/§5.3 entry to layer.yaml, or route it through an engine.")
|
|
|
|
if stale_decls:
|
|
print("\nNote: declared but no contact found (module removed or refactored?):")
|
|
for m in sorted(stale_decls):
|
|
print(f" {m}")
|
|
|
|
if ok and not args.report:
|
|
print(f"PASS — {len(found)} module(s) with Tooling contact, all declared.")
|
|
elif ok:
|
|
print("\nPASS — every direct Tooling contact maps to a declared shape.")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|