Skip to content

feat(a2a): sign served AgentCards and verify consumed ones - #3111

Open
vvlrff wants to merge 5 commits into
mainfrom
feat/a2a-card-sign
Open

feat(a2a): sign served AgentCards and verify consumed ones#3111
vvlrff wants to merge 5 commits into
mainfrom
feat/a2a-card-sign

Conversation

@vvlrff

@vvlrff vvlrff commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Why are these changes needed?

A2A v1.0 defines JWS signatures on the AgentCard (the signatures field), and the
a2a-sdk ships the primitives for it (create_agent_card_signer /
create_signature_verifier). ag2 had no way to reach them: a server could not sign the
cards it served, and a client had no way to check the signature on a card it consumed, so
card discovery was trust-on-first-fetch. This PR wires both sides through, opt-in and
off by default.

Server — signing what we serve. A2AServer(card_signer=...) signs the public card
in all three transports (JSON-RPC, REST, gRPC) and signs the extended card when one is
configured. The subtlety is card modifiers: they mutate the card per request, and
mutating a signed card invalidates the JWS, so wrap_card_modifier /
wrap_extended_card_modifier re-sign the modifier's output instead of letting a stale
signature ship. Signing happens after clone_card_with_capabilities, so the
capability flags the server derives are inside the signed payload.

Client — verifying what we consume. A2AConfig(card_signature_verifier=...) (also
accepted directly by A2AClient) verifies every card the client takes in: the fetched
card, a preset_card supplied by the caller, and the extended card. A failure raises
the new A2ACardSignatureError, which carries the card URL plus a source field
("fetched agent card", "preset agent card", "extended agent card") so the failing
card is identifiable when several are in play. The SDK's SignatureVerificationError is
wrapped so callers only need to catch ag2's error type.

Both knobs default to None, which is a no-op — existing setups are unaffected. Note
the asymmetry once a verifier is set: the SDK verifier rejects unsigned cards, so
configuring verification makes signatures mandatory rather than best-effort. That is
deliberate (an attacker must not be able to downgrade to "no signature"), and it is
documented on A2AConfig.

The two type aliases are ag2.a2a.transports._common.CardSigner and
ag2.a2a.client.CardVerifier. Neither is re-exported from ag2.a2a, so callers pass the
SDK's callables positionally and annotate against the plain shapes
(Callable[[AgentCard], AgentCard] and Callable[[AgentCard], None]) — worth a reviewer
call on whether CardSigner should become public rather than living in a private module.

Dependency note. Signing is implemented in the SDK on top of PyJWT, which a2a-sdk
ships behind its own signing extra — ag2's a2a extra (a2a-sdk>=1.1.0,<2) does not
pull it in. Using the feature therefore needs pip install "a2a-sdk[signing]" alongside
ag2[a2a]. It passes in CI only because mcp depends on pyjwt[crypto] unconditionally
and is present in the dev environment.

Tests. test/a2a/test_card_signing.py covers the feature end to end with real
EC P-256 keypairs rather than fakes: served card is signed / stays unsigned without a
signer, a verifying client completes a real conversation, both modifier flavours re-sign
their output, and four rejection paths — unsigned card against a verifier, wrong key,
tampered preset_card, unsigned extended card.

Not included in this diff yet, called out so it is not mistaken for done: the A2A
user-guide pages under website/docs/user-guide/a2a/. A card_signing.mdx page plus
cross-references is written locally and will be pushed as a follow-up commit on this
branch; until then the behaviour is documented in the A2AConfig and A2AServer
docstrings only.

Related issue number

None — no existing issue tracks A2A card signing.

Checks

AI assistance

  • I understand the changes in this PR and can explain them in my own words.
  • I have verified that the PR description accurately reflects the actual diff.
  • If AI assistance was used, I reviewed, tested, and validated the generated code/text before submitting.

@vvlrff
vvlrff marked this pull request as draft July 28, 2026 19:59
@github-actions github-actions Bot added the area:a2a A2A protocol support (ag2/a2a) label Jul 28, 2026
@vvlrff vvlrff changed the title feat(tests): add comprehensive tests for card signing feat(a2a): sign served AgentCards and verify consumed ones Jul 28, 2026
@vvlrff
vvlrff marked this pull request as ready for review July 28, 2026 20:13
vvlrff and others added 3 commits July 28, 2026 23:24
Remove `CardSigner` and `CardVerifier` from the top-level `ag2.a2a`
package exports to clean up the public API. Update test imports to
reference these classes from their correct submodules.
Add comprehensive user guides for JWS-based card signing and verification.
This includes:
- A new `card_signing.mdx` guide explaining the "why" and "how" of
  securing AgentCards.
- Updates to the A2A overview, client, and server documentation to
  reflect new signing/verification capabilities.
- Documentation for the new `A2ACardSignatureError` exception.
- Updates to the documentation sidebar and navigation.
@github-actions github-actions Bot added the area:docs Documentation (website/) label Jul 29, 2026
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 56 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ag2/a2a/client.py 0.00% 17 Missing ⚠️
ag2/a2a/transports/_common.py 0.00% 15 Missing ⚠️
ag2/a2a/errors.py 0.00% 5 Missing ⚠️
ag2/a2a/transports/grpc.py 0.00% 5 Missing ⚠️
ag2/a2a/transports/jsonrpc.py 0.00% 5 Missing ⚠️
ag2/a2a/transports/rest.py 0.00% 5 Missing ⚠️
ag2/a2a/config.py 0.00% 3 Missing ⚠️
ag2/a2a/server.py 0.00% 1 Missing ⚠️
Files with missing lines Coverage Δ
ag2/a2a/server.py 0.00% <0.00%> (ø)
ag2/a2a/config.py 0.00% <0.00%> (ø)
ag2/a2a/errors.py 0.00% <0.00%> (ø)
ag2/a2a/transports/grpc.py 0.00% <0.00%> (ø)
ag2/a2a/transports/jsonrpc.py 0.00% <0.00%> (ø)
ag2/a2a/transports/rest.py 0.00% <0.00%> (ø)
ag2/a2a/transports/_common.py 0.00% <0.00%> (ø)
ag2/a2a/client.py 0.00% <0.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:a2a A2A protocol support (ag2/a2a) area:docs Documentation (website/)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant