From b5bc4c6b6482dfabf997e145101682ad8a45dcf2 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Mon, 24 Aug 2026 16:41:54 -0700 Subject: [PATCH] docs: add agentic commerce accountability example --- .github/workflows/ci.yml | 21 +++++++ README.md | 1 + agentic-commerce-accountability/README.md | 27 ++++++++ .../fixtures/overspend-tamper.json | 44 +++++++++++++ .../fixtures/valid-purchase.json | 44 +++++++++++++ .../generate_fixtures.py | 34 ++++++++++ .../tests/test_verify_purchase.py | 35 +++++++++++ .../verify_purchase.py | 63 +++++++++++++++++++ 8 files changed, 269 insertions(+) create mode 100644 agentic-commerce-accountability/README.md create mode 100644 agentic-commerce-accountability/fixtures/overspend-tamper.json create mode 100644 agentic-commerce-accountability/fixtures/valid-purchase.json create mode 100644 agentic-commerce-accountability/generate_fixtures.py create mode 100644 agentic-commerce-accountability/tests/test_verify_purchase.py create mode 100644 agentic-commerce-accountability/verify_purchase.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3530c7..5407564 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,27 @@ jobs: - name: Run credit engine tests run: python -m unittest discover -s tests -v + agentic-commerce-accountability: + runs-on: ubuntu-latest + defaults: + run: + working-directory: agentic-commerce-accountability + steps: + - uses: actions/checkout@v7 + + - name: Set up Python 3.11 + uses: actions/setup-python@v7 + with: + python-version: "3.11" + + - name: Run accountability tests + run: python -m unittest discover -s tests -v + + - name: Regenerate and compare committed fixtures + run: | + python generate_fixtures.py + git diff --exit-code -- fixtures + industrial-embodied-ai: runs-on: ubuntu-latest defaults: diff --git a/README.md b/README.md index 8996c29..4bfc4ad 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ adoption, and project governance. | Example | What it shows | Platform | Compliance | |---|---|---|---| +| `agentic-commerce-accountability/` | Delegated spend authority linked to checkout, policy, runtime evidence and receipt, with offline tamper detection | Software-only (offline) | Accountability, least authority | | `ca2a-delegation/` | Agent-to-agent delegation with cA2A: attenuated credit-workflow chain, offline verification, and a rejected scope escalation | Software-only (offline) | Separation of duties, least authority | | `embodied-action-receipts/` | Fixture-style offline verification for embodied action receipts: accepted chain, missing receipt, signature mismatch and valid controller rejection | Software-only fixtures | TRACE action-receipt evidence boundary | | `financial-services/` | Corporate credit risk agent: six-step assessment with CDD, exposure and IFRS 9 guardrails on the write | SEV-SNP / TDX | EU AI Act Art. 9/12, CRR Art. 395, EBA/GL/2020/06, EU AML, DORA Art. 9 | diff --git a/agentic-commerce-accountability/README.md b/agentic-commerce-accountability/README.md new file mode 100644 index 0000000..43f7566 --- /dev/null +++ b/agentic-commerce-accountability/README.md @@ -0,0 +1,27 @@ +# Agentic Commerce Accountability + +This runnable example asks whether an auditor can connect a completed purchase +to the authority the user granted, the exact request evaluated by policy, and +the runtime evidence for that decision. + +The bundle links a constrained authority grant, a UCP-shaped checkout request, +an AGT-style policy decision, a TRACE evidence reference, and a merchant receipt +with canonical SHA-256 digests. The valid fixture stays below a delegated USD +200 limit. The tampered fixture changes the purchase to USD 500 after the policy +decision, so verification detects the overspend and broken digest link. + +```bash +python generate_fixtures.py +python -m unittest discover -s tests -v +python verify_purchase.py fixtures/valid-purchase.json +python verify_purchase.py fixtures/overspend-tamper.json +``` + +## Security boundary + +This is a deterministic composition example, not a claim of UCP, AGT, cA2A, or +TRACE conformance. It verifies constraints and cross-artifact bindings. It does +not verify signatures, merchant settlement, hardware quotes, revocation, or a +live transparency-log receipt. Production use should replace each illustrative +artifact with the corresponding protocol's signed, independently verifiable +record. diff --git a/agentic-commerce-accountability/fixtures/overspend-tamper.json b/agentic-commerce-accountability/fixtures/overspend-tamper.json new file mode 100644 index 0000000..c3f15ff --- /dev/null +++ b/agentic-commerce-accountability/fixtures/overspend-tamper.json @@ -0,0 +1,44 @@ +{ + "authority_grant": { + "grant_id": "grant-2026-08-24-001", + "principal": "user:alice", + "delegate": "spiffe://buyer.example/agent/travel", + "allowed_operations": [ + "ucp.checkout.complete" + ], + "allowed_merchants": [ + "merchant:hotel-example" + ], + "currency": "USD", + "max_amount_minor": 20000, + "expires_at": "2026-08-25T00:00:00Z" + }, + "purchase_request": { + "request_id": "checkout-001", + "operation": "ucp.checkout.complete", + "merchant_id": "merchant:hotel-example", + "currency": "USD", + "amount_minor": 50000, + "cart_digest": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + }, + "policy_decision": { + "decision_id": "agt-decision-001", + "outcome": "allow", + "policy_id": "commerce-spend-v1", + "authority_digest": "sha256:a81ddc9c8c8d6c442e999ed2666e0d722d6ded1be3943c1a24e884735c7e9e1c", + "request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324" + }, + "runtime_evidence": { + "evidence_id": "trace-record-001", + "profile": "illustrative-trace-reference", + "runtime_identity": "spiffe://buyer.example/agent/travel", + "policy_decision_digest": "sha256:e0c2b5df43b9f44cc222a936d57774dfdf60857db65e2714b46cc15554cfa323", + "attestation_reference": "urn:example:attestation:tdx:001" + }, + "purchase_receipt": { + "receipt_id": "merchant-receipt-001", + "status": "completed", + "request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324", + "runtime_evidence_digest": "sha256:712e86343a9f149a8d3a7d25a350ee9051fdbb909be694972e334021a682b8e5" + } +} diff --git a/agentic-commerce-accountability/fixtures/valid-purchase.json b/agentic-commerce-accountability/fixtures/valid-purchase.json new file mode 100644 index 0000000..4fcbf8e --- /dev/null +++ b/agentic-commerce-accountability/fixtures/valid-purchase.json @@ -0,0 +1,44 @@ +{ + "authority_grant": { + "grant_id": "grant-2026-08-24-001", + "principal": "user:alice", + "delegate": "spiffe://buyer.example/agent/travel", + "allowed_operations": [ + "ucp.checkout.complete" + ], + "allowed_merchants": [ + "merchant:hotel-example" + ], + "currency": "USD", + "max_amount_minor": 20000, + "expires_at": "2026-08-25T00:00:00Z" + }, + "purchase_request": { + "request_id": "checkout-001", + "operation": "ucp.checkout.complete", + "merchant_id": "merchant:hotel-example", + "currency": "USD", + "amount_minor": 12500, + "cart_digest": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + }, + "policy_decision": { + "decision_id": "agt-decision-001", + "outcome": "allow", + "policy_id": "commerce-spend-v1", + "authority_digest": "sha256:a81ddc9c8c8d6c442e999ed2666e0d722d6ded1be3943c1a24e884735c7e9e1c", + "request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324" + }, + "runtime_evidence": { + "evidence_id": "trace-record-001", + "profile": "illustrative-trace-reference", + "runtime_identity": "spiffe://buyer.example/agent/travel", + "policy_decision_digest": "sha256:e0c2b5df43b9f44cc222a936d57774dfdf60857db65e2714b46cc15554cfa323", + "attestation_reference": "urn:example:attestation:tdx:001" + }, + "purchase_receipt": { + "receipt_id": "merchant-receipt-001", + "status": "completed", + "request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324", + "runtime_evidence_digest": "sha256:712e86343a9f149a8d3a7d25a350ee9051fdbb909be694972e334021a682b8e5" + } +} diff --git a/agentic-commerce-accountability/generate_fixtures.py b/agentic-commerce-accountability/generate_fixtures.py new file mode 100644 index 0000000..75f371a --- /dev/null +++ b/agentic-commerce-accountability/generate_fixtures.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Generate deterministic fixtures for the commerce accountability example.""" +from __future__ import annotations + +import json +from copy import deepcopy +from pathlib import Path + +from verify_purchase import digest + +HERE = Path(__file__).resolve().parent +FIXTURES = HERE / "fixtures" + + +def build(amount_minor: int = 12_500) -> dict: + grant = {"grant_id": "grant-2026-08-24-001", "principal": "user:alice", "delegate": "spiffe://buyer.example/agent/travel", "allowed_operations": ["ucp.checkout.complete"], "allowed_merchants": ["merchant:hotel-example"], "currency": "USD", "max_amount_minor": 20_000, "expires_at": "2026-08-25T00:00:00Z"} + request = {"request_id": "checkout-001", "operation": "ucp.checkout.complete", "merchant_id": "merchant:hotel-example", "currency": "USD", "amount_minor": amount_minor, "cart_digest": "sha256:" + "a1" * 32} + decision = {"decision_id": "agt-decision-001", "outcome": "allow", "policy_id": "commerce-spend-v1", "authority_digest": digest(grant), "request_digest": digest(request)} + evidence = {"evidence_id": "trace-record-001", "profile": "illustrative-trace-reference", "runtime_identity": "spiffe://buyer.example/agent/travel", "policy_decision_digest": digest(decision), "attestation_reference": "urn:example:attestation:tdx:001"} + receipt = {"receipt_id": "merchant-receipt-001", "status": "completed", "request_digest": digest(request), "runtime_evidence_digest": digest(evidence)} + return {"authority_grant": grant, "purchase_request": request, "policy_decision": decision, "runtime_evidence": evidence, "purchase_receipt": receipt} + + +def write(name: str, value: dict) -> None: + FIXTURES.mkdir(exist_ok=True) + (FIXTURES / name).write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + valid = build() + write("valid-purchase.json", valid) + overspend = deepcopy(valid) + overspend["purchase_request"]["amount_minor"] = 50_000 + write("overspend-tamper.json", overspend) diff --git a/agentic-commerce-accountability/tests/test_verify_purchase.py b/agentic-commerce-accountability/tests/test_verify_purchase.py new file mode 100644 index 0000000..70a51fb --- /dev/null +++ b/agentic-commerce-accountability/tests/test_verify_purchase.py @@ -0,0 +1,35 @@ +from __future__ import annotations +import json +import sys +import unittest +from pathlib import Path + +EXAMPLE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLE)) +from generate_fixtures import build # noqa: E402 +from verify_purchase import verify # noqa: E402 + + +class PurchaseVerificationTests(unittest.TestCase): + def test_valid_purchase_is_accepted(self) -> None: + self.assertEqual(verify(build()), []) + + def test_overspend_and_stale_link_are_rejected(self) -> None: + bundle = build() + bundle["purchase_request"]["amount_minor"] = 50_000 + errors = verify(bundle) + self.assertIn("amount exceeds delegated authority", errors) + self.assertIn("policy decision is not bound to the purchase request", errors) + + def test_merchant_substitution_is_rejected(self) -> None: + bundle = build() + bundle["purchase_request"]["merchant_id"] = "merchant:attacker" + self.assertIn("merchant is outside delegated authority", verify(bundle)) + + def test_committed_fixture_matches_generator(self) -> None: + committed = json.loads((EXAMPLE / "fixtures" / "valid-purchase.json").read_text()) + self.assertEqual(committed, build()) + + +if __name__ == "__main__": + unittest.main() diff --git a/agentic-commerce-accountability/verify_purchase.py b/agentic-commerce-accountability/verify_purchase.py new file mode 100644 index 0000000..a9b6e6b --- /dev/null +++ b/agentic-commerce-accountability/verify_purchase.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Offline verifier for the agentic-commerce accountability example.""" +from __future__ import annotations + +import hashlib +import json +import sys +from pathlib import Path +from typing import Any + + +def digest(value: Any) -> str: + encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + return "sha256:" + hashlib.sha256(encoded).hexdigest() + + +def verify(bundle: dict[str, Any]) -> list[str]: + errors: list[str] = [] + grant = bundle["authority_grant"] + request = bundle["purchase_request"] + decision = bundle["policy_decision"] + evidence = bundle["runtime_evidence"] + receipt = bundle["purchase_receipt"] + if request["operation"] not in grant["allowed_operations"]: + errors.append("operation is outside delegated authority") + if request["currency"] != grant["currency"]: + errors.append("currency differs from delegated authority") + if request["merchant_id"] not in grant["allowed_merchants"]: + errors.append("merchant is outside delegated authority") + if request["amount_minor"] > grant["max_amount_minor"]: + errors.append("amount exceeds delegated authority") + if decision["request_digest"] != digest(request): + errors.append("policy decision is not bound to the purchase request") + if decision["authority_digest"] != digest(grant): + errors.append("policy decision is not bound to the authority grant") + if evidence["policy_decision_digest"] != digest(decision): + errors.append("runtime evidence is not bound to the policy decision") + if receipt["request_digest"] != digest(request): + errors.append("receipt is not bound to the purchase request") + if receipt["runtime_evidence_digest"] != digest(evidence): + errors.append("receipt is not bound to the runtime evidence") + if decision["outcome"] != "allow": + errors.append("policy decision did not allow the purchase") + return errors + + +def main() -> int: + if len(sys.argv) != 2: + print("usage: python verify_purchase.py ", file=sys.stderr) + return 2 + bundle = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + errors = verify(bundle) + if errors: + print("REJECTED") + for error in errors: + print(f"- {error}") + return 1 + print("ACCEPTED: authority, decision, runtime evidence, and receipt are linked") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())