Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
27 changes: 27 additions & 0 deletions agentic-commerce-accountability/README.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 44 additions & 0 deletions agentic-commerce-accountability/fixtures/overspend-tamper.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
44 changes: 44 additions & 0 deletions agentic-commerce-accountability/fixtures/valid-purchase.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
34 changes: 34 additions & 0 deletions agentic-commerce-accountability/generate_fixtures.py
Original file line number Diff line number Diff line change
@@ -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)
35 changes: 35 additions & 0 deletions agentic-commerce-accountability/tests/test_verify_purchase.py
Original file line number Diff line number Diff line change
@@ -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()
63 changes: 63 additions & 0 deletions agentic-commerce-accountability/verify_purchase.py
Original file line number Diff line number Diff line change
@@ -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 <bundle.json>", 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())