Skip to main content
Version: 1.9.0

A2A Gateway API

Use the A2A Gateway API when one agent needs to delegate work to a registered peer under workspace policy, approval, loop, identity, and audit controls.

Enterprise only

Not included in DVARA Open Source.

Choose an operation​

EndpointPurpose
GET /a2a/{agentId}/.well-known/agent-card.jsonRead the governed Agent Card
GET /a2a/.well-known/ard.jsonList the agents your key may use, as an ARD manifest (experimental, off by default; see discovery endpoints)
POST /a2a/{agentId}/message:sendSend a message and wait for the result
POST /a2a/{agentId}/message:streamSend a message and stream task updates
POST /a2a/{agentId}/tasks:getRead one task
POST /a2a/{agentId}/tasks:listList tasks
POST /a2a/{agentId}/tasks:cancelCancel a task
POST /a2a/{agentId}/tasks:resubscribeResume task event streaming
POST /a2a/{agentId}/pushConfig:setCreate or replace push configuration
POST /a2a/{agentId}/pushConfig:getRead push configuration
POST /a2a/{agentId}/pushConfig:listList push configurations
POST /a2a/{agentId}/pushConfig:deleteRemove push configuration
POST /a2a/{agentId}/push:notify/{callback}Receive a governed push notification from the peer
POST /a2a/{agentId}Any of the operations above, named by the JSON-RPC method (from 1.9.0)

Every request uses the target's registered agentId. DVARA resolves the workspace from the bearer API key before it looks up the target.

Name the method​

A2A 1.0 renamed every JSON-RPC method. From 1.9.0 DVARA knows both sets of names and uses each with its own version:

A2A 0.3A2A 1.0Path
message/sendSendMessagemessage:send
message/streamSendStreamingMessagemessage:stream
tasks/getGetTasktasks:get
tasks/listListTaskstasks:list
tasks/cancelCancelTasktasks:cancel
tasks/resubscribeSubscribeToTasktasks:resubscribe
tasks/pushNotificationConfig/setCreateTaskPushNotificationConfigpushConfig:set
tasks/pushNotificationConfig/getGetTaskPushNotificationConfigpushConfig:get
tasks/pushNotificationConfig/listListTaskPushNotificationConfigspushConfig:list
tasks/pushNotificationConfig/deleteDeleteTaskPushNotificationConfigpushConfig:delete

You can call an operation in two ways. Post to its path, as in the first table; the path names the operation. Or post a standard JSON-RPC request to /a2a/{agentId}, the address in DVARA's Agent Card, and name the operation in method, as stock A2A clients do:

curl --silent https://<dvara-host>/a2a/<agent-id> \
--header "Authorization: Bearer $DVARA_API_KEY" \
--header "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","id":"1","method":"tasks/get","params":{"id":"<task-id>"}}'

Both ways run the same governance and give the same answer.

DVARA reads method in the version you name (see below):

You sendDVARA answers
An A2A 0.3 name, as a 0.3 requestThe operation
An A2A 1.0 name, as a 0.3 request, or an unknown name404, JSON-RPC -32601 (method not found), naming the right method
No method400, JSON-RPC -32600
An A2A 1.0 name, as a 1.0 requestThe operation, read and answered in the A2A 1.0 shape (see "Send A2A 1.0 messages" below)

DVARA calls each peer agent with the method name of the version it uses for that peer: GetTask for a peer it reaches in A2A 1.0, tasks/get for a peer it reaches in 0.3. Audit events keep one name for each operation (the 0.3 name), and also record the name your client sent and the name sent to the peer.

Name the A2A version​

From 1.9.0, DVARA reads the A2A protocol version of every request, as the A2A specification asks. Send it in the A2A-Version header, or as an A2A-Version query parameter:

A2A-Version: 0.3
You sendWhat happens
No versionThe request is served as A2A 0.3, as the specification requires. Existing 0.3 clients need no change.
0.3 (a patch such as 0.3.1 is ignored)The request is served.
1.0The request is served in the A2A 1.0 shape.
Any other versionRefused with 400 before the request is authenticated.

A refused version gets the A2A VersionNotSupportedError, and the versions DVARA serves:

