A2A conformance¶
Where the A2A implementation in core/agents-interop/.../a2a stands against the Agent2Agent
specification, requirement by requirement. The machine-checked capability rows (are the
files and symbols there) live in docs/api/conformance.md, generated
by scripts/ci/conformance-report.py; this page is the judgement layer on top: what the spec
requires, what we do, and where we deviate.
- Spec read: A2A 1.0.0 (latest release; earlier: 0.3.0, 0.2.6, 0.1.0), the HTML
specification at https://a2a-protocol.org/latest/specification/, on 2026-10-14. Version
identifiers are
Major.Minor(spec 3.6). - Bindings the spec defines: JSON-RPC (section 9), gRPC (section 10), HTTP+JSON/REST (section 11), plus custom bindings (section 12). Section 5.1: every supported binding MUST offer identical functionality.
- Task states (4.1.3):
SUBMITTED,WORKING,COMPLETED,FAILED,CANCELED,INPUT_REQUIRED,REJECTED,AUTH_REQUIRED(plusUNSPECIFIED). Terminal: completed, failed, canceled, rejected. Interrupted: input-required, auth-required.
How this was verified, and its limits, are in What was not verified.
Summary¶
| Area | Status |
|---|---|
| Agent Card (discovery, required fields, interfaces) | Met |
| Card signing (JWS) | Partial - works, deviates from 8.4 in three ways |
| JSON-RPC binding, 4 of 11 operations | Partial (by design, advertised honestly) |
| HTTP+JSON/REST binding, 4 of 11 operations | Partial (new, same four operations) |
| gRPC binding | Not implemented - decision needed, see gRPC proposal |
| Task state machine | Partial - 5 of 8 states |
| Content parts | Partial - text only |
| Errors and versioning | Met for JSON-RPC; REST has one documented deviation |
The generated table agrees on these two. docs/api/conformance.md is produced by
scripts/ci/conformance-report.py, which checks that the source contains evidence for each row. Its ListTasks and
SubscribeToTask rows used to read partial and implemented because a plain search for the operation name also
found the dispatcher arm that refuses it; both operations are refused with UnsupportedOperationError on JSON-RPC and
REST (A2aJsonRpcController, A2aRestController), and the checks now require an arm that answers instead of
throwing, so both read missing. This page stays the place for the reasoning. The ROADMAP therefore lists A2A as in
progress, not done. No external A2A client and no TCK has been run against this server.
Requirement table¶
Status key: Met = implemented and covered by a test; Partial = implemented with a stated gap; Missing = not implemented; n/a = optional and not offered.
Agent Card¶
| Spec | Requirement | Status | Evidence / note |
|---|---|---|---|
| 8.1 | Servers MUST make an Agent Card available | Met | A2aAgentCardController (/.well-known/agent-card.json, legacy /.well-known/agent.json); A2aAgentCardServiceTest |
| 8.2 | Well-known URI /.well-known/agent-card.json |
Met | same |
| 4.4.1 | Required fields: name, description, supportedInterfaces, version, capabilities, defaultInputModes, defaultOutputModes, skills |
Met | A2aAgentCardService.card; the platform card sends skills: [] on purpose (unauthenticated endpoint, see class javadoc). Whether an empty skills satisfies "required" is not stated in the part of the spec I read |
| 8.3.1 / 4.4.6 | Each AgentInterface has url, protocolBinding, protocolVersion; first entry is preferred |
Met | JSON-RPC first, HTTP+JSON second; A2aAgentCardServiceTest |
| 8.3.1 | tenant on an interface (optional) |
n/a | not used |
| 8.4 | Cards MAY be signed with JWS (RFC 7515) | Met | A2aCardSigningService (EdDSA/Ed25519, kid, JWKS at /.well-known/jwks.json); A2aCardSigningServiceTest, A2aCardVerifierTest |
| 8.4.1 | Canonicalise with JCS (RFC 8785), exclude signatures, drop default-valued fields |
Partial | We sort map keys and write compact JSON; that equals JCS for the types the card holds (strings, booleans, maps, lists, no floats), but we do not drop default-valued fields (pushNotifications:false etc. stay in the signed bytes). A third-party verifier that follows 8.4.1 literally may compute a different payload. Not tested against an external verifier |
| 8.4 | Protected header: alg, typ (SHOULD be "JOSE"), kid |
Partial | We send typ: "JWS". Changing it changes every signature, so it is left until a verifier interop test exists |
| 8.6 | Servers SHOULD send Cache-Control and ETag |
Partial | Cache-Control yes (5 min public on the platform card, no-store on per-agent cards); no ETag |
| 4.4.x | 0.3 compatibility fields (url, preferredTransport) |
Met (extra) | both generations' fields are emitted, non-conflicting |
Operations¶
The cards advertise streaming: true, pushNotifications: false, extendedAgentCard: false.
| Spec | Operation | JSON-RPC | REST | Note |
|---|---|---|---|---|
| 3.1.1 | SendMessage | Met | Met | blocking by default; returnImmediately/blocking:false honoured |
| 3.1.2 | SendStreamingMessage | Met | Met | SSE, frame order pinned by tests |
| 3.1.3 | GetTask | Met | Met | historyLength honoured |
| 3.1.4 | ListTasks | Missing | Missing | answered with UnsupportedOperationError (-32004 / HTTP 400). Needs a tenant-scoped listing on A2aTaskStore, including its Redis mode |
| 3.1.5 | CancelTask | Met | Met | TaskNotCancelableError on a terminal task |
| 3.1.6 | SubscribeToTask | Missing | Missing | UnsupportedOperationError. The spec requires that error when capabilities.streaming is false; ours is true, so this is a gap, not a conforming refusal |
| 3.1.7-3.1.10 | Push-notification config (create/get/list/delete) | Missing | Missing | PushNotificationNotSupportedError; matches pushNotifications:false |
| 3.1.11 | GetExtendedAgentCard | Missing | Missing | UnsupportedOperationError, which section 13 prescribes when extendedAgentCard is false |
| 5.1 | Identical functionality across bindings | Met | Met | both bindings expose the same four operations and share the task service, RUNNER check and SSE writer |
Data model¶
| Spec | Requirement | Status | Note |
|---|---|---|---|
| 4.1.3 | Task states | Partial | emitted: submitted, working, completed, failed, canceled. Not emitted: input-required, auth-required, rejected. An agent turn here never pauses for the caller; a follow-up on the same contextId continues the conversation (A2aTaskState javadoc). A message carrying taskId is refused with invalid-params |
| 4.1.6 | Part: exactly one of text, raw, url, data |
Partial | text in and out only; raw/url/data parts are refused with ContentTypeNotSupportedError (-32005 / HTTP 400) rather than dropped (A2aWire.extractText) |
| 4.1.7 | Artifact: artifactId, at least one part |
Met | one response artifact per task |
| 5.5 | camelCase JSON; ProtoJSON enum names (TASK_STATE_*, ROLE_*) |
Met (1.0 dialect) | 0.3 dialect is separate and selected by header |
| 5.6.1 | Timestamps ISO 8601 UTC with Z, ms precision SHOULD |
Partial | Instant.toString(): Z always; precision is whatever the clock gives, not clamped to milliseconds |
| A.2.1 | 1.0 servers MUST NOT emit kind |
Met | A2aWire adds kind only in the 0.3 dialect; A2aJsonRpcControllerTest |
| 5.7 | Ignore unrecognised fields | Met | tree-based parsing |
Errors and versioning¶
| Spec | Requirement | Status | Note |
|---|---|---|---|
| 5.4 / 9.5 | JSON-RPC codes: -32001 TaskNotFound, -32002 TaskNotCancelable, -32003 PushNotificationNotSupported, -32004 UnsupportedOperation, -32005 ContentTypeNotSupported, -32006 InvalidAgentResponse, -32007 ExtendedAgentCardNotConfigured, -32008 ExtensionSupportRequired, -32009 VersionNotSupported | Partial | produced: -32001, -32002, -32003, -32004, -32005, -32009. Not produced: -32006 (we never validate a remote reply on the server side), -32007 (no extended card), -32008 (no extension is declared required) |
| 5.4 / 11.6 | REST: HTTP status per error (404 TaskNotFound; 400 for the FAILED_PRECONDITION and INVALID_ARGUMENT family; 500 InvalidAgentResponse) | Met | A2aRestController.mapping; A2aRestControllerTest |
| 11.6 | REST error body is google.rpc.Status JSON with a google.rpc.ErrorInfo detail: reason UPPER_SNAKE_CASE, domain: a2a-protocol.org |
Met | same |
| 9.1 | JSON-RPC failures travel in the envelope | Met | HTTP 200 with error, as the JSON-RPC binding is written here |
| 3.2.6 / 3.6.2 | A2A-Version; unsupported version -> VersionNotSupportedError; empty value means 0.3 |
Met on JSON-RPC | absent header = 0.3 dialect |
| 3.6 | same, on REST | Deviation | REST is 1.0-only (0.3 REST used different paths). An absent header is accepted as 1.0 rather than rejected or treated as 0.3. Any 0.x value gets VersionNotSupportedError. Documented in A2aRestController |
| 3.2.6 | A2A-Extensions header |
n/a | ignored; no extension is declared |
| 13 | Every operation enforces the caller's authorization, before queries that could leak existence | Met | RUNNER check on the target agent, tenant+subject on task lookup; unknown and not-yours are indistinguishable (404 / "unknown agentId"); A2aTaskServiceTenantTest |
| 11.4 | Media type application/a2a+json SHOULD be used |
Met | responses use it; requests accept it and application/json |
| 11.7 | SSE: first a Task (or Message), then status/artifact updates, stream closes after terminal state | Met | same writer as JSON-RPC, without the envelope; A2aRestControllerTest |
REST binding¶
Added in this change. A2aRestController serves, for both the platform base
/api/a2a/rest and the agent-scoped base /api/agents/{agentId}/a2a/rest:
| Method and path | Operation |
|---|---|
POST /message:send |
SendMessage |
POST /message:stream |
SendStreamingMessage (SSE) |
GET /tasks/{id} |
GetTask |
POST /tasks/{id}:cancel |
CancelTask |
GET /tasks, POST /tasks/{id}:subscribe, push-config routes, GET /extendedAgentCard |
answered with the spec's unsupported-operation errors |
Both cards list {url: <endpoint>/rest, protocolBinding: "HTTP+JSON", protocolVersion: "1.0"}
after the JSON-RPC entry, so JSON-RPC stays the preferred interface and 0.3 clients, which
read url/preferredTransport, see no change. Auth is the same bearer JWT through the main
Spring Security chain; no new SecurityConfig rule was needed.
Signed cards keep working: the card is built first and signed last, so the added interface is
covered by the signature. A2aCardSigningServiceTest and A2aCardVerifierTest pass unchanged.
gRPC proposal (needs your decision)¶
Not implemented. The spec defines it (section 10) with the same operations as the other
bindings plus tenant routing. What it would take, and why I would hold off:
- Work: generate the service from the spec's
a2a.proto; add grpc-java and protobuf runtimes (new dependencies: licence review againstscripts/ci/license-allowlist.txt, SBOM, CVE surface); a second listener (gRPC needs HTTP/2 end to end, so a new port or an h2c connector plus a proxy rule in Compose, Helm and Caddy); a server interceptor that validates the Keycloak JWT and bindsTenantContext(a ThreadLocal, AGENTS.md rule 12) on the call thread; server-streaming forSendStreamingMessage; the 5.4 status mapping; tests. - Rough size: a week of work plus a deploy-surface change. The chat hot path would not
gain a hop (rule 6): the gRPC service would call
A2aTaskServicein process. - Value: only clients that cannot use JSON-RPC or REST. The popular SDKs speak JSON-RPC and REST; I have not checked which clients in your target set need gRPC.
- Recommendation: defer until a concrete integration asks for it. If you want it, say so and I will write the design (ADR) first rather than the code.
Known gaps worth scheduling¶
ListTasksandSubscribeToTask(the card saysstreaming: true, so the second one is a real gap).- Align card signing with 8.4.1/8.4 (drop default-valued fields,
typ: JOSE) together with an interop test against an independent verifier, then re-sign. ETagon the cards.INPUT_REQUIRED/AUTH_REQUIREDif approvals should surface as A2A task states instead of a tool-level refusal.
What was not verified¶
- The specification page is about 256,000 characters. It was read through a summarising fetch in three windows, not verbatim, and the very end (after Section 14 and Appendix A/B) was seen only as an outline. Sections 3.1.4 (ListTasks) and 3.1.6 (SubscribeToTask) and the whole of Section 10 (gRPC) were not read in detail; the statements about them above come from the section lists and the error table, not from their text.
- The "required" status of fields I cite comes from 4.4.1 as summarised. Re-check against the spec before claiming certification.
- No external A2A client or the official TCK was run against this server. "Met" means "covered by this repository's unit tests", not "interoperates with other implementations".
- REST routing is tested through Spring MVC standalone (
MockMvc), not on a running stack.