Skip to content

Commit b5bc4c6

Browse files
docs: add agentic commerce accountability example
1 parent 3d4b2fe commit b5bc4c6

8 files changed

Lines changed: 269 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,27 @@ jobs:
4242
- name: Run credit engine tests
4343
run: python -m unittest discover -s tests -v
4444

45+
agentic-commerce-accountability:
46+
runs-on: ubuntu-latest
47+
defaults:
48+
run:
49+
working-directory: agentic-commerce-accountability
50+
steps:
51+
- uses: actions/checkout@v7
52+
53+
- name: Set up Python 3.11
54+
uses: actions/setup-python@v7
55+
with:
56+
python-version: "3.11"
57+
58+
- name: Run accountability tests
59+
run: python -m unittest discover -s tests -v
60+
61+
- name: Regenerate and compare committed fixtures
62+
run: |
63+
python generate_fixtures.py
64+
git diff --exit-code -- fixtures
65+
4566
industrial-embodied-ai:
4667
runs-on: ubuntu-latest
4768
defaults:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ adoption, and project governance.
1616

1717
| Example | What it shows | Platform | Compliance |
1818
|---|---|---|---|
19+
| `agentic-commerce-accountability/` | Delegated spend authority linked to checkout, policy, runtime evidence and receipt, with offline tamper detection | Software-only (offline) | Accountability, least authority |
1920
| `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 |
2021
| `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 |
2122
| `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 |
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Agentic Commerce Accountability
2+
3+
This runnable example asks whether an auditor can connect a completed purchase
4+
to the authority the user granted, the exact request evaluated by policy, and
5+
the runtime evidence for that decision.
6+
7+
The bundle links a constrained authority grant, a UCP-shaped checkout request,
8+
an AGT-style policy decision, a TRACE evidence reference, and a merchant receipt
9+
with canonical SHA-256 digests. The valid fixture stays below a delegated USD
10+
200 limit. The tampered fixture changes the purchase to USD 500 after the policy
11+
decision, so verification detects the overspend and broken digest link.
12+
13+
```bash
14+
python generate_fixtures.py
15+
python -m unittest discover -s tests -v
16+
python verify_purchase.py fixtures/valid-purchase.json
17+
python verify_purchase.py fixtures/overspend-tamper.json
18+
```
19+
20+
## Security boundary
21+
22+
This is a deterministic composition example, not a claim of UCP, AGT, cA2A, or
23+
TRACE conformance. It verifies constraints and cross-artifact bindings. It does
24+
not verify signatures, merchant settlement, hardware quotes, revocation, or a
25+
live transparency-log receipt. Production use should replace each illustrative
26+
artifact with the corresponding protocol's signed, independently verifiable
27+
record.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"authority_grant": {
3+
"grant_id": "grant-2026-08-24-001",
4+
"principal": "user:alice",
5+
"delegate": "spiffe://buyer.example/agent/travel",
6+
"allowed_operations": [
7+
"ucp.checkout.complete"
8+
],
9+
"allowed_merchants": [
10+
"merchant:hotel-example"
11+
],
12+
"currency": "USD",
13+
"max_amount_minor": 20000,
14+
"expires_at": "2026-08-25T00:00:00Z"
15+
},
16+
"purchase_request": {
17+
"request_id": "checkout-001",
18+
"operation": "ucp.checkout.complete",
19+
"merchant_id": "merchant:hotel-example",
20+
"currency": "USD",
21+
"amount_minor": 50000,
22+
"cart_digest": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
23+
},
24+
"policy_decision": {
25+
"decision_id": "agt-decision-001",
26+
"outcome": "allow",
27+
"policy_id": "commerce-spend-v1",
28+
"authority_digest": "sha256:a81ddc9c8c8d6c442e999ed2666e0d722d6ded1be3943c1a24e884735c7e9e1c",
29+
"request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324"
30+
},
31+
"runtime_evidence": {
32+
"evidence_id": "trace-record-001",
33+
"profile": "illustrative-trace-reference",
34+
"runtime_identity": "spiffe://buyer.example/agent/travel",
35+
"policy_decision_digest": "sha256:e0c2b5df43b9f44cc222a936d57774dfdf60857db65e2714b46cc15554cfa323",
36+
"attestation_reference": "urn:example:attestation:tdx:001"
37+
},
38+
"purchase_receipt": {
39+
"receipt_id": "merchant-receipt-001",
40+
"status": "completed",
41+
"request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324",
42+
"runtime_evidence_digest": "sha256:712e86343a9f149a8d3a7d25a350ee9051fdbb909be694972e334021a682b8e5"
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"authority_grant": {
3+
"grant_id": "grant-2026-08-24-001",
4+
"principal": "user:alice",
5+
"delegate": "spiffe://buyer.example/agent/travel",
6+
"allowed_operations": [
7+
"ucp.checkout.complete"
8+
],
9+
"allowed_merchants": [
10+
"merchant:hotel-example"
11+
],
12+
"currency": "USD",
13+
"max_amount_minor": 20000,
14+
"expires_at": "2026-08-25T00:00:00Z"
15+
},
16+
"purchase_request": {
17+
"request_id": "checkout-001",
18+
"operation": "ucp.checkout.complete",
19+
"merchant_id": "merchant:hotel-example",
20+
"currency": "USD",
21+
"amount_minor": 12500,
22+
"cart_digest": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
23+
},
24+
"policy_decision": {
25+
"decision_id": "agt-decision-001",
26+
"outcome": "allow",
27+
"policy_id": "commerce-spend-v1",
28+
"authority_digest": "sha256:a81ddc9c8c8d6c442e999ed2666e0d722d6ded1be3943c1a24e884735c7e9e1c",
29+
"request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324"
30+
},
31+
"runtime_evidence": {
32+
"evidence_id": "trace-record-001",
33+
"profile": "illustrative-trace-reference",
34+
"runtime_identity": "spiffe://buyer.example/agent/travel",
35+
"policy_decision_digest": "sha256:e0c2b5df43b9f44cc222a936d57774dfdf60857db65e2714b46cc15554cfa323",
36+
"attestation_reference": "urn:example:attestation:tdx:001"
37+
},
38+
"purchase_receipt": {
39+
"receipt_id": "merchant-receipt-001",
40+
"status": "completed",
41+
"request_digest": "sha256:7069a7a4ae68fd431f6dd697327478d9cb07cd4d04f77cf08e9e1ea6c4653324",
42+
"runtime_evidence_digest": "sha256:712e86343a9f149a8d3a7d25a350ee9051fdbb909be694972e334021a682b8e5"
43+
}
44+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python3
2+
"""Generate deterministic fixtures for the commerce accountability example."""
3+
from __future__ import annotations
4+
5+
import json
6+
from copy import deepcopy
7+
from pathlib import Path
8+
9+
from verify_purchase import digest
10+
11+
HERE = Path(__file__).resolve().parent
12+
FIXTURES = HERE / "fixtures"
13+
14+
15+
def build(amount_minor: int = 12_500) -> dict:
16+
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"}
17+
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}
18+
decision = {"decision_id": "agt-decision-001", "outcome": "allow", "policy_id": "commerce-spend-v1", "authority_digest": digest(grant), "request_digest": digest(request)}
19+
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"}
20+
receipt = {"receipt_id": "merchant-receipt-001", "status": "completed", "request_digest": digest(request), "runtime_evidence_digest": digest(evidence)}
21+
return {"authority_grant": grant, "purchase_request": request, "policy_decision": decision, "runtime_evidence": evidence, "purchase_receipt": receipt}
22+
23+
24+
def write(name: str, value: dict) -> None:
25+
FIXTURES.mkdir(exist_ok=True)
26+
(FIXTURES / name).write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
27+
28+
29+
if __name__ == "__main__":
30+
valid = build()
31+
write("valid-purchase.json", valid)
32+
overspend = deepcopy(valid)
33+
overspend["purchase_request"]["amount_minor"] = 50_000
34+
write("overspend-tamper.json", overspend)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
import json
3+
import sys
4+
import unittest
5+
from pathlib import Path
6+
7+
EXAMPLE = Path(__file__).resolve().parents[1]
8+
sys.path.insert(0, str(EXAMPLE))
9+
from generate_fixtures import build # noqa: E402
10+
from verify_purchase import verify # noqa: E402
11+
12+
13+
class PurchaseVerificationTests(unittest.TestCase):
14+
def test_valid_purchase_is_accepted(self) -> None:
15+
self.assertEqual(verify(build()), [])
16+
17+
def test_overspend_and_stale_link_are_rejected(self) -> None:
18+
bundle = build()
19+
bundle["purchase_request"]["amount_minor"] = 50_000
20+
errors = verify(bundle)
21+
self.assertIn("amount exceeds delegated authority", errors)
22+
self.assertIn("policy decision is not bound to the purchase request", errors)
23+
24+
def test_merchant_substitution_is_rejected(self) -> None:
25+
bundle = build()
26+
bundle["purchase_request"]["merchant_id"] = "merchant:attacker"
27+
self.assertIn("merchant is outside delegated authority", verify(bundle))
28+
29+
def test_committed_fixture_matches_generator(self) -> None:
30+
committed = json.loads((EXAMPLE / "fixtures" / "valid-purchase.json").read_text())
31+
self.assertEqual(committed, build())
32+
33+
34+
if __name__ == "__main__":
35+
unittest.main()
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python3
2+
"""Offline verifier for the agentic-commerce accountability example."""
3+
from __future__ import annotations
4+
5+
import hashlib
6+
import json
7+
import sys
8+
from pathlib import Path
9+
from typing import Any
10+
11+
12+
def digest(value: Any) -> str:
13+
encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
14+
return "sha256:" + hashlib.sha256(encoded).hexdigest()
15+
16+
17+
def verify(bundle: dict[str, Any]) -> list[str]:
18+
errors: list[str] = []
19+
grant = bundle["authority_grant"]
20+
request = bundle["purchase_request"]
21+
decision = bundle["policy_decision"]
22+
evidence = bundle["runtime_evidence"]
23+
receipt = bundle["purchase_receipt"]
24+
if request["operation"] not in grant["allowed_operations"]:
25+
errors.append("operation is outside delegated authority")
26+
if request["currency"] != grant["currency"]:
27+
errors.append("currency differs from delegated authority")
28+
if request["merchant_id"] not in grant["allowed_merchants"]:
29+
errors.append("merchant is outside delegated authority")
30+
if request["amount_minor"] > grant["max_amount_minor"]:
31+
errors.append("amount exceeds delegated authority")
32+
if decision["request_digest"] != digest(request):
33+
errors.append("policy decision is not bound to the purchase request")
34+
if decision["authority_digest"] != digest(grant):
35+
errors.append("policy decision is not bound to the authority grant")
36+
if evidence["policy_decision_digest"] != digest(decision):
37+
errors.append("runtime evidence is not bound to the policy decision")
38+
if receipt["request_digest"] != digest(request):
39+
errors.append("receipt is not bound to the purchase request")
40+
if receipt["runtime_evidence_digest"] != digest(evidence):
41+
errors.append("receipt is not bound to the runtime evidence")
42+
if decision["outcome"] != "allow":
43+
errors.append("policy decision did not allow the purchase")
44+
return errors
45+
46+
47+
def main() -> int:
48+
if len(sys.argv) != 2:
49+
print("usage: python verify_purchase.py <bundle.json>", file=sys.stderr)
50+
return 2
51+
bundle = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
52+
errors = verify(bundle)
53+
if errors:
54+
print("REJECTED")
55+
for error in errors:
56+
print(f"- {error}")
57+
return 1
58+
print("ACCEPTED: authority, decision, runtime evidence, and receipt are linked")
59+
return 0
60+
61+
62+
if __name__ == "__main__":
63+
raise SystemExit(main())

0 commit comments

Comments
 (0)