{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32009,
"message": "A2A protocol version 0.5 is not supported. Supported versions: 0.3, 1.0",
"data": {
"type": "VersionNotSupportedError",
"requestedVersion": "0.5",
"supportedVersions": ["0.3", "1.0"],
"code": "a2a_version_not_supported",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}
}

This is the A2A 0.3 form, which a version starting 0. gets. From 1.9.0, any other refused version, such as 2.0, gets the A2A 1.0 form, where data is a list holding one google.rpc.ErrorInfo (see Read a refusal):

{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32009,
"message": "A2A protocol version 2.0 is not supported. Supported versions: 0.3, 1.0",
"data": [{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "VERSION_NOT_SUPPORTED",
"domain": "a2a-protocol.org",
"metadata": {
"code": "a2a_version_not_supported",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"requestedVersion": "2.0",
"supportedVersions": "0.3,1.0"
}
}]
}
}

An operator who wants to hold DVARA at A2A 0.3 can set a2a.protocol.serve-v1: false. DVARA then refuses a 1.0 request, lists only 0.3 as supported, and calls every peer in 0.3.

DVARA also names the version on every call it makes to a peer agent, chosen from that peer's Agent Card (see below). If the peer answers that it does not support that version, your call fails with 502 a2a_peer_version_not_supported, and the message names the peer. Push notifications from peers are not checked for a version. The Agent Card follows its own rule, from 1.9.0: see Read the card in your version.

How DVARA talks to each peer​

From 1.9.0, DVARA reads each peer's Agent Card to decide how to call it, as the A2A specification asks a client to. It goes through the card's supportedInterfaces list in the order the peer wrote it, and uses the first entry that it can speak: the JSON-RPC binding at a version DVARA serves, or, from 1.9.0, the HTTP+JSON (REST) binding at A2A 1.0. It sends that entry's tenant, if it has one.

The peer's card listsDVARA calls it with
No supportedInterfaces (an A2A 0.3 card)A2A 0.3 over JSON-RPC
A JSON-RPC 1.0 entry, then a JSON-RPC 0.3 entryA2A 1.0, the first entry
A JSON-RPC 0.3 entry, then a JSON-RPC 1.0 entryA2A 0.3, the first entry
An HTTP+JSON 1.0 entry, then a JSON-RPC 1.0 entryA2A 1.0 over HTTP+JSON, the first entry (from 1.9.0)
A JSON-RPC 1.0 entry, then an HTTP+JSON 1.0 entryA2A 1.0 over JSON-RPC, the first entry
Only gRPC entries, or only HTTP+JSON 0.3 entriesNothing: your call fails with 502 a2a_peer_protocol_unsupported

When no entry fits, the message lists what the peer offers and why each entry was passed over, and nothing is sent to the peer. Your client and the peer do not need the same version: DVARA translates between them (see below).

Which address DVARA calls​

Each entry on the card has its own url. DVARA prefers an entry at the endpoint the agent was registered with. From 1.9.0, when the chosen entry lists another address, DVARA calls that address, as the A2A specification asks, but only after it passes the same checks as the registered endpoint, on every call:

  • It is on the same scheme, host and port as the registered endpoint. Only the path may differ, so a peer can serve A2A 1.0 at /a2a/v1 beside /a2a.
  • It is a plain https:// address (or http:// where your deployment allows plain HTTP for development), with no user name, password or fragment.
  • The address DVARA connects to passes the same network checks as the registered endpoint.
  • The card's signature allows it. A card whose signature is invalid never gives an address. If you pinned a public key for the agent, or require signed cards, only an address that the verified signature covers is used.

If the address fails a check, the call fails with 502 a2a_peer_url_refused and a message that names the check. Nothing is sent, and DVARA does not quietly call the registered endpoint instead. The refusal is recorded with the hop.

If a peer's card lists only a public name while you registered an internal one, calls to it now fail this way. Register the address the card lists, or ask the peer to list your registered endpoint on its card.

Bindings DVARA supports​

DVARA calls peers over two of the A2A bindings:

  • JSON-RPC, at A2A 0.3 or 1.0.
  • HTTP+JSON (REST), at A2A 1.0, from 1.9.0.

DVARA does not support the gRPC binding, for calls to peers or for calls to DVARA. The A2A specification makes every binding optional, and the official A2A SDKs and the hosted agent platforms we checked all serve JSON-RPC or REST. If your agent serves gRPC, also offer JSON-RPC or HTTP+JSON on its Agent Card. A gRPC entry is passed over as gRPC is not supported.

From 1.9.0, Flightdeck checks this when an agent is registered or edited, so you find out then rather than at the first call. If the peer's Agent Card offers no binding DVARA supports, the save is refused with a message that lists what the card offers. An operator can tick Save anyway for a special case, such as a peer that is about to add a JSON-RPC entry. That choice is recorded in the audit log, and calls to the agent still fail with 502 a2a_peer_protocol_unsupported until its card offers a supported binding. If Flightdeck cannot read the card when you save, the save goes ahead.

Peers that speak HTTP+JSON​

From 1.9.0, DVARA can call a peer that offers the A2A HTTP+JSON (REST) binding. Nothing changes for your client: you still call DVARA as before, and DVARA calls the peer at the REST address for each operation under the registered endpoint, such as POST <endpoint>/message:send or GET <endpoint>/tasks/{id}. The same checks run as for a JSON-RPC peer: policy, PII, limits, approval, the agent's credential, the extensions you are allowed, and the audit record. Streams work the same way.

DVARA reads the peer's answer strictly, as it does for any A2A 1.0 peer. An answer it cannot read fails your call with 502 a2a_upstream_invalid_response, and nothing from it reaches you. When the peer answers with an A2A error, you get that error in your own version: a task the peer does not know is JSON-RPC -32001, as it would be from a JSON-RPC peer. The peer's own error text is not passed on.

DVARA itself is still called over JSON-RPC only, and the Agent Card it serves lists only JSON-RPC. It keeps its choice for as long as it keeps the card, and makes it again when the card changes. Each call's record shows the version and binding used.

Send A2A 1.0 messages​

From 1.9.0 DVARA serves A2A 1.0 as well as 0.3. A2A 1.0 changed how messages, tasks and stream events are written, not only how methods are named:

A2A 0.3A2A 1.0
A part of a message{"kind": "text", "text": "…"}, {"kind": "file", "file": {…}}, {"kind": "data", "data": {…}}{"text": "…"}, {"raw": "<base64>"}, {"url": "…"}, {"data": …}, each with an optional mediaType and filename
Roles and task statesuser, agent; working, completed, input-required …ROLE_USER, ROLE_AGENT; TASK_STATE_WORKING, TASK_STATE_COMPLETED …
The answer to a messagethe task or the message{"task": …} or {"message": …}
A stream eventthe object, with kind, and final on a status update{"statusUpdate": …}, {"artifactUpdate": …}, {"task": …} or {"message": …}; no final
A push notification configtaskId and a nested pushNotificationConfigflat: taskId, id, url, token, authentication
Listing tasksnot a JSON-RPC methodListTasks, with pageSize (1 to 100) and pageToken; every answer has nextPageToken, empty on the last page

A 1.0 request looks like this:

curl --silent https://<dvara-host>/a2a/<agent-id> \
--header "Authorization: Bearer $DVARA_API_KEY" \
--header "A2A-Version: 1.0" \
--header "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","id":"1","method":"SendMessage","params":{"message":
{"messageId":"m-1","role":"ROLE_USER","parts":[{"text":"Plan a trip to Lisbon"},
{"data":{"days":3}}]}}}'

Your client and the peer can speak different versions. DVARA translates every message, answer, stream event and push notification between the version your client sends and the version it uses for the peer. A 1.0 client can call a 0.3 agent, and a 0.3 client can call a 1.0 agent. A 0.3 client still gets final on stream events, worked out from the task's state. A stream ends when it closes, with no extra [DONE] line. From 1.9.0 that is true for a 0.3 client too: every A2A stream event is a JSON-RPC answer, and the official 0.3 SDK failed on the extra line after a complete answer.

Push config lists are paged for a 0.3 agent. An A2A 0.3 agent lists every push config in one answer. From 1.9.0, when a 1.0 client sends pageSize or pageToken to such an agent, DVARA asks the agent for the whole list and returns the page, with a nextPageToken for the rest. Use the token DVARA gave you; a token it did not give is refused with -32602. Earlier releases refused the paged request.

