From 0c7e3157b5a2bb7455a555a10997fc20a8a779e1 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Tue, 21 Jul 2026 11:44:26 -0700 Subject: [PATCH] feat(ca2a-delegation): add agent-to-agent delegation example Add a new example demonstrating the cA2A agent-to-agent boundary, the layer above the cMCP agent-to-tool boundary the other examples show. - A three-hop credit delegation chain (credit-platform -> lead-credit-agent -> screening-sub-agent -> bureau-connector) built with ca2a-runtime. The authority to write the risk report is granted to the lead agent but withheld from every delegated scope, so attenuation alone guarantees no sub-agent can regain it (separation of duties by credential). - The demo verifies the valid chain, then shows a sub-agent trying to widen its scope and being rejected with SCOPE_ESCALATION. Both chains are also verifiable via `ca2a verify-chain`. - README maps the same pattern onto healthcare, multi-tenant-saas and industrial-embodied-ai, and is explicit that only attenuated delegation and offline verification run today; the live peer path and sealed channel are cA2A roadmap. - Pure delegation_scenario module + unit tests and a CI job. Cross-linked from the startup-tpm next-steps table and the root README. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 22 ++++ README.md | 6 +- ca2a-delegation/README.md | 114 ++++++++++++++++++ .../chain-output/credit-delegation-chain.json | 43 +++++++ .../chain-output/escalation-attempt.json | 44 +++++++ ca2a-delegation/delegation_agent.py | 84 +++++++++++++ ca2a-delegation/delegation_scenario.py | 95 +++++++++++++++ ca2a-delegation/requirements.txt | 3 + .../tests/test_delegation_scenario.py | 53 ++++++++ startup-tpm/README.md | 1 + 10 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 ca2a-delegation/README.md create mode 100644 ca2a-delegation/chain-output/credit-delegation-chain.json create mode 100644 ca2a-delegation/chain-output/escalation-attempt.json create mode 100644 ca2a-delegation/delegation_agent.py create mode 100644 ca2a-delegation/delegation_scenario.py create mode 100644 ca2a-delegation/requirements.txt create mode 100644 ca2a-delegation/tests/test_delegation_scenario.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85ea68d..70d552a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,3 +123,25 @@ jobs: - name: Run people directory tests run: python -m unittest discover -s tests -v + + ca2a-delegation: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ca2a-delegation + steps: + - uses: actions/checkout@v7 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Install cA2A runtime (pre-release) + run: python -m pip install --pre ca2a-runtime + + - name: Run delegation tests + run: python -m unittest discover -s tests -v + + - name: Run the delegation demo + run: python delegation_agent.py diff --git a/README.md b/README.md index 134328f..0261356 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) [![cMCP](https://img.shields.io/badge/Uses-cMCP_Runtime-7c3aed)](https://github.com/agentrust-io/cmcp) [![Agent Manifest](https://img.shields.io/badge/Uses-Agent_Manifest-0ea5e9)](https://github.com/agentrust-io/agent-manifest) +[![cA2A](https://img.shields.io/badge/Uses-cA2A-7c3aed)](https://github.com/agentrust-io/ca2a) [![Discord](https://dcbadge.limes.pink/api/server/9JWNpH7E?style=flat)](https://discord.gg/9JWNpH7E) # agentrust-io Examples -End-to-end integration examples showing cMCP, Agent Manifest, and TRACE working together across deployment scenarios. Each example is self-contained and runnable on a fresh cloud VM. Running them shows how the three projects compose: cMCP enforces policy at the tool call boundary, Agent Manifest carries the identity and capability declaration, and TRACE emits a signed Trust Record for every tool invocation so you can see what the full audit trail looks like in practice. +End-to-end integration examples showing cMCP, Agent Manifest, and TRACE working together across deployment scenarios. Each example is self-contained and runnable on a fresh cloud VM. Running them shows how the projects compose: cMCP enforces policy at the tool call boundary, Agent Manifest carries the identity and capability declaration, cA2A attenuates authority at the agent-to-agent boundary, and TRACE emits a signed Trust Record for every tool invocation so you can see what the full audit trail looks like in practice. ## Examples | Example | What it shows | Platform | Compliance | |---|---|---|---| +| `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 | | `healthcare/` | Clinical agent on a coherent ICD-10 patient: drug-interaction check feeds EU AI Act Art. 14 HITL and contraindication denies | SEV-SNP / TDX | EU AI Act Art. 14, HIPAA | @@ -18,7 +20,7 @@ End-to-end integration examples showing cMCP, Agent Manifest, and TRACE working | `multi-tenant-saas/` | HR SaaS with an EU tenant (enforcing GDPR residency/Art. 9) and a US tenant (advisory) on one catalog | TDX | GDPR Art. 6/9/44, customer DPA | | `startup-tpm/` | 15-minute quickstart on any cloud VM with Trusted Launch | TPM 2.0 | Development / staging | -Each example is fully runnable with no external dependencies: it ships a mock upstream MCP server, an agent script, an attested tool catalog, and a Cedar policy bundle, and ends by printing the signed TRACE Trust Record for the session. The `trace-output/` files in each example are captured from real runs. +Most examples are fully runnable with no external dependencies: they ship a mock upstream MCP server, an agent script, an attested tool catalog, and a Cedar policy bundle, and end by printing the signed TRACE Trust Record for the session. The `trace-output/` files are captured from real runs. The `ca2a-delegation/` and `embodied-action-receipts/` examples are offline verifiers and ship their captured chains instead. ## Quickstart diff --git a/ca2a-delegation/README.md b/ca2a-delegation/README.md new file mode 100644 index 0000000..b26d009 --- /dev/null +++ b/ca2a-delegation/README.md @@ -0,0 +1,114 @@ +# ca2a-delegation: The Agent-to-Agent Boundary + +The other examples in this repo govern the **agent-to-tool** boundary: cMCP decides what a *single* agent may call. This example governs the **agent-to-agent** boundary with [cA2A](https://github.com/agentrust-io/ca2a): when a lead agent hands part of a task to a sub-agent, each hop carries a signed delegation credential whose scope is a provable subset of its parent, so authority can only ever narrow as it flows outward. + +It uses the credit-risk workflow from [`financial-services/`](../financial-services/README.md) as the worked case, and maps the same pattern onto the other examples at the bottom. + +> **Scope of what runs here.** cA2A is in alpha. This example exercises the part that is built today: **attenuated delegation credentials and offline chain verification** (`ca2a_runtime.delegation`). The live peer path (attesting an inbound peer, sealing the payload to its measurement) and the per-hop TRACE provenance record are cA2A roadmap. See the [cA2A ROADMAP](https://github.com/agentrust-io/ca2a/blob/main/ROADMAP.md) and [LIMITATIONS](https://github.com/agentrust-io/ca2a/blob/main/LIMITATIONS.md). + +--- + +## The scenario + +A credit assessment is decomposed across three agents. The authority to **write the risk report** is the sensitive one: it is granted to the lead agent but is deliberately never delegated onward, so no sub-agent can regain it. + +``` + credit-platform + │ grants: read:documents, screen:sanctions, read:bureau, run:risk-model, write:risk-report + ▼ + lead-credit-agent + │ delegates (evidence-gathering only): read:documents, screen:sanctions, read:bureau + │ WITHHELD: run:risk-model, write:risk-report + ▼ + screening-sub-agent + │ delegates (narrowest): read:bureau + ▼ + bureau-connector +``` + +Each hop's scope is a subset of its parent's; each hop's issuer is the previous hop's subject; each links to its parent by `credential_id`. + +--- + +## Run it + +```bash +git clone https://github.com/agentrust-io/examples.git +cd examples/ca2a-delegation +pip install --pre ca2a-runtime # cA2A is alpha; --pre is required +``` + +```bash +python delegation_agent.py +``` + +``` +Building the credit delegation chain: + [0] credit-platform -> lead-credit-agent + scope: ['read:bureau', 'read:documents', 'run:risk-model', 'screen:sanctions', 'write:risk-report'] + [1] lead-credit-agent -> screening-sub-agent + scope: ['read:bureau', 'read:documents', 'screen:sanctions'] + [2] screening-sub-agent -> bureau-connector + scope: ['read:bureau'] + +verify_chain: verified=True hops=3 leaf_scope=['read:bureau'] + authority withheld at the first delegation (never reaches a sub-agent): ['run:risk-model', 'write:risk-report'] + +Now the bureau connector tries to grant itself write:risk-report ... + verify_chain: verified=False code=SCOPE_ESCALATION + reason: hop 2 scope exceeds parent grant +``` + +The demo writes both chains to `chain-output/`. Verify either from the CLI, which checks the same four invariants: + +```bash +ca2a verify-chain --chain chain-output/credit-delegation-chain.json +# {"verified": true, "hops": 3, "leaf_scope": ["read:bureau"]} + +ca2a verify-chain --chain chain-output/escalation-attempt.json +# {"verified": false, "code": "SCOPE_ESCALATION", "error": "hop 2 scope exceeds parent grant"} +``` + +--- + +## What verification checks + +`verify_chain` fails on the first violation: + +1. **Signature** on every hop against the issuer's Ed25519 public key. +2. **Continuity**: each hop's issuer is the previous hop's subject. +3. **Attenuation**: each hop's scope is a subset of its parent's scope (`SCOPE_ESCALATION` otherwise). +4. **Anti-replay / structure**: unique `credential_id`s, `parent_id` links to the previous hop, depth increments by one and stays within `max_depth`. + +Because the write authority is withheld at the first delegation, attenuation alone guarantees that no descendant, however many hops down, can write the risk report. That is separation of duties enforced by the credential, not by convention. + +--- + +## How this maps onto the other examples + +The same agent-to-agent boundary applies wherever one agent hands work to another. cA2A is the layer above the cMCP tool boundary each of these already demonstrates. + +| Example | Delegation hop | Attenuation the chain proves | Escalation it blocks | +|---|---|---|---| +| [`financial-services`](../financial-services/) | lead credit agent → screening sub-agent → bureau connector | evidence-gathering scope is a subset; `write:risk-report` is withheld | a sub-agent trying to write the risk report | +| [`healthcare`](../healthcare/) | clinician agent → pharmacy/medication-safety sub-agent | pharmacist gets only `check:interaction`; no `read:record` or `write:plan` | a consult agent trying to write the treatment plan | +| [`multi-tenant-saas`](../multi-tenant-saas/) | tenant orchestrator → export processor | EU parent holds `export:eea-only`; it cannot mint a child with `export:us` | a cross-border delegation the tenant never had authority to grant | +| [`industrial-embodied-ai`](../industrial-embodied-ai/) | cell orchestrator → robot-cell agent (its `delegation_chain` field is the hook) | the cell agent gets only `move:buffer-zone-1` | a motion request outside the granted zone | + +In each, the sensitive capability is granted high and withheld from the delegated scope, so cA2A's attenuation check is what stops a sub-agent from doing more than it was handed. Where a sub-agent is in a different trust domain (the SaaS export processor, a cross-hospital consult), the cA2A **sealed channel** (roadmap) is what would bind the task payload to that peer's attested measurement. + +--- + +## The tests + +`tests/test_delegation_scenario.py` checks that the chain verifies, that each hop is a subset of its parent, that the write authority is never delegated, and that the escalation attempt raises `ScopeEscalation`. + +```bash +python -m unittest discover -s tests -v +``` + +--- + +## License + +Apache 2.0. See [LICENSE](../LICENSE) in the repo root. diff --git a/ca2a-delegation/chain-output/credit-delegation-chain.json b/ca2a-delegation/chain-output/credit-delegation-chain.json new file mode 100644 index 0000000..e969a72 --- /dev/null +++ b/ca2a-delegation/chain-output/credit-delegation-chain.json @@ -0,0 +1,43 @@ +{ + "chain": [ + { + "credential_id": "credit-cred-0", + "issuer": "7f55fd29bc6632cc46c68910e44d8d0efdb46d6c0599e316ef693ffe07f1cc5b", + "subject": "cb3a37b9f41f452008e0077222a6850f5f8a6bada53b383a85e86c0dee1b7739", + "scope": [ + "read:bureau", + "read:documents", + "run:risk-model", + "screen:sanctions", + "write:risk-report" + ], + "depth": 0, + "parent_id": null, + "signature": "1c67f820a1acabb7821d8310cd85883da567eff2ceaecf678c8bb8dcd8fe06858157a8f40d35f11fd7e4b91ba2a29476bf495fbc2198bcaf9227746e51528500" + }, + { + "credential_id": "credit-cred-1", + "issuer": "cb3a37b9f41f452008e0077222a6850f5f8a6bada53b383a85e86c0dee1b7739", + "subject": "a399d0d49232152829593199ca2c74ff2b66ab669e6e02c1fdeaeb910f0ab1b5", + "scope": [ + "read:bureau", + "read:documents", + "screen:sanctions" + ], + "depth": 1, + "parent_id": "credit-cred-0", + "signature": "62fae79108808b023a95ed68c4ae328de6b4dcd30e11f9ed87cb61fb03fe46f350f2377b9aa2deac55d87da1ec975273b01b8961dc86a48a3b3f7674a2948a0f" + }, + { + "credential_id": "credit-cred-2", + "issuer": "a399d0d49232152829593199ca2c74ff2b66ab669e6e02c1fdeaeb910f0ab1b5", + "subject": "b93a9ce5014c6484d9861d9dc22cab731b0d6d21594c91edea8d11c1be65bfbf", + "scope": [ + "read:bureau" + ], + "depth": 2, + "parent_id": "credit-cred-1", + "signature": "4e8b5ac961f1d174297c726046069f87dac7a6894002ff385fdda5729912783615ae1e5857815377b6289dadde2897ab2f6a56dc8e548e968bc3f923b20dd501" + } + ] +} diff --git a/ca2a-delegation/chain-output/escalation-attempt.json b/ca2a-delegation/chain-output/escalation-attempt.json new file mode 100644 index 0000000..c9e5452 --- /dev/null +++ b/ca2a-delegation/chain-output/escalation-attempt.json @@ -0,0 +1,44 @@ +{ + "chain": [ + { + "credential_id": "credit-cred-0", + "issuer": "a8ea035f0d574cb3ace5c8e988fa10b972935887f266af109b725b909e91d243", + "subject": "e5c7125ecce92f4de683e32e4fa28b9d8ab87fe5a10ceb7de35077a8ab95c435", + "scope": [ + "read:bureau", + "read:documents", + "run:risk-model", + "screen:sanctions", + "write:risk-report" + ], + "depth": 0, + "parent_id": null, + "signature": "6a7bdb7d20800d7e415173449b6a93fddedacbe1e103a6add8e92e52ebd017ae7590aa498721002aea168cd1354f692e38249edb1654ce216f8302295b3ea000" + }, + { + "credential_id": "credit-cred-1", + "issuer": "e5c7125ecce92f4de683e32e4fa28b9d8ab87fe5a10ceb7de35077a8ab95c435", + "subject": "915f9a0266a99eb6c4d92dea2c8a476f53b8ff8ef77fc749dd935a353fc1721b", + "scope": [ + "read:bureau", + "read:documents", + "screen:sanctions" + ], + "depth": 1, + "parent_id": "credit-cred-0", + "signature": "34ee23de76780d88168a7f80abde7b5437aaa029073cd9c37978d7ff205d45c8c7fb12c3409dfeaff601fbbeb1f2ef7da93c087bc6c76cfc36b4aef0a9a88f07" + }, + { + "credential_id": "credit-cred-2", + "issuer": "915f9a0266a99eb6c4d92dea2c8a476f53b8ff8ef77fc749dd935a353fc1721b", + "subject": "a90c204fa79fc6a2f222ad5a9a5581d7cd1f7d47f0e65dba9a269627b275bc9f", + "scope": [ + "read:bureau", + "write:risk-report" + ], + "depth": 2, + "parent_id": "credit-cred-1", + "signature": "7bd69aa6ef7b6ce975b26aa65b15fdcd6c6df7c96a4fefb6ac6e12eea4a9a8d6020586de2e9e706c058906c1be50b9aaae3fb36f616b2ba4509f20664cc76708" + } + ] +} diff --git a/ca2a-delegation/delegation_agent.py b/ca2a-delegation/delegation_agent.py new file mode 100644 index 0000000..7ad5182 --- /dev/null +++ b/ca2a-delegation/delegation_agent.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +cA2A delegation demo for the credit-risk workflow. + +Builds a three-hop delegation chain (credit platform -> lead credit agent -> +screening sub-agent -> bureau connector), verifies it offline, then shows a +sub-agent trying to widen its scope and being rejected. No network, no TEE. + +Usage: + python delegation_agent.py +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import delegation_scenario as scenario # noqa: E402 +from ca2a_runtime.errors import CA2AError # noqa: E402 + +OUT_DIR = Path(__file__).resolve().parent / "chain-output" + + +def _write(document: dict, name: str) -> Path: + OUT_DIR.mkdir(exist_ok=True) + path = OUT_DIR / name + path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8") + return path + + +def show_valid_chain() -> None: + chain = scenario.build_credit_chain() + print("Building the credit delegation chain:") + for label, cred in zip(scenario.HOP_LABELS, chain): + print(f" [{cred.depth}] {label}") + print(f" scope: {sorted(cred.scope)}") + print() + + scenario.verify(chain) + leaf = sorted(chain[-1].scope) + withheld = sorted(scenario.CREDIT_CHAIN_SCOPES[0] - chain[1].scope) + print(f"verify_chain: verified=True hops={len(chain)} leaf_scope={leaf}") + print(f" authority withheld at the first delegation (never reaches a sub-agent): {withheld}") + path = _write(scenario.as_chain_document(chain), "credit-delegation-chain.json") + print(f" wrote {path.name}") + print() + + +def show_escalation_attempt() -> None: + chain = scenario.build_escalation_attempt() + print("Now the bureau connector tries to grant itself write:risk-report ...") + path = _write(scenario.as_chain_document(chain), "escalation-attempt.json") + try: + scenario.verify(chain) + print(" UNEXPECTED: the chain verified. This should not happen.") + sys.exit(1) + except CA2AError as exc: + code = getattr(exc, "code", "CA2A_ERROR") + print(f" verify_chain: verified=False code={code}") + print(f" reason: {exc}") + print(f" wrote {path.name}") + print() + + +def main() -> None: + print("=== cA2A: agent-to-agent delegation for the credit-risk workflow ===") + print() + show_valid_chain() + show_escalation_attempt() + print("The write:risk-report authority stays with the lead agent by construction:") + print("it is not in any delegated child's scope, so no descendant can regain it.") + print() + print("Verify either chain from the CLI (same four invariants):") + print(" ca2a verify-chain --chain chain-output/credit-delegation-chain.json") + + +if __name__ == "__main__": + main() diff --git a/ca2a-delegation/delegation_scenario.py b/ca2a-delegation/delegation_scenario.py new file mode 100644 index 0000000..951f86a --- /dev/null +++ b/ca2a-delegation/delegation_scenario.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +cA2A delegation scenario for the credit-risk workflow. + +cMCP governs the agent-to-tool boundary (what one agent may call). cA2A governs +the agent-to-agent boundary: when a lead agent hands part of a task to a +sub-agent, each hop carries a signed delegation credential whose scope is a +provable subset of its parent. This module builds that chain for the credit +scenario and a deliberately invalid variant that tries to widen scope. + +What runs here is the part of cA2A that is built today: attenuated delegation +credentials and offline chain verification (``ca2a_runtime.delegation``). The +live peer path (attesting an inbound peer, sealing the payload to its +measurement) and the per-hop TRACE provenance record are cA2A roadmap; see the +README. +""" + +from __future__ import annotations + +from typing import Any + +from ca2a_runtime.delegation import DelegationCredential, new_keypair, verify_chain + +# Capabilities in the credit workflow. The authority to WRITE the risk report +# is the sensitive one: it is granted to the lead agent at the root but is +# deliberately never delegated onward, so no sub-agent can regain it. +CAP_READ_DOCS = "read:documents" +CAP_SCREEN = "screen:sanctions" +CAP_READ_BUREAU = "read:bureau" +CAP_RUN_MODEL = "run:risk-model" +CAP_WRITE_REPORT = "write:risk-report" + +# depth 0: the credit platform grants the lead agent full assessment authority. +# depth 1: the lead agent delegates evidence-gathering to a screening sub-agent, +# withholding run:risk-model and write:risk-report (separation of duties). +# depth 2: the screening sub-agent delegates only the bureau pull to a connector. +CREDIT_CHAIN_SCOPES = [ + frozenset({CAP_READ_DOCS, CAP_SCREEN, CAP_READ_BUREAU, CAP_RUN_MODEL, CAP_WRITE_REPORT}), + frozenset({CAP_READ_DOCS, CAP_SCREEN, CAP_READ_BUREAU}), + frozenset({CAP_READ_BUREAU}), +] + +HOP_LABELS = [ + "credit-platform -> lead-credit-agent", + "lead-credit-agent -> screening-sub-agent", + "screening-sub-agent -> bureau-connector", +] + + +def _build(scopes: list[frozenset[str]]) -> list[DelegationCredential]: + """Sign a linear delegation chain over the given per-hop scopes. + + Continuity is preserved: each hop's issuer is the previous hop's subject. + """ + chain: list[DelegationCredential] = [] + priv, pub = new_keypair() + parent_id: str | None = None + for depth, scope in enumerate(scopes): + next_priv, next_pub = new_keypair() + cred = DelegationCredential( + credential_id=f"credit-cred-{depth}", + issuer=pub, + subject=next_pub, + scope=scope, + depth=depth, + parent_id=parent_id, + ).sign(priv) + chain.append(cred) + parent_id = cred.credential_id + priv, pub = next_priv, next_pub + return chain + + +def build_credit_chain() -> list[DelegationCredential]: + """A valid, attenuating credit delegation chain (verify_chain passes).""" + return _build(CREDIT_CHAIN_SCOPES) + + +def build_escalation_attempt() -> list[DelegationCredential]: + """An invalid chain: the bureau connector tries to grant itself + write:risk-report, which its parent (the screening sub-agent) never held. + verify_chain raises ScopeEscalation.""" + scopes = list(CREDIT_CHAIN_SCOPES) + scopes[2] = frozenset({CAP_READ_BUREAU, CAP_WRITE_REPORT}) + return _build(scopes) + + +def as_chain_document(chain: list[DelegationCredential]) -> dict[str, Any]: + """Serialize a chain to the {"chain": [...]} shape ca2a verify-chain reads.""" + return {"chain": [c.body() | {"signature": c.signature} for c in chain]} + + +def verify(chain: list[DelegationCredential]) -> None: + """Raise a ca2a_runtime error on the first invariant violation.""" + verify_chain(chain) diff --git a/ca2a-delegation/requirements.txt b/ca2a-delegation/requirements.txt new file mode 100644 index 0000000..2ca10f6 --- /dev/null +++ b/ca2a-delegation/requirements.txt @@ -0,0 +1,3 @@ +# cA2A is in alpha, so this is a pre-release. Install with: +# pip install --pre ca2a-runtime +ca2a-runtime>=0.1.0a1 diff --git a/ca2a-delegation/tests/test_delegation_scenario.py b/ca2a-delegation/tests/test_delegation_scenario.py new file mode 100644 index 0000000..0f2631d --- /dev/null +++ b/ca2a-delegation/tests/test_delegation_scenario.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +EXAMPLE_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLE_DIR)) + +import delegation_scenario as scenario # noqa: E402 +from ca2a_runtime.delegation.credential import ScopeEscalation # noqa: E402 + + +class DelegationScenarioTests(unittest.TestCase): + def test_valid_chain_verifies(self) -> None: + chain = scenario.build_credit_chain() + scenario.verify(chain) # raises on any violation + self.assertEqual(len(chain), 3) + self.assertEqual(sorted(chain[-1].scope), ["read:bureau"]) + + def test_each_hop_is_a_subset_of_its_parent(self) -> None: + chain = scenario.build_credit_chain() + for parent, child in zip(chain, chain[1:]): + self.assertTrue(child.scope <= parent.scope, f"{child.scope} not subset of {parent.scope}") + + def test_write_authority_is_never_delegated(self) -> None: + chain = scenario.build_credit_chain() + self.assertIn(scenario.CAP_WRITE_REPORT, chain[0].scope) # granted to the lead + for child in chain[1:]: + self.assertNotIn(scenario.CAP_WRITE_REPORT, child.scope) + + def test_continuity_issuer_is_previous_subject(self) -> None: + chain = scenario.build_credit_chain() + for parent, child in zip(chain, chain[1:]): + self.assertEqual(child.issuer, parent.subject) + + def test_escalation_attempt_is_rejected(self) -> None: + chain = scenario.build_escalation_attempt() + with self.assertRaises(ScopeEscalation) as ctx: + scenario.verify(chain) + self.assertEqual(getattr(ctx.exception, "code", None), "SCOPE_ESCALATION") + + def test_chain_document_shape(self) -> None: + doc = scenario.as_chain_document(scenario.build_credit_chain()) + self.assertIn("chain", doc) + self.assertEqual(len(doc["chain"]), 3) + for hop in doc["chain"]: + self.assertIn("signature", hop) + self.assertIn("credential_id", hop) + + +if __name__ == "__main__": + unittest.main() diff --git a/startup-tpm/README.md b/startup-tpm/README.md index 2a6533f..6ae33a3 100644 --- a/startup-tpm/README.md +++ b/startup-tpm/README.md @@ -235,6 +235,7 @@ cmcp verify claim.json --policy-hash sha256: --catalog-hash sha256: