feat(transport): A2A extension adapter for PeerRequest#48
Conversation
Add ca2a_runtime.transport to map A2A SendMessage metadata to PeerRequest (and reverse) under https://agentrust.io/extensions/ca2a/v0.1. Fail closed on malformed cA2A fields; absence of all keys returns None (ordinary A2A). Docs and LIMITATIONS keep the claim boundary: no live server or attestation. Signed-off-by: Susan Poudel <susanpdl77@gmail.com>
carloshvp
left a comment
There was a problem hiding this comment.
Requesting changes on one transport-boundary issue.
The new adapter promises that malformed cA2A metadata fails closed with TRANSPORT_ERROR, but sealed_payload currently uses base64.urlsafe_b64decode() without alphabet validation. Python's default decoder can ignore some non-base64url characters, so malformed extension metadata can be accepted and converted into opaque bytes instead of failing closed.
Repro against this PR head:
PYTHONPATH=src python3 - <<'PY'
from ca2a_runtime.transport import (
KEY_DELEGATION_CHAIN,
KEY_PARENT_RECORD_HASH,
KEY_RECORD_ID,
KEY_REQUESTED_CAPABILITY,
KEY_SEALED_PAYLOAD,
parse_peer_request,
)
from tests.unit.test_a2a_adapter import _cred_dicts
meta = {
KEY_DELEGATION_CHAIN: _cred_dicts([frozenset({"read", "write"}), frozenset({"read"})]),
KEY_REQUESTED_CAPABILITY: "read",
KEY_RECORD_ID: "rec-invalid-b64",
KEY_PARENT_RECORD_HASH: None,
KEY_SEALED_PAYLOAD: "abcd?",
}
try:
req = parse_peer_request({"metadata": meta})
print("ACCEPTED", req.sealed_payload)
except Exception as exc:
print("RAISED", type(exc).__name__, str(exc), getattr(exc, "code", None))
PYActual result: ACCEPTED b'i\xb7\x1d'.
That conflicts with the PR's fail-closed contract for any present-but-malformed cA2A metadata, and it leaves the parser accepting a value that is not actually base64url. Please make the decode strict, for example by validating the URL-safe alphabet/padding before decoding or by translating to standard base64 and using base64.b64decode(..., validate=True), and add a regression test for a character outside the base64url alphabet such as abcd?.
Validation performed: inspected the PR diff, ran ruff check src/ tests/ successfully, ran pytest tests/unit/test_a2a_adapter.py tests/unit/test_errors.py successfully, reproduced the malformed sealed_payload acceptance above. mypy src/ca2a_runtime/ src/ca2a_verify/ did not complete in my local temp checkout because types-PyYAML is not installed; pytest tests/unit/ also did not complete locally because cedarpy is not installed. GitHub CI for the PR is green except the maintainer-approval gate.
base64.urlsafe_b64decode silently ignores characters outside the alphabet, so a value like "abcd?" was accepted as opaque bytes instead of failing closed. Validate the base64url alphabet before decoding and add regression tests for characters outside it. Signed-off-by: Susan Poudel <susanpdl77@gmail.com>
|
Good catch, thanks. Fixed in 9e5a2b7.
Added a parametrized regression test covering characters outside the alphabet: |
carloshvp
left a comment
There was a problem hiding this comment.
Requesting changes on one remaining adapter-shape issue.
The strict sealed_payload fix on this head addresses my previous blocker. The remaining problem is in attach_ca2a_metadata(): it only writes to out["metadata"]. If a caller passes the full A2A JSON-RPC message/send / SendMessage envelope that parse_peer_request() explicitly accepts, the helper attaches cA2A fields at the envelope root instead of under params.message.metadata or params.metadata. That means the helper can produce an object that the companion parser immediately treats as ordinary A2A.
Repro against 9e5a2b76ce40c523ac3599edb2cd37918f1f01dd:
PYTHONPATH=src python3 - <<'PY'
from ca2a_runtime.peer import PeerRequest
from ca2a_runtime.transport import attach_ca2a_metadata, parse_peer_request, KEY_DELEGATION_CHAIN
from tests.unit.conftest import build_chain
request = PeerRequest(
chain=build_chain([frozenset({"read", "write"}), frozenset({"read"})]),
requested_capability="read",
record_id="rec-envelope",
sealed_payload=None,
parent_record_hash=None,
)
envelope = {
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"messageId": "msg-1",
"role": "user",
"parts": [{"text": "task"}],
"metadata": {"https://example.com/ext/other/v1/note": "keep"},
}
},
}
attached = attach_ca2a_metadata(envelope, request)
print("top_level_has_metadata", "metadata" in attached)
print("message_has_ca2a", KEY_DELEGATION_CHAIN in attached["params"]["message"].get("metadata", {}))
print("parse_attached_returns", parse_peer_request(attached))
PYActual result:
top_level_has_metadata True
message_has_ca2a False
parse_attached_returns None
That is a transport-boundary bug, not just a test-shape nit: the PR advertises a parse/attach helper for A2A SendMessage-shaped data, but the attach half can silently drop the cA2A envelope from the actual A2A metadata location. Please either make attach_ca2a_metadata() mirror collect_metadata()'s supported shapes and attach under params.message.metadata / params.metadata for envelopes, or narrow the API/docs/tests so it only accepts a message object and fails closed on a JSON-RPC envelope instead of producing a misleading root-level metadata field. Add a round-trip regression for the full envelope shape.
Validation performed: inspected the updated PR diff; ran ruff check src/ tests/ successfully; ran pytest tests/unit/test_a2a_adapter.py tests/unit/test_errors.py -q successfully; reproduced the envelope round-trip failure above in a temp checkout. GitHub CI is green except the maintainer-approval gate.
attach_ca2a_metadata only wrote to the root metadata, so a full JSON-RPC message/send envelope got cA2A fields at the root while parse_peer_request reads params.message.metadata / params.metadata. The result was an object the parser treated as ordinary A2A. Attach now mirrors collect_metadata's shapes so attach then parse round-trips for both bare messages and envelopes. Add round-trip regressions for the envelope shapes. Signed-off-by: Susan Poudel <susanpdl77@gmail.com>
|
Fixed in a321bb6.
Your repro now prints: Added round-trip regressions for the full |
carloshvp
left a comment
There was a problem hiding this comment.
I rechecked a321bb6 against both earlier findings. The strict decoder rejects sealed_payload="abcd?" with TRANSPORT_ERROR, and attach_ca2a_metadata() places cA2A fields under params.message.metadata so a full SendMessage envelope round-trips through parse_peer_request().
The final diff matches the #47 acceptance bar. The adapter preserves unrelated metadata and A2A routing/payload fields. The docs keep HTTP serving and seal-to-verified-measurement binding outside this PR’s claims.
Validation on this head:
ruff check src/ tests/mypy src/ca2a_runtime/ src/ca2a_verify/pytest tests/unit/ tests/conformance/(178 passed)- direct reproductions for both prior blockers
I found no remaining blocking issues.
|
@imran-siddique friendly ping: @carloshvp approved the latest head ( |
imran-siddique
left a comment
There was a problem hiding this comment.
Approving and merging. Carlos raised two transport-boundary blockers (base64url alphabet validation on sealed_payload, and attach_ca2a_metadata writing to the wrong envelope level). The author fixed both, and Carlos re-reviewed a321bb6 and confirmed: the strict decoder rejects malformed sealed_payload with TRANSPORT_ERROR, and cA2A fields now round-trip under params.message.metadata through parse_peer_request. Matches the #47 acceptance bar. Thanks Susanpdl and Carlos.
#48 added the A2A profile adapter; this branch adds a reference HTTP server/client, the attestation handshake, and PeerNode. The scope docs still said the A2A transport was flatly out of scope and that nothing served a live inbound call, which is now false in software mode. Reconcile honestly, keeping the transport in core (a reference, not the profile) and preserving the software-vs-hardware boundary: - LIMITATIONS: reference transport + handshake are built (software mode); the remaining gap is the hardware verifier seam, not live serving. Reframe the out-of-scope bullet: the profile mandates no transport, a non-normative reference one ships. - transport.md: status table and the overlay claim distinguish the profile (no new endpoint) from the reference transport (which does expose endpoints). - call-graph.md, concepts.md: the live path now runs in software mode; the hardware quote via the verifier seam is what remains. - CHANGELOG: record the reference transport under Unreleased. No code change: the transport already layers cleanly on #48's adapter after the merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sport) (#53) * feat(transport): live-call wiring (attestation handshake and A2A transport) Compose the landed decision core (peer.handle_peer_request), Cedar policy, sealed channel, and provenance DAG into a runnable live call, closing the two v0.2 "Remaining" items in ROADMAP: wire the decision core to a live A2A transport, and bind the seal to a verified attestation report on a live call. - attestation.py: handshake that gates the sealed channel on a verified channel key (offer, verify, seal). Software mode records assurance="none"; hardware plugs in via an injected verifier callable, so this module carries no hardware dependency. - transport/a2a.py: A2A wire binding. Parse an A2A message's ca2a block into a PeerRequest, build that message, and serialize result, error, and channel offer. - node.py: PeerNode holding the enclave channel key, policy, and provider. - transport/server.py, transport/client.py: standard-library-only reference HTTP server and client that run the full inbound pipeline over the wire. - tee/software.py: explicit, never-auto-selected software provider (no hardware guarantee), so the path runs off confidential hardware. - tests/unit/test_live_call.py: in-process pipeline, HTTP end-to-end, and fail-closed on over-scope, tampered payload, and stale nonce. Software mode runs off confidential hardware; the real-hardware quote path is the verifier seam. ruff, mypy --strict, and the full unit suite (136 tests) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: reconcile scope with the shipped reference transport #48 added the A2A profile adapter; this branch adds a reference HTTP server/client, the attestation handshake, and PeerNode. The scope docs still said the A2A transport was flatly out of scope and that nothing served a live inbound call, which is now false in software mode. Reconcile honestly, keeping the transport in core (a reference, not the profile) and preserving the software-vs-hardware boundary: - LIMITATIONS: reference transport + handshake are built (software mode); the remaining gap is the hardware verifier seam, not live serving. Reframe the out-of-scope bullet: the profile mandates no transport, a non-normative reference one ships. - transport.md: status table and the overlay claim distinguish the profile (no new endpoint) from the reference transport (which does expose endpoints). - call-graph.md, concepts.md: the live path now runs in software mode; the hardware quote via the verifier seam is what remains. - CHANGELOG: record the reference transport under Unreleased. No code change: the transport already layers cleanly on #48's adapter after the merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transport): reject non-HTTP(S) URLs in the reference client (bandit B310) bandit flagged B310 on the reference client's two urllib.request.urlopen calls (file:/ and custom schemes). The existing `# noqa: S310` only silenced ruff, not bandit, so `bandit -r src/` failed CI. Validate the URL scheme against an http/https allowlist before opening, failing closed with TransportError, and annotate the calls for both tools. Add a test for the rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
ca2a_runtime.transportto parse A2ASendMessagemetadata intoPeerRequest, and attach the same fields on the way out.https://agentrust.io/extensions/ca2a/v0.1. Malformed cA2A metadata fails closed (TRANSPORT_ERROR); no cA2A keys means ordinary A2A (None).docs/spec/transport.mdandLIMITATIONS.mdso this is clearly parse/attach only. No HTTP server,ca2a start, or seal-to-verified-measurement binding.Closes the first Tier 2 checkbox on #47 (adapter only), matching the acceptance notes from @carloshvp.
Test plan
ruff check src/ tests/mypy src/ca2a_runtime/ src/ca2a_verify/pytest tests/unit/(141 passed locally)