Governance covers every kind of part. DVARA reads each message into one form before any check runs, so PII redaction and blocking, policy and the task checks apply the same way whichever version you send. PII is checked in text parts and in every string inside a data part. File content is not read. From 1.9.0 a stream is checked the same way: data parts in the message you stream, and in every event the agent streams back, are redacted or blocked like text. Each string in a data part is checked as a value of its own.

What DVARA does not understand, it refuses. It never skips a part it cannot read, because a part it skipped is a part nobody checked:

What happensAnswer
Your request has a part, member or value its version does not define, such as {"image": …} or "role": "user" in a 1.0 request400, JSON-RPC -32602, with the place and the reason, for example params.message.parts[1]: unknown member image. Nothing is sent to the peer
Your request has something the peer's version cannot carry, such as a filename on a text part for a 0.3 peer, or ListTasks to a 0.3 peer (A2A 0.3 has no JSON-RPC method for it)400, JSON-RPC -32004, a2a_untranslatable. DVARA refuses rather than drop part of your message
The peer answers with a shape DVARA cannot read502, JSON-RPC -32006, a2a_upstream_invalid_response. On a stream, one error event, and the stream ends
The peer's answer cannot be carried in your version502, a2a_upstream_untranslatable

DVARA has been checked against the official A2A Python SDK (version 1.0.3), as the client and as the agent, and from 1.9.0 against its 0.3 release (0.3.26) as a streaming client. These checks run on every change to the A2A gateway.

Choose the content type​

From 1.9.0, DVARA accepts two content types for a request body on every A2A operation:

  • application/json, which the A2A JSON-RPC binding uses.
  • application/a2a+json, the A2A media type. The A2A specification names it for its HTTP+JSON binding, so a client built for that binding can send it unchanged.

Both are read the same way, under the same limits and governance. Any other type, such as text/plain or YAML, is refused with 415 a2a_unsupported_media_type, and the Accept header of the answer names the two types DVARA reads.

DVARA answers with application/json, as the JSON-RPC binding requires. If your client accepts only application/a2a+json, DVARA answers with that type instead; the body is the same. Streams are text/event-stream.

DVARA sends a JSON-RPC peer application/json and, from 1.9.0, an HTTP+JSON peer application/a2a+json, and it reads an answer in either JSON type. If a peer answers with anything else, such as an HTML error page, your call fails with 502 a2a_upstream_content_type, and the message names the type the peer sent.

Authenticate and scope the call​

Workspace-facing A2A operations require a DVARA workspace API key:

Authorization: Bearer <your-api-key>
ScopeOperations
a2a:readtasks:get, tasks:list, tasks:resubscribe, and push-configuration reads
a2a:sendmessage:send, message:stream, tasks:cancel, and push-configuration writes or deletion

An unscoped API key is unrestricted. Agent Card discovery still requires a valid workspace key, but it is not narrowed by an A2A scope in 1.8.

Send a governed message​

Replace the host, target agent ID, and workspace API key:

curl --silent https://<dvara-host>/a2a/<agent-id>/message:send \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": "hop-1042",
"method": "message/send",
"params": {
"metadata": {"skill": "summarize"},
"message": {
"role": "user",
"parts": [
{"kind": "text", "text": "Summarize claim CLM-1042 for review."}
]
}
}
}'

A completed peer call returns an A2A message or task inside the JSON-RPC envelope:

{
"jsonrpc": "2.0",
"id": "hop-1042",
"result": {
"task": {
"id": "task-7f12"
}
}
}

If policy denies the hop, DVARA refuses it with a2a_policy_denied and does not call the peer (HTTP 200 with a JSON-RPC error from 1.9.0; see Read a refusal).

Open Agents → A2A Hops to inspect the source, target, decision, status, and latency. The same hop writes intent and result or denial evidence to Governance → Audit, joined by its trace information.

Stream a message or resume a task​

message:stream and tasks:resubscribe return Server-Sent Events. They use the same authentication and governance checks as their unary forms. Streaming is on by default; when a2a.streaming.enabled=false, either route is refused with a2a_streaming_disabled.

From 1.9.0, a governance refusal, or an agent that answers with an error instead of a stream, is a plain JSON-RPC error response, not an event stream. Before, it was delivered as one SSE error event. If the peer fails or stalls after the stream opens, DVARA sends an error event and does not send [DONE]. Treat that event as the final outcome rather than a partial success. A stream that completes ends when it closes. From 1.9.0 DVARA sends no [DONE] line at either A2A version.

