|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Enforce this repo's consent-plane surface envelope (fail-closed). |
| 3 | +
|
| 4 | +Reads consent-plane/surface.yaml and asserts the hard invariants for its |
| 5 | +surface_id, so CI FAILS if the surface's containment is weakened. Conforms to |
| 6 | +socioprophet-agent-standards consent-plane/001 + sourceos-spec |
| 7 | +isolation-spaces-and-taints. Proven both ways by consent-plane/self_test.py. |
| 8 | +""" |
| 9 | +from __future__ import annotations |
| 10 | +import sys |
| 11 | +from pathlib import Path |
| 12 | +try: |
| 13 | + import yaml # type: ignore |
| 14 | +except Exception as exc: # pragma: no cover |
| 15 | + raise SystemExit("PyYAML is required (pip install pyyaml)") from exc |
| 16 | + |
| 17 | +# Minimum containment each surface MUST assert (subset checks). |
| 18 | +EXPECTED = { |
| 19 | + "terminal": {"deny_purposes": {"egress", "operate"}, |
| 20 | + "space_deny": {"kernel-space", "system-space"}}, |
| 21 | + "notes": {"deny_purposes": {"egress", "operate"}, |
| 22 | + "space_deny": {"kernel-space", "system-space", "data-namespace"}, |
| 23 | + "consent_required": "per-purpose"}, |
| 24 | + "browser": {"deny_purposes": {"implement", "operate"}, |
| 25 | + "space_deny": {"kernel-space", "system-space", "user-space", "data-namespace"}, |
| 26 | + "untrusted_input": True}, |
| 27 | +} |
| 28 | + |
| 29 | +def main() -> int: |
| 30 | + cfg = Path(__file__).resolve().parent / "surface.yaml" |
| 31 | + cp = yaml.safe_load(cfg.read_text()) or {} |
| 32 | + sid = cp.get("surface_id") |
| 33 | + errors: list[str] = [] |
| 34 | + if sid not in EXPECTED: |
| 35 | + print(f"ERR: unknown surface_id {sid!r} (expected one of {sorted(EXPECTED)})", file=sys.stderr) |
| 36 | + return 1 |
| 37 | + exp = EXPECTED[sid] |
| 38 | + for key, want in exp.items(): |
| 39 | + got = cp.get(key) |
| 40 | + if isinstance(want, set): |
| 41 | + have = set(got or []) |
| 42 | + if not want <= have: |
| 43 | + errors.append(f"{key} must include {sorted(want)}; missing {sorted(want - have)}") |
| 44 | + else: |
| 45 | + if got != want: |
| 46 | + errors.append(f"{key} must be {want!r}, got {got!r}") |
| 47 | + if errors: |
| 48 | + print(f"FAIL: {sid} surface envelope violated:", file=sys.stderr) |
| 49 | + for e in errors: print(f" - {e}", file=sys.stderr) |
| 50 | + return 1 |
| 51 | + print(f"OK: {sid} surface envelope holds ({', '.join(exp)}).") |
| 52 | + return 0 |
| 53 | + |
| 54 | +if __name__ == "__main__": |
| 55 | + sys.exit(main()) |
0 commit comments