diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7914d26..74b931c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install validator dependencies + run: python -m pip install -r requirements.txt - name: Validate JSON and public repo gate run: | python -m json.tool schemas/trust-action-receipt.schema.json >/dev/null diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..332413d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +jsonschema[format]>=4,<5 diff --git a/tools/check-public-task-repo.sh b/tools/check-public-task-repo.sh index 09df9c7..76ff083 100755 --- a/tools/check-public-task-repo.sh +++ b/tools/check-public-task-repo.sh @@ -2,7 +2,7 @@ set -euo pipefail fail() { echo "❌ $1" >&2; exit 1; } -required_files=(README.md SPEC.md PILOT_ACCESS.md ADOPTION.md SECURITY.md CONTRIBUTING.md GOVERNANCE.md VERSIONING.md schemas/trust-action-receipt.schema.json examples/remote-worker.receipt.json docs/public-vs-pilot.md docs/production-checklist.md docs/standardization-roadmap.md tools/validate-receipts.py) +required_files=(README.md SPEC.md PILOT_ACCESS.md ADOPTION.md SECURITY.md CONTRIBUTING.md GOVERNANCE.md VERSIONING.md requirements.txt schemas/trust-action-receipt.schema.json examples/remote-worker.receipt.json docs/public-vs-pilot.md docs/production-checklist.md docs/standardization-roadmap.md tools/validate-receipts.py) for f in "${required_files[@]}"; do [ -f "$f" ] || fail "Missing required file: $f" diff --git a/tools/validate-receipts.py b/tools/validate-receipts.py index 516f621..dd47a28 100755 --- a/tools/validate-receipts.py +++ b/tools/validate-receipts.py @@ -2,22 +2,84 @@ import json from pathlib import Path -required = ["receipt_type", "scenario_id", "actor", "subject", "action", "decision", "evidence", "issued_at"] -valid_decisions = {"allow", "deny", "review_required", "freeze", "revoke"} -ok = True - -for path in sorted(Path("examples").glob("*.json")): - data = json.loads(path.read_text(encoding="utf-8")) - missing = [k for k in required if k not in data] - if missing: - print(f"❌ {path}: missing {', '.join(missing)}") - ok = False - elif data["receipt_type"] != "trust_action_receipt": - print(f"❌ {path}: invalid receipt_type") - ok = False - elif data["decision"] not in valid_decisions: - print(f"❌ {path}: invalid decision") - ok = False - else: - print(f"✅ {path}: valid public draft receipt") -raise SystemExit(0 if ok else 1) +try: + from jsonschema import Draft202012Validator, FormatChecker +except ImportError as exc: + raise SystemExit("Missing dependency. Run: python3 -m pip install -r requirements.txt") from exc + + +SCHEMA_PATH = Path("schemas/trust-action-receipt.schema.json") +EXAMPLES_DIR = Path("examples") +DEMO_SIGNATURE = "demo-signature-not-production" +FORBIDDEN_PUBLIC_MARKERS = ( + "BEGIN " + "PRIVATE KEY", + "PRIVATE_KEY" + "=", + "SECRET" + "=", + "sk_" + "live_", + "pk_" + "live_", + "AKIA", + "scoped key " + "value", + "production signing " + "key", + "real customer " + "receipt", +) + + +def load_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def public_safety_errors(path, receipt): + errors = [] + text = path.read_text(encoding="utf-8") + + for marker in FORBIDDEN_PUBLIC_MARKERS: + if marker in text: + errors.append(f"contains forbidden public marker: {marker}") + + signature = receipt.get("signature") + if signature != DEMO_SIGNATURE: + errors.append("public examples must keep the demo signature marker") + + if receipt.get("receipt_id"): + errors.append("public examples must not claim a production receipt_id") + + if receipt.get("policy_ref"): + errors.append("public examples must not include production policy_ref") + + if receipt.get("proof_ref"): + errors.append("public examples must not include production proof_ref") + + return errors + + +def main(): + schema = load_json(SCHEMA_PATH) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + paths = sorted(EXAMPLES_DIR.glob("*.receipt.json")) + ok = True + + if not paths: + print("❌ examples: no *.receipt.json files found") + return 1 + + for path in paths: + receipt = load_json(path) + schema_errors = sorted(validator.iter_errors(receipt), key=lambda err: list(err.path)) + safety_errors = public_safety_errors(path, receipt) + + if schema_errors or safety_errors: + ok = False + print(f"❌ {path}: invalid public demo receipt") + for err in schema_errors: + field = ".".join(str(part) for part in err.path) or "" + print(f" schema: {field}: {err.message}") + for err in safety_errors: + print(f" safety: {err}") + continue + + print(f"✅ {path}: schema-valid public demo receipt") + + return 0 if ok else 1 + + +raise SystemExit(main())