Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/consent-plane-surface.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Consent Plane Surface
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.x' }
- run: python -m pip install pyyaml
- run: python consent-plane/self_test.py
- run: python consent-plane/verify_surface.py
21 changes: 21 additions & 0 deletions consent-plane/self_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Prove verify_surface fires both ways: passes on the real envelope, fires when
the containment is weakened or the surface_id is switched."""
from __future__ import annotations
import copy, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import verify_surface as v # noqa: E402

cp = v.load()
assert v.check(cp) == [], f"real envelope should pass: {v.check(cp)}"

if cp.get("space_deny"):
weak = copy.deepcopy(cp); weak["space_deny"] = weak["space_deny"][:-1]
assert v.check(weak), "verifier did not fire on a weakened space_deny"

switched = copy.deepcopy(cp)
switched["surface_id"] = "browser" if cp["surface_id"] != "browser" else "terminal"
assert v.check(switched), "verifier did not fire on a switched surface_id"

print("OK: verify_surface fires both ways (holds on real; catches weakening + switch).")
9 changes: 9 additions & 0 deletions consent-plane/surface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Consent-plane surface envelope. Conforms to socioprophet-agent-standards
# consent-plane/001 + sourceos-spec isolation-spaces-and-taints. Enforced by
# consent-plane/verify_surface.py (consent-plane-surface CI).
surface_id: terminal
conforms_to: socioprophet-agent-standards/standards/consent-plane/surfaces_v1.yaml#terminal
purposes: [discover, implement, verify]
deny_purposes: [egress, operate] # a terminal must not egress or operate live infra
data_classes: [source-and-config, first-party-source]
space_deny: [kernel-space, system-space] # no OS-core / infra ring from a shell
72 changes: 72 additions & 0 deletions consent-plane/verify_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Enforce this repo's consent-plane surface envelope (fail-closed).

This repo IS the terminal surface; EXPECTED_SURFACE pins it so surface.yaml
cannot be silently switched to a weaker surface. Reads consent-plane/surface.yaml
and asserts the hard invariants. Proven both ways by consent-plane/self_test.py.
Conforms to socioprophet-agent-standards consent-plane/001 + sourceos-spec
isolation-spaces-and-taints.
"""
from __future__ import annotations
import sys
from pathlib import Path
try:
import yaml # type: ignore
except Exception as exc: # pragma: no cover
raise SystemExit("PyYAML is required (python -m pip install pyyaml)") from exc

EXPECTED_SURFACE = "terminal"

# Minimum containment each surface MUST assert (subset checks).
EXPECTED = {
"terminal": {"deny_purposes": {"egress", "operate"},
"space_deny": {"kernel-space", "system-space"}},
"notes": {"deny_purposes": {"egress", "operate"},
"space_deny": {"kernel-space", "system-space", "data-namespace"},
"consent_required": "per-purpose"},
"browser": {"deny_purposes": {"implement", "operate"},
"space_deny": {"kernel-space", "system-space", "user-space", "data-namespace"},
"untrusted_input": True},
}


def check(cp: dict) -> list[str]:
errors: list[str] = []
sid = cp.get("surface_id")
if sid != EXPECTED_SURFACE:
return [f"surface_id must be {EXPECTED_SURFACE!r} for this repo, got {sid!r}"]
for key, want in EXPECTED[sid].items():
got = cp.get(key)
if isinstance(want, set):
if not isinstance(got, list):
errors.append(f"{key} must be a list, got {type(got).__name__}")
continue
missing = want - set(got)
if missing:
errors.append(f"{key} must include {sorted(want)}; missing {sorted(missing)}")
elif got != want:
errors.append(f"{key} must be {want!r}, got {got!r}")
return errors


def load() -> dict:
cfg = Path(__file__).resolve().parent / "surface.yaml"
cp = yaml.safe_load(cfg.read_text())
if not isinstance(cp, dict):
raise SystemExit("consent-plane/surface.yaml top-level must be a mapping")
return cp


def main() -> int:
errors = check(load())
if errors:
print(f"FAIL: {EXPECTED_SURFACE} surface envelope violated:", file=sys.stderr)
for e in errors:
print(f" - {e}", file=sys.stderr)
return 1
print(f"OK: {EXPECTED_SURFACE} surface envelope holds.")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading