Skip to content

Commit 4883a93

Browse files
committed
Replay helper receipt validator on current main
1 parent 06ea42a commit 4883a93

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

tools/check_helper_receipts.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python3
2+
"""Minimal CI policy gate for SourceOS Helper Causal Receipts.
3+
4+
This intentionally checks high-value invariants first:
5+
- every event has a root intent
6+
- helper spawns declare parent/process/trigger/reason/policy
7+
- capability requests declare decision/classification/data_accessed
8+
- local-only profiles do not allow network, DNS, analytics, pasteboard, or account lookup
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from pathlib import Path
14+
import argparse
15+
import json
16+
import sys
17+
from typing import Iterable
18+
19+
LOCAL_ONLY_PROFILES = {
20+
"preview.local_only.v1",
21+
"preview.web_thumbnail.local_only.v1",
22+
"terminal.preview.local_only.v1",
23+
"cache_cleanup.local_only.v1",
24+
"file_picker.native_ui.v1",
25+
}
26+
27+
DENY_IN_LOCAL_ONLY = {
28+
"network.egress",
29+
"dns.lookup",
30+
"analytics.emit",
31+
"pasteboard.read",
32+
"pasteboard.write",
33+
"account.lookup",
34+
"credentials.keychain.lookup",
35+
}
36+
37+
38+
def iter_events(path: Path) -> Iterable[dict]:
39+
if path.suffix == ".jsonl":
40+
for line in path.read_text(errors="replace").splitlines():
41+
if line.strip():
42+
yield json.loads(line)
43+
else:
44+
data = json.loads(path.read_text(errors="replace"))
45+
if isinstance(data, list):
46+
yield from data
47+
else:
48+
yield data
49+
50+
51+
def check_event(ev: dict) -> list[str]:
52+
errors: list[str] = []
53+
54+
if not str(ev.get("root_intent_id", "")).startswith("intent."):
55+
errors.append("root_intent_id must start with 'intent.'.")
56+
57+
event_type = ev.get("event_type")
58+
59+
if event_type == "helper.spawn":
60+
for field in ["parent_process", "child_process", "trigger", "spawn_reason", "policy_profile"]:
61+
if field not in ev or ev.get(field) in (None, ""):
62+
errors.append(f"helper.spawn missing {field}")
63+
64+
if event_type == "capability.request":
65+
for field in ["requestor", "capability", "requested_service", "decision", "classification", "data_accessed"]:
66+
if field not in ev:
67+
errors.append(f"capability.request missing {field}")
68+
69+
if ev.get("policy_profile") in LOCAL_ONLY_PROFILES:
70+
if ev.get("capability") in DENY_IN_LOCAL_ONLY and ev.get("decision") == "allow":
71+
errors.append(
72+
f"local-only policy {ev.get('policy_profile')} allowed forbidden capability {ev.get('capability')}"
73+
)
74+
75+
if event_type == "helper.exit":
76+
for field in ["process", "exit_status", "duration_ms", "receipt_complete"]:
77+
if field not in ev:
78+
errors.append(f"helper.exit missing {field}")
79+
80+
return errors
81+
82+
83+
def main() -> int:
84+
parser = argparse.ArgumentParser()
85+
parser.add_argument("paths", nargs="+", type=Path)
86+
args = parser.parse_args()
87+
88+
failures: list[dict] = []
89+
checked = 0
90+
91+
for path in args.paths:
92+
for index, event in enumerate(iter_events(path)):
93+
checked += 1
94+
for error in check_event(event):
95+
failures.append({"path": str(path), "index": index, "error": error})
96+
97+
print(json.dumps({"checked": checked, "errors": failures}, indent=2, sort_keys=True))
98+
return 1 if failures else 0
99+
100+
101+
if __name__ == "__main__":
102+
raise SystemExit(main())

0 commit comments

Comments
 (0)