Discover the governed Agent Card​

GET /a2a/{agentId}/.well-known/agent-card.json returns the card JSON directly, not a JSON-RPC envelope. DVARA resolves the agent inside the caller's workspace, filters its skills with that workspace's allow-list, and serves the cached card until its discovery TTL expires.

From 1.9.0 the card describes DVARA, not the peer, because your client talks to DVARA. Its supportedInterfaces lists DVARA's address for the agent (https://<gateway>/a2a/{agentId}) with the JSON-RPC binding, once for each A2A version DVARA serves, 1.0 and then 0.3. For A2A 0.3 clients, url, preferredTransport (JSONRPC), additionalInterfaces and protocolVersion (0.3) say the same. The peer's own address and versions are not shown. Post JSON-RPC requests to that address, or send each operation to that address plus its path, such as /message:send (see Name the method).

Add ?refresh=true to fetch the peer again immediately. If the agent has a pinned public key, DVARA verifies the card signature and reports the trust state in x-dvara-signature-trust. From 1.9.0 the signature is checked the way the card's own A2A version defines, and the served card does not include the peer's signatures, because they cover the peer's card, not the one DVARA serves. From 1.9.0, an agent with a pinned key gets 502 a2a_card_signature_invalid for any card that is not VERIFIED, including an unsigned one, and each signature must carry kid and typ JOSE. A fetch failure, or a card whose structure does not match its version, uses a valid cached card when one is available; otherwise it returns 502 a2a_card_unavailable.

From 1.9.0 the answer carries Cache-Control: private, max-age=60, Vary: Authorization, X-Dvara-Delegated-Authorization, A2A-Version and an ETag. Send the ETag in If-None-Match to get 304 Not Modified with no body when the card has not changed:

curl --include https://<gateway>/a2a/researcher/.well-known/agent-card.json \
--header 'Authorization: Bearer <your-api-key>' \
--header 'If-None-Match: "<etag from the last answer>"'

DVARA checks the key and governs the card before it compares the tag, so a refused request gets its error, not a 304. Callers who see different cards, for example two workspaces with different allow lists, get different tags. DVARA keeps the peer's own card for as long as the peer's caching headers say, between 30 seconds and one hour, and then asks the peer with its ETag.

Read the card in your version​

From 1.9.0 DVARA serves the Agent Card in the A2A version your client names in the A2A-Version header. A2A 0.3 and 1.0 write a card's security schemes in different shapes under the same name, so one card cannot serve both: an SDK reading the other version's shape refuses the card or misreads it.

You sendThe card you get
No A2A-Version headerThe A2A 1.0 card.
A2A-Version: 1.0The A2A 1.0 card: supportedInterfaces, security schemes such as {"httpAuthSecurityScheme": {"scheme": "bearer"}}, and securityRequirements. It also carries url, preferredTransport and protocolVersion, so a 0.3 client that sends no header still finds DVARA's address.
A2A-Version: 0.3The A2A 0.3 card: url, preferredTransport, additionalInterfaces and protocolVersion 0.3, security schemes such as {"type": "http", "scheme": "bearer"}, and security. No supportedInterfaces.
Any other versionRefused with 400 and the A2A VersionNotSupportedError, as for any A2A request.

Only the header picks the card's version; an A2A-Version query parameter does not. The extended Agent Card follows the same rule.

In both versions the card's extended-card flag (capabilities.extendedAgentCard at 1.0, supportsAuthenticatedExtendedCard at 0.3) is false, because DVARA does not serve the extended-card method. The extended card, when DVARA can fetch it for you, is the card at this same address.

An A2A 0.3 client should send A2A-Version: 0.3 when it fetches the card. Without it, the client gets the 1.0 card and cannot read its security schemes.

DVARA translates each security scheme the peer declares, whichever version the peer wrote its card in. A few things have no A2A 0.3 form: an OAuth 2.0 device code flow, and the pkceRequired flag. DVARA leaves them out of the 0.3 card and lists each, with the reason, in x-dvara-security-schemes-omitted. A security requirement that names a scheme left out is left out too.

With a2a.protocol.serve-v1: false, DVARA serves 0.3 only, so the card is the 0.3 card and a request for the 1.0 card is refused.

Check DVARA's signature on the card​

