feat(transport): bridge to the official a2a-sdk (#91) - #95
Merged
Conversation
cA2A calls itself a profile on A2A and integrated with no A2A implementation. transport.a2a_adapter parsed A2A-shaped dicts and transport.server was a bespoke stdlib HTTP server; both are honest about being a reference, but a team already running the official SDK could only adopt the profile by replacing their transport with ours, which nobody does to try an alpha. The bridge is thin on purpose. The SDK carries A2A metadata as a google.protobuf.Struct, so converting it to a plain mapping hands the existing adapter what it already parses: one parser, one set of tests, and the profile stays transport-agnostic. Optional extra; the base install still depends on no A2A implementation. The protobuf round trip nearly broke every chain. Struct has no integer type, so a credential depth of 0 arrives as 0.0, and credential signatures cover RFC 8785 canonical bytes where canonicalize refuses floats outright. Chains verify because from_dict coerces depth with int() before anything is canonicalized, so the bytes verified are the integer form the signer signed; a non-integral float coerces to a different integer and the signature then fails. Both directions are tested against the real SDK over multi-hop chains, so the non-zero depths actually cross the boundary rather than only depth 0. The SDK is in the dev extra, not just the optional one, so those tests run in CI instead of skipping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #91, the P0 adoption item.
The gap
cA2A calls itself a profile on A2A and integrated with no A2A implementation:
transport.a2a_adapterhand-parsed A2A-shaped dicts;transport.serverwas a bespoke stdlib HTTP server. Both are honest about being a reference, but the practical effect was that a team already on the official SDK could only adopt the profile by replacing their transport with ours — which nobody does to try an alpha. A2A hit v1.0 in April 2026 under the LF with SDKs in six languages, wired into Google ADK, Azure AI Foundry, Bedrock AgentCore and Copilot Studio. The profile reached none of it.The bridge
ca2a_runtime.transport.a2a_sdk, deliberately thin. The SDK carriesmetadataas agoogle.protobuf.Struct, so converting it to a plain mapping hands the existing adapter exactly what it already parses:One parser, one set of tests, and the profile stays transport-agnostic. Nothing in the module verifies, enforces, or appraises — it converts and delegates. Optional extra (
pip install 'ca2a[a2a-sdk]'); the base install still depends on no A2A implementation.Also
attach_to_sdk_messagefor the outbound direction (which appends the extension URI toMessage.extensions, idempotently) andopted_in()for theA2A-Extensionsheader.Structhas no integer type. A credential'sdepthof0comes back as0.0:And credential signatures cover the RFC 8785 canonical bytes of the body, where our canonicalizer refuses floats outright:
So the naive bridge would have broken every chain it carried. It works because
DelegationCredential.from_dictcoercesdepthwithint()before anything is canonicalized, so the bytes that get verified are the integer form the signer signed.I checked the other direction too, since a coercion that silently accepts anything would be worse than a crash: a non-integral float cannot be smuggled past the signature. Rewriting a signed depth of
1to2.5coerces to2, the canonical bytes differ from the signed ones, and verification fails withInvalidCredential. The test deliberately uses2.5rather than1.5, because1.5coerces back to the signed1and would prove nothing.Tested over multi-hop chains, so non-zero depths actually cross the boundary — a one-hop chain only ever exercises depth 0, the value least likely to expose a float problem. (My first attempt hand-set
depthon a one-hop chain and failed on the unrelated "root credential must have depth 0" rule, which is why the helper now builds real chains.)Tests run in CI, not skip
The SDK is in the
devextra as well as the optional one. A bridge whose tests only ever skip is a bridge nobody has exercised — which is exactly the failure mode I found in agent-manifest today, where 12 post-quantum tests had been reporting as skips because their dependency was unpublishable.22 new tests against the real SDK 1.1.2, including the full inbound pipeline and mutual attestation over the bridge. Suite: 389 passed, 3 skipped.
ruff check,ruff format --checkandmypyclean.Not in this PR
Publishing the extension URI in the Agent Card (#92). The SDK's
find_extension_by_uriandAgentExtensionmake that straightforward now, and the decision to advertiserequire_caller_attestationinAgentExtension.paramsis recorded there.🤖 Generated with Claude Code