#!/usr/bin/env python3 """Emissar Receipt v0.1 checks. python validate.py 1. Runs receipt-demo.mjs --json and validates every example object against the JSON Schema: receipt documents (root), decoded protected headers, openings, the refusal, anchors, the Agent Card extension entry, the client's request metadata and the receipt artifact inside the final A2A task. 2. Negative tests: objects the schema MUST reject. 3. Parses the OpenAPI YAML, validates it with openapi-spec-validator when installed, and resolves every $ref. Needs: pip install --user jsonschema pyyaml rfc3339-validator (optionally openapi-spec-validator). """ import copy import json import pathlib import subprocess import sys import yaml from jsonschema import Draft202012Validator, FormatChecker HERE = pathlib.Path(__file__).resolve().parent SPECS = HERE.parent.parent SCHEMA_PATH = SPECS / "schemas" / "receipt-v0.1.schema.json" OPENAPI_PATH = SPECS / "openapi" / "receipt-v0.1.yaml" EXT = "https://emissar.ai/ext/receipt/v1" failures = [] def check(ok, label): print(f" {'PASS' if ok else 'FAIL'} {label}") if not ok: failures.append(label) schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) Draft202012Validator.check_schema(schema) fc = FormatChecker() root = Draft202012Validator(schema, format_checker=fc) def sub(name): return Draft202012Validator({**schema, "$ref": f"#/$defs/{name}"}, format_checker=fc) def validate(validator, obj, label): errs = sorted(validator.iter_errors(obj), key=lambda e: list(e.path)) check(not errs, label + (f": {errs[0].message} at {list(errs[0].path)}" if errs else "")) out = subprocess.run( ["node", str(HERE / "receipt-demo.mjs"), "--json"], capture_output=True, text=True, encoding="utf-8" ) if out.returncode != 0: print(out.stdout[-2000:], out.stderr[-2000:]) sys.exit("receipt-demo.mjs failed") data = json.loads(out.stdout) # --json mode prints only the JSON object print(f"JSON Schema ({SCHEMA_PATH.name}, Draft 2020-12, format checks on)") for doc in data["receipts"]: validate(root, doc, f"receipt document {doc['receipt']['receiptId']} ({len(doc['signatures'])} signature(s), {len(doc.get('anchors', []))} anchor(s))") header_v = sub("ProtectedHeader") for i, h in enumerate(data["protectedHeaders"]): validate(header_v, h, f"protected header {i + 1} ({h['alg']}, role {h[EXT]['role']}, seq {h[EXT]['seq']})") openings_v = sub("Openings") for o in data["openings"]: validate(openings_v, o, f"openings for {o['receiptId']}") validate(sub("Refusal"), data["refusal"], "signed refusal") anchor_v = sub("Anchor") for a in data["anchors"]: validate(anchor_v, a, f"anchor at leaf {a['leafIndex']} of {a['treeHead']['treeSize']}") validate(sub("CardExtension"), data["cardExtension"], "Agent Card extension entry") validate(sub("RequestMetadata"), data["requestMetadata"], "request metadata") validate(sub("RequestMetadata"), data["requestMessage"]["metadata"][EXT], "request metadata inside the A2A message") art = next(a for a in data["finalTask"]["artifacts"] if EXT in a.get("extensions", [])) validate(root, art["parts"][0]["data"], "receipt document inside the A2A receipt artifact") validate(openings_v, art["parts"][1]["data"], "openings inside the A2A receipt artifact") validate(sub("ArtifactMetadata"), art["metadata"][EXT], "receipt artifact metadata") for name, doc in data["invalid"].items(): validate(root, doc, f"'{name}' example is schema-valid (it fails cryptographic checks, not the schema)") print("Schema negative tests (each MUST be rejected)") good = data["receipts"][1] bad = [] x = copy.deepcopy(good); x["receipt"]["request"]["messages"][0]["digest"] = "ni:///sha-256-128;AAAAAAAAAAAAAAAAAAAAAA"; bad.append(("truncated digest suite sha-256-128", x)) x = copy.deepcopy(good); x["receipt"]["timestamps"]["issuedAt"] = "2026-09-26T14:05:43Z"; bad.append(("timestamp without milliseconds", x)) x = copy.deepcopy(good); x["receipt"]["timestamps"]["issuedAt"] = "2026-09-26T10:05:43.960-04:00"; bad.append(("timestamp with an offset", x)) x = copy.deepcopy(good); x["receipt"]["outcome"]["state"] = "TASK_STATE_WORKING"; bad.append(("non-terminal final state", x)) x = copy.deepcopy(good); x["receipt"]["parties"]["client"]["principal"] = {"type": "person", "id": "jane@example.com"}; bad.append(("person principal named in clear", x)) x = copy.deepcopy(good); x["receipt"]["notes"] = "free text"; bad.append(("unknown receipt member", x)) x = copy.deepcopy(good); x["signatures"][0]["header"] = {"kid": "x"}; bad.append(("unprotected JWS header", x)) x = copy.deepcopy(good); x["signatures"].append(copy.deepcopy(x["signatures"][0])); bad.append(("three signatures", x)) x = copy.deepcopy(good); x["receipt"]["payments"][0]["amount"]["value"] = 1840; bad.append(("amount as a JSON number", x)) x = copy.deepcopy(good); x["receipt"]["parties"]["server"]["agentCard"]["url"] = "http://freight.example.net/.well-known/agent-card.json"; bad.append(("http Agent Card URL", x)) x = copy.deepcopy(good); x["payload"] = "e30"; bad.append(("attached JWS payload member", x)) for label, obj in bad: check(not root.is_valid(obj), label) h = copy.deepcopy(data["protectedHeaders"][0]); h["alg"] = "EdDSA" check(not header_v.is_valid(h), "header alg EdDSA (deprecated by RFC 9864)") h = copy.deepcopy(data["protectedHeaders"][0]); h["crit"] = [] check(not header_v.is_valid(h), "header with empty crit") h = copy.deepcopy(data["protectedHeaders"][0]); del h[EXT]["prev"] check(not header_v.is_valid(h), "header without chain prev") print(f"OpenAPI ({OPENAPI_PATH.name})") api = yaml.safe_load(OPENAPI_PATH.read_text(encoding="utf-8")) check(str(api.get("openapi", "")).startswith("3.1"), f"YAML parses; openapi {api.get('openapi')}; {len(api['paths'])} paths") try: from openapi_spec_validator import validate as validate_openapi validate_openapi(api, base_uri=OPENAPI_PATH.as_uri()) check(True, "openapi-spec-validator: valid OpenAPI 3.1 (external $refs resolved)") except ImportError: print(" SKIP openapi-spec-validator not installed") except Exception as e: # noqa: BLE001 check(False, f"openapi-spec-validator: {str(e).splitlines()[0]}") def resolve_ref(ref): target, _, pointer = ref.partition("#") doc = api if not target else json.loads((OPENAPI_PATH.parent / target).read_text(encoding="utf-8")) node = doc for part in [p for p in pointer.split("/") if p]: node = node[part.replace("~1", "/").replace("~0", "~")] return node def walk(node): if isinstance(node, dict): if "$ref" in node: yield node["$ref"] for v in node.values(): yield from walk(v) elif isinstance(node, list): for v in node: yield from walk(v) refs = sorted(set(walk(api))) missing = [] for ref in refs: try: resolve_ref(ref) except (KeyError, FileNotFoundError): missing.append(ref) check(not missing, f"all {len(refs)} distinct $refs resolve" + (f"; missing: {missing}" if missing else "")) internal = sorted({r for r in walk(schema) if r.startswith("#/")}) dangling = [r for r in internal if r.split("/")[-1] not in schema["$defs"]] check(not dangling, f"all {len(internal)} internal schema $refs resolve" + (f"; missing: {dangling}" if dangling else "")) print(f"\n{'ALL CHECKS PASSED' if not failures else f'{len(failures)} FAILED'}") sys.exit(1 if failures else 0)