From 1.9.0 the card DVARA serves carries DVARA's own signatures, once an owner has made a card signing key in Flightdeck. Each signature's protected header has alg (ES256), kid, typ (JOSE) and jku, which points to the public keys:

curl https://<gateway>/a2a/.well-known/jwks.json

No API key is needed for the key set. Check the card with your A2A SDK's signature verifier, taking the key by its kid from that key set. The card is signed in the version you asked for, so a 0.3 SDK checks the 0.3 card and a 1.0 SDK the 1.0 card. A rotated-out key stays in the key set for 30 days. The same card keeps the same signature, so its ETag does not change.

Ask for an A2A extension​

From 1.9.0 DVARA governs A2A extensions. An extension is named by a URI and can add data, methods and task states to A2A. Ask for one in the A2A-Extensions header, as the A2A specification describes:

curl --include https://<gateway>/a2a/researcher \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--header 'A2A-Extensions: https://a2a-protocol.org/extensions/timestamp/v1' \
--data '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m-1","role":"user","parts":[{"kind":"text","text":"Summarise the report"}]}}}'

DVARA passes an extension to the agent only when all three are true:

  • you asked for it in A2A-Extensions;
  • your workspace allows it (a2a.extensions.allowed; nothing is allowed until an operator allows it);
  • the agent's Agent Card declares it.

The answer's A2A-Extensions header lists the extensions the agent says it activated. A stream does not carry this header. Extension URIs that are not in use are removed from the extensions list of messages and artifacts in both directions, and the Agent Card DVARA serves lists only the extensions your workspace allows.

If the agent's card marks an extension as required:

Your workspaceYou asked for itResult
Does not allow itEither403, JSON-RPC -32000, code a2a_extension_not_allowed. The agent is not called
Allows itNo400, JSON-RPC -32008 (ExtensionSupportRequiredError), code a2a_extension_support_required. Send the header and try again
Allows itYesThe call goes ahead

DVARA does not forward methods an extension adds. It cannot read them, so it could not check them for PII, policy or limits. Such a call gets 404, JSON-RPC -32601.

Govern push notifications​

The pushConfig:* routes manage the peer's push-notification configuration. pushConfig:set validates the workspace webhook address and refuses loopback, private, cloud-metadata, and plain HTTP targets before contacting the peer.

When a2a.push.callback-base-url is configured, DVARA replaces the workspace webhook details sent to the peer with a sealed Gateway callback. The peer posts updates to:

POST /a2a/{agentId}/push:notify/{callback}
X-A2A-Notification-Token: <notification-token>

The peer callback takes no DVARA API key. The sealed path and notification token identify the expected agent and task; an agent with a pinned key must also send a signed bearer JWT. DVARA checks and PII-scans the notification, records it, and then sends it to the workspace webhook. Duplicate notifications are accepted without a second delivery.

If the Gateway has no public callback base URL, governed push configuration is refused with 503 a2a_push_not_available, unless the workspace explicitly uses direct delivery. Direct delivery sends the workspace webhook to the peer and does not put the Gateway on the notification path.

Apply the request and response limits​

BoundaryDefaultFailure
Text across an outbound message:send or message:stream message50,000 characters413 a2a_message_too_large
Whole peer answer, task response, or Agent Card16 MiB502 a2a_response_too_large; Agent Card discovery uses a2a_card_unavailable when no cached card can be served
message:stream or tasks:resubscribe stream64 MiBThe stream ends with an SSE error event
Incoming governed push-notification body1 MiB413 a2a_push_too_large

The message limit runs before policy and PII scanning. Response limits cover the bytes DVARA reads from a peer. Operators can change the fleet defaults; the response limits can also be set per workspace.

Read a refusal​

