From 65a07fed289b3a06ea8c2db8fcb5149e46a2418f Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Mon, 27 Jul 2026 17:24:41 -0700 Subject: [PATCH] fix: move the extension URI off a domain we do not own; correct stale caveats The cA2A extension URI was https://agentrust.io/extensions/ca2a/v0.1, and agentrust.io is not ours: it resolves to parked AWS addresses. That URI is precisely the string we are asking other operators to copy into their Agent Cards, so it must not depend on a domain we do not control. It moves to agentrust-io.com, along with the transport spec, the fixture, the maintainer email, and the test that pins the constant. A2A treats extension URIs as identifiers rather than fetchable URLs, so this is not a functional break, but a peer pinning the old string must update. Doing it before filing the A2A proposal is the point: after the proposal it would be someone else's problem too. Separately, two spec pages had gone stale against hardware-validation.md. profile.md P-6 still said the appraisal runs at assurance="none" and call-graph.md still called hardware appraisal "the remaining hardware step", when verify_offer had already returned assurance="hardware" off a live SEV-SNP quote. Both now say what is true, and both keep the limits that genuinely remain: the appraisal is one-directional, and the committed examples stay software-attested because real evidence embeds per-CPU identifiers. The tag:agentrust.io,2026:trace-v0.1 EAT profile identifier is deliberately untouched. It is a cross-repo spec identifier embedded in signed records, so it needs a transition decision rather than a rename. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 10 +++++++++- docs/spec/call-graph.md | 2 +- docs/spec/profile.md | 4 +++- docs/spec/transport.md | 14 +++++++------- examples/rejection-with-proof/chain.json | 18 +++++++++--------- examples/rejection-with-proof/dag.json | 14 +++++++------- pyproject.toml | 2 +- src/ca2a_runtime/transport/constants.py | 2 +- tests/fixtures/a2a/send_message_ca2a.json | 8 ++++---- tests/unit/test_a2a_adapter.py | 2 +- 10 files changed, 43 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 516b5d1..ffedb63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Extension URI moved to `https://agentrust-io.com/extensions/ca2a/v0.1`.** The previous `agentrust.io` host is not ours: it resolves to parked AWS addresses. An extension identifier is something we are asking other operators to copy into their Agent Cards, so it must not depend on a domain we do not control. A2A treats these URIs as identifiers rather than fetchable URLs, so nothing breaks functionally, but any peer pinning the old string must update. The adapter constant, the transport spec, the fixture, and the test that pins the constant all move together. + +### Fixed + +- Two spec pages contradicted `hardware-validation.md` after the live hardware run landed. `profile.md` P-6 still read "not yet met ... `assurance="none"`" and `call-graph.md` still called hardware appraisal "the remaining hardware step", when `verify_offer` had already returned `assurance="hardware"` off a live SEV-SNP quote on 2026-07-27. Both now state what is true, including the two limits that remain: the appraisal is one-directional (the caller appraised the callee, not the reverse), and the committed examples stay software-attested because genuine evidence embeds per-CPU identifiers. + ### Added - **A refusal is now evidence.** `enforce_peer_call` previously raised on an over-scoped call and emitted nothing, so a denial left no artifact and an auditor saw a gap where a hop should be. It now builds a linked denial record (`provenance.denial_record_for`) carrying the requested capability, the effective scope it fell outside, and the reason, and attaches it to `ScopeNotPermitted.record`. The call still fails closed; the record is evidence of the refusal, not a way to continue. `verify_dag` treats a denial as terminal and rejects any chain that continues past a refused hop, `cross_check_chain` matches only hop records positionally against the chain while still requiring a denial to name a credential that is in it, and `ca2a verify-dag` reports `outcome: denied` with the requested capability and effective scope. Denial fields are omitted from an allow record's hashed body, so existing allow-record hashes are unchanged. @@ -25,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A2A transport adapter (`ca2a_runtime.transport`): parse/attach cA2A extension metadata on A2A `SendMessage`-shaped messages into `PeerRequest` (and the - reverse). Extension URI `https://agentrust.io/extensions/ca2a/v0.1`. Fail closed + reverse). Extension URI `https://agentrust-io.com/extensions/ca2a/v0.1`. Fail closed on malformed cA2A metadata; absence of all cA2A keys returns `None` (ordinary A2A). The adapter itself adds no HTTP serving or seal-to-verified-measurement binding; the reference transport below adds serving, and hardware measurement diff --git a/docs/spec/call-graph.md b/docs/spec/call-graph.md index 73b9e8a..00d36e7 100644 --- a/docs/spec/call-graph.md +++ b/docs/spec/call-graph.md @@ -78,7 +78,7 @@ A capability is granted only when it is both delegated down the chain and allowe ## Step 4: seal the payload to the peer's attested key (crypto implemented) -Once the peer's attested public key is known (from its report, step 2), the task payload is sealed to it so only the holder of the peer's private key can open it. The channel is implemented (`SealedChannel(peer_pub).seal(...)` and `open_sealed(...)`, an HPKE-style X25519 -> HKDF-SHA256 -> ChaCha20-Poly1305 scheme). `open_sealed` fails closed with `SEALED_CHANNEL_ERROR` on a wrong key or tampered ciphertext. On a live call the handshake (`ca2a_runtime.attestation.verify_offer`) now gates the seal on a channel key the caller has appraised under a fresh nonce, but in software mode that appraisal is `assurance="none"`. The property that the payload decrypts *only inside the attested measurement* rests on the private key being enclave-bound and on a hardware-verified measurement, which is the remaining hardware step. See [sealed-channel.md](sealed-channel.md). +Once the peer's attested public key is known (from its report, step 2), the task payload is sealed to it so only the holder of the peer's private key can open it. The channel is implemented (`SealedChannel(peer_pub).seal(...)` and `open_sealed(...)`, an HPKE-style X25519 -> HKDF-SHA256 -> ChaCha20-Poly1305 scheme). `open_sealed` fails closed with `SEALED_CHANNEL_ERROR` on a wrong key or tampered ciphertext. On a live call the handshake (`ca2a_runtime.attestation.verify_offer`) gates the seal on a channel key the caller has appraised under a fresh nonce. As of 2026-07-27 that appraisal has run at `assurance="hardware"` off a live SEV-SNP quote, so the property that the payload decrypts *only inside the attested measurement* now rests on a hardware-verified measurement rather than on a software-mode stand-in. It holds one-directionally: the caller appraised the callee, and mutual simultaneous attestation is still outstanding. In software mode the appraisal remains `assurance="none"`, which is what the committed examples exercise. See [sealed-channel.md](sealed-channel.md). ## Step 5: emit a linked provenance record (implemented) diff --git a/docs/spec/profile.md b/docs/spec/profile.md index 0917959..38d98db 100644 --- a/docs/spec/profile.md +++ b/docs/spec/profile.md @@ -55,7 +55,9 @@ A callee MUST compute the effective scope as the leaf credential's delegated sco Before accepting a delegated task across a trust boundary, a peer SHOULD be required to present attestation evidence binding its channel key to a measured runtime, and the caller MUST appraise that evidence against its expected measurement before sealing anything to that key. A caller that cannot appraise the evidence MUST NOT treat the peer as attested. -> Not yet met by this implementation. The handshake gates the seal on an appraised channel key, but the appraisal runs in software mode at `assurance="none"`. See [attestation](attestation.md) and [hardware-validation](../hardware-validation.md). +> Met one-directionally as of 2026-07-27. Driven off a live SEV-SNP quote on a running confidential VM, `verify_offer` returned `assurance="hardware"` and a payload was sealed to a channel key that a hardware-verified measurement vouches for. A cross-operator, cross-TEE run followed: an Azure SEV-SNP peer calling a GCP Intel TDX peer. +> +> Two limits remain, and P-6 is not fully met until both close. The appraisal is **one-directional**: the caller appraised the callee, not the reverse, so neither side has yet proven itself to a peer that is simultaneously proving itself back. And the committed harness in `examples/` still runs software-attested against synthetic vectors, because genuine evidence embeds per-CPU identifiers and is not shippable as a fixture. See [attestation](attestation.md) and [hardware-validation](../hardware-validation.md). ### P-7 Sealed payload diff --git a/docs/spec/transport.md b/docs/spec/transport.md index 92b0a99..613db09 100644 --- a/docs/spec/transport.md +++ b/docs/spec/transport.md @@ -32,18 +32,18 @@ Per [A2A v1.0 extensions](https://a2a-protocol.org/v1.0.0/topics/extensions/), a | Item | Value | |---|---| -| Extension URI | `https://agentrust.io/extensions/ca2a/v0.1` | -| Opt-in header | `A2A-Extensions: https://agentrust.io/extensions/ca2a/v0.1` | +| Extension URI | `https://agentrust-io.com/extensions/ca2a/v0.1` | +| Opt-in header | `A2A-Extensions: https://agentrust-io.com/extensions/ca2a/v0.1` | Namespaced keys on A2A `metadata` (message and/or params): | Metadata key | JSON type | Meaning | |---|---|---| -| `https://agentrust.io/extensions/ca2a/v0.1/delegation_chain` | array of credential objects | Root-to-leaf delegation chain | -| `https://agentrust.io/extensions/ca2a/v0.1/requested_capability` | string | Capability the callee must grant | -| `https://agentrust.io/extensions/ca2a/v0.1/record_id` | string | Provenance record id for this hop | -| `https://agentrust.io/extensions/ca2a/v0.1/parent_record_hash` | string or `null` | Parent TRACE/provenance hash; `null` for a root hop | -| `https://agentrust.io/extensions/ca2a/v0.1/sealed_payload` | string (base64url) or omitted | Opaque sealed ciphertext only, not a verified measurement binding | +| `https://agentrust-io.com/extensions/ca2a/v0.1/delegation_chain` | array of credential objects | Root-to-leaf delegation chain | +| `https://agentrust-io.com/extensions/ca2a/v0.1/requested_capability` | string | Capability the callee must grant | +| `https://agentrust-io.com/extensions/ca2a/v0.1/record_id` | string | Provenance record id for this hop | +| `https://agentrust-io.com/extensions/ca2a/v0.1/parent_record_hash` | string or `null` | Parent TRACE/provenance hash; `null` for a root hop | +| `https://agentrust-io.com/extensions/ca2a/v0.1/sealed_payload` | string (base64url) or omitted | Opaque sealed ciphertext only, not a verified measurement binding | Constants and helpers live in `ca2a_runtime.transport`. diff --git a/examples/rejection-with-proof/chain.json b/examples/rejection-with-proof/chain.json index 40e7f33..6ee96aa 100644 --- a/examples/rejection-with-proof/chain.json +++ b/examples/rejection-with-proof/chain.json @@ -2,8 +2,8 @@ "chain": [ { "credential_id": "cred-0-orchestrator", - "issuer": "30831ec7f2a3abdcc223d22dc89311e3e8870cfbea931149d50e07077d518e9b", - "subject": "3f542267fb293071807ea37ac01a1be5f4b4bd9823f10156f1c998545fb2abca", + "issuer": "0aca4cf1251cb3648cd1f8605481d8dece5cd3a106a362a11b86f43d270da087", + "subject": "41eebfb4e9ac7f0944041b6c8fad485159f3d0391721dcec1bbe478986c6879f", "scope": [ "task:read", "task:write", @@ -12,12 +12,12 @@ ], "depth": 0, "parent_id": null, - "signature": "493358a97965be0a78a81cc832028dd494e65a16d7622842661080b08af7b800f4c71e267c4d2745485964504209f836f93d1923d10453ba6375427b68399803" + "signature": "2380d30571a6493f6236c3aa9d381fd28a9cd3fcd4fa0fd5316166867575b206d8eb882c39fc7362c89cd407eef77848c228410a7816a10b563888eb99789a08" }, { "credential_id": "cred-1-researcher", - "issuer": "3f542267fb293071807ea37ac01a1be5f4b4bd9823f10156f1c998545fb2abca", - "subject": "f327db8ed54be433ee43fec9f4c144d3f0d84555eb4654989c4458087e23b99d", + "issuer": "41eebfb4e9ac7f0944041b6c8fad485159f3d0391721dcec1bbe478986c6879f", + "subject": "cb42a0b6b9ec7839846a0ddbcb67d851d9b1013fc04066687181938d9ea7dce8", "scope": [ "task:read", "tool:purchase", @@ -25,18 +25,18 @@ ], "depth": 1, "parent_id": "cred-0-orchestrator", - "signature": "35ba31dccb83829c4c8f9f4215aeb19b49aafd0033aef1d5ebb876a8a3d0110797f6f5a013ab55171a969c4940a1288b1591d6a090edf5df6b0a8b4db8b08300" + "signature": "ad8d7b0aa00bd04e45bd9dca84f92bd7f23b4f4dde9e6eeff441ccf7c5222c5a0ccb45b5ba1b8399b6625cd3b4cfd97bc953b16bba01034a05ae01d55ba50503" }, { "credential_id": "cred-2-retriever", - "issuer": "f327db8ed54be433ee43fec9f4c144d3f0d84555eb4654989c4458087e23b99d", - "subject": "555d4389edcb44043216dbce5079bd61edaa5873f0674b8c57b4c7b79f441cd5", + "issuer": "cb42a0b6b9ec7839846a0ddbcb67d851d9b1013fc04066687181938d9ea7dce8", + "subject": "2014e45331bb1717e9983dbba3fd4b6adc81ee48257afe5c0e027f12f61e8d7d", "scope": [ "tool:search" ], "depth": 2, "parent_id": "cred-1-researcher", - "signature": "f134bd6fd0f55573c285dc42debaf2944360d060ded3596d2ba85f6a0a223cbd7d99a7a111cf358793f01ca2454f3d7e301021b3789f84b6e90553e726d34904" + "signature": "0a18f20de83626e8f32960f3a3a543fed07b4c9f1ccce1eec4e3a6b7798311b68d08052ec408a675cd153fbfb9594d369c6333b672d2c1ba16854c517ebebf05" } ] } diff --git a/examples/rejection-with-proof/dag.json b/examples/rejection-with-proof/dag.json index 0fc7534..76de619 100644 --- a/examples/rejection-with-proof/dag.json +++ b/examples/rejection-with-proof/dag.json @@ -3,7 +3,7 @@ { "record_id": "rec-0-orchestrator", "credential_id": "cred-0-orchestrator", - "subject": "3f542267fb293071807ea37ac01a1be5f4b4bd9823f10156f1c998545fb2abca", + "subject": "41eebfb4e9ac7f0944041b6c8fad485159f3d0391721dcec1bbe478986c6879f", "scope": [ "task:read", "task:write", @@ -15,31 +15,31 @@ { "record_id": "rec-1-researcher", "credential_id": "cred-1-researcher", - "subject": "f327db8ed54be433ee43fec9f4c144d3f0d84555eb4654989c4458087e23b99d", + "subject": "cb42a0b6b9ec7839846a0ddbcb67d851d9b1013fc04066687181938d9ea7dce8", "scope": [ "task:read", "tool:purchase", "tool:search" ], - "parent_record_hash": "d2b7837dc31713ee5ce1adac414e787f88a42e83663ac4c333318127ff4606b5" + "parent_record_hash": "143db9ccd89d3cf4c50426e5e8dedc32e7feac0bbc2735f63b605bba45eef613" }, { "record_id": "rec-2-retriever", "credential_id": "cred-2-retriever", - "subject": "555d4389edcb44043216dbce5079bd61edaa5873f0674b8c57b4c7b79f441cd5", + "subject": "2014e45331bb1717e9983dbba3fd4b6adc81ee48257afe5c0e027f12f61e8d7d", "scope": [ "tool:search" ], - "parent_record_hash": "3dac51b23f24cde0396bc9bc3ae0f06aaf7bf13c9e40800f24d499869beee8e4" + "parent_record_hash": "2e0ac019c2a7949a0cb5c8eab207ddef3351405db20dd4360144b6c4e605c6a0" }, { "record_id": "rec-denied-purchase", "credential_id": "cred-2-retriever", - "subject": "555d4389edcb44043216dbce5079bd61edaa5873f0674b8c57b4c7b79f441cd5", + "subject": "2014e45331bb1717e9983dbba3fd4b6adc81ee48257afe5c0e027f12f61e8d7d", "scope": [ "tool:search" ], - "parent_record_hash": "6b10750d625c1cff83b5868948eeb7a6ebdbbe427ce3ba2bc802c258c8cdc82d", + "parent_record_hash": "c8ce42ebbbe084bbe88571a12576d8a4c1d5d3753d071e35dd19838274605f09", "decision": "deny", "requested_capability": "tool:purchase", "effective_scope": [ diff --git a/pyproject.toml b/pyproject.toml index 7c1afeb..888d70c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Confidential agent-to-agent runtime: attested, attenuated delegat readme = "README.md" license = { text = "MIT" } authors = [ - { name = "AgenTrust Contributors", email = "oss@agentrust.io" }, + { name = "AgenTrust Contributors", email = "oss@agentrust-io.com" }, ] keywords = ["a2a", "agent-to-agent", "delegation", "tee", "attestation", "confidential-computing", "ai-agents"] classifiers = [ diff --git a/src/ca2a_runtime/transport/constants.py b/src/ca2a_runtime/transport/constants.py index bf6b9f3..f4b4d4a 100644 --- a/src/ca2a_runtime/transport/constants.py +++ b/src/ca2a_runtime/transport/constants.py @@ -8,7 +8,7 @@ from __future__ import annotations -EXTENSION_URI = "https://agentrust.io/extensions/ca2a/v0.1" +EXTENSION_URI = "https://agentrust-io.com/extensions/ca2a/v0.1" KEY_DELEGATION_CHAIN = f"{EXTENSION_URI}/delegation_chain" KEY_REQUESTED_CAPABILITY = f"{EXTENSION_URI}/requested_capability" diff --git a/tests/fixtures/a2a/send_message_ca2a.json b/tests/fixtures/a2a/send_message_ca2a.json index f2b3f8a..54d4018 100644 --- a/tests/fixtures/a2a/send_message_ca2a.json +++ b/tests/fixtures/a2a/send_message_ca2a.json @@ -13,10 +13,10 @@ ], "metadata": { "https://example.com/ext/unrelated/v1/note": "preserved by non-cA2A peers", - "https://agentrust.io/extensions/ca2a/v0.1/delegation_chain": [], - "https://agentrust.io/extensions/ca2a/v0.1/requested_capability": "read", - "https://agentrust.io/extensions/ca2a/v0.1/record_id": "rec-fixture", - "https://agentrust.io/extensions/ca2a/v0.1/parent_record_hash": null + "https://agentrust-io.com/extensions/ca2a/v0.1/delegation_chain": [], + "https://agentrust-io.com/extensions/ca2a/v0.1/requested_capability": "read", + "https://agentrust-io.com/extensions/ca2a/v0.1/record_id": "rec-fixture", + "https://agentrust-io.com/extensions/ca2a/v0.1/parent_record_hash": null } } } diff --git a/tests/unit/test_a2a_adapter.py b/tests/unit/test_a2a_adapter.py index 9cca599..6d85b6d 100644 --- a/tests/unit/test_a2a_adapter.py +++ b/tests/unit/test_a2a_adapter.py @@ -241,5 +241,5 @@ def test_sealed_payload_is_opaque_bytes_only() -> None: def test_extension_uri_is_stable() -> None: - assert EXTENSION_URI == "https://agentrust.io/extensions/ca2a/v0.1" + assert EXTENSION_URI == "https://agentrust-io.com/extensions/ca2a/v0.1" assert KEY_DELEGATION_CHAIN.startswith(EXTENSION_URI)