From 1.9.0, every refusal on a JSON-RPC call is a JSON-RPC error that an A2A client can read, in the form of the A2A version you sent. Before, a refusal by DVARA itself, such as a policy denial, put DVARA's code as text in error.code, where the specification requires a number.

  • error.code is always a number. Where the A2A specification or JSON-RPC names the error, it is theirs: -32009 version not supported, -32602 invalid parameters, -32004 unsupported operation, -32006 an answer from the peer that does not follow the specification, -32700 a body that is not JSON. A decision DVARA made (policy, PII, limits, approval, loop, session kill, key, delegation, an unreachable peer) is -32000.
  • DVARA's own code is in error.data. With A2A 0.3 (or no version), data is an object: data.code is the code, such as a2a_policy_denied, with data.type and data.trace_id. With A2A 1.0, data is a list holding one google.rpc.ErrorInfo, as the 1.0 specification asks: the code is in metadata.code, and the trace ID in metadata.traceId.
  • The domain says who refused. In the 1.0 form, a refusal by DVARA has domain dvarahq.com and its code in capitals as the reason (A2A_POLICY_DENIED). An error the A2A specification defines has domain a2a-protocol.org and the specification's reason (TASK_NOT_FOUND).
  • A peer's A2A error reaches you as the peer sent it. If the agent answers, for example, -32001 task not found, you get -32001 and its reason, and DVARA's code a2a_upstream_error. The rest of the peer's error detail is not passed on.

A policy denial at A2A 1.0:

{
"jsonrpc": "2.0",
"id": "1",
"error": {
"code": -32000,
"message": "Hop denied by A2A policy",
"data": [{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "A2A_POLICY_DENIED",
"domain": "dvarahq.com",
"metadata": { "code": "a2a_policy_denied", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736" }
}]
}
}

The same refusal at A2A 0.3 has "data": {"type": "a2a_error", "code": "a2a_policy_denied", "trace_id": "…"}.

On a stream, DVARA's own error event (a peer that stalls, a stream cut at the size limit or stopped for PII or a session kill) is a JSON-RPC error with your call's id, in the same form. From 1.9.0, a stream refused before it opens is not a stream: if DVARA refuses it, or the agent answers with an error instead of a stream (for example TaskNotFoundError when you subscribe to a task it does not know), you get a plain JSON-RPC error response (application/json), as for a call that is not a stream. A refused Agent Card request answers an A2A 1.0 client with a google.rpc.Status body: {"error": {"code": 404, "status": "NOT_FOUND", "message": "…", "details": [ErrorInfo]}}; without a version it keeps {"error": {"code": "a2a_agent_not_found", …}}.

The HTTP status of a refusal​

From 1.9.0, DVARA answers a refusal of a JSON-RPC call with HTTP 200, as the official A2A SDK's own server does. The JSON-RPC binding of A2A gives an error no HTTP status; the official A2A Python SDK reads the error only from a 200 answer, and raises a plain HTTP error on any other status. So an SDK client now gets the A2A error DVARA sent: for example JSON-RPC Error -32000 for a policy denial, or TaskNotFoundError when the agent does not know the task. Branch on error.code and DVARA's code in error.data, not on the status.

One refusal keeps its status: a missing, expired or revoked key is still answered 401, with the same JSON-RPC body. A2A checks credentials at the HTTP level, and the A2A 0.3 specification asks for 401 there.

Also from 1.9.0, when the agent answers that your request is invalid (-32600 to -32603, for example -32602 invalid parameters), you get the agent's code. Before, it reached you as -32000.

Before 1.9.0 the status was the refusal's own, such as 403 for a policy denial and 502 for an agent's error. The operator setting a2a.protocol.json-rpc-error-status: refusal brings those statuses back. These keep their status either way: an Agent Card request, a request that is not a POST, a body that is not application/json (415), and a stream (which was always 200, with the error as its event).

DVARA's own records still tell refusals apart: its request log and the a2a_requests_total metric carry the refusal's code and JSON-RPC code, and the hop history shows the refusal's status, such as 403.

Diagnose a failed call​

A missing, expired, or revoked workspace key returns 401. A key without the required A2A scope is refused with a2a_scope_denied (HTTP 200 from 1.9.0; see The HTTP status of a refusal). A target outside the caller's workspace is not exposed. A message or task refusal is a JSON-RPC error with DVARA's code and the request trace ID in error.data (see Read a refusal). A stream refused before it opens is a plain JSON-RPC error; a failure after it opens is one JSON-RPC error event; Agent Card discovery returns an ordinary HTTP JSON error.

Peer discovery, signature, credential, policy, approval, loop, size, and transport failures use distinct error codes so you can separate configuration failures from governed denials. See Error handling for the status and code table.

For delegation chains, approval behavior, streaming, push callbacks, and Agent Card trust, continue with Govern agent-to-agent calls with the A2A Gateway.