Skip to main content
Version: Latest (1.9.x dev)

Govern peer-agent calls with the A2A Gateway

An agent can delegate work to another agent, but that delegation should not bypass identity, policy, data protection, or human review. The DVARA A2A Gateway governs each peer-agent call as a distinct, traceable hop.

Enterprise only

Not included in DVARA Open Source.

Statuses on this page

A status on this page, such as 403 A2A_POLICY_DENIED, is the refusal's own status: the one the hop history shows. From 1.9.0, DVARA sends a refusal of a JSON-RPC call with HTTP 200 and the code in the JSON-RPC error, so an A2A SDK client can read it. See The HTTP status of a refusal.

How does DVARA govern a peer-agent call?​

Your application sends one A2A request to DVARA. DVARA:

  1. authenticates the API key and resolves its workspace;
  2. finds the target agent in that workspace's registry;
  3. records A2A_HOP_INTENT before the peer is contacted;
  4. evaluates matching A2A policy and scans the outbound message for PII;
  5. checks the session for a repeated hop, cycle, or excessive rate;
  6. applies any matching human-approval rule on a synchronous hop;
  7. authenticates to the peer and forwards an allowed request; and
  8. scans the reply, records the outcome, and returns it to the caller.

DVARA governs one hop. It does not choose the next agent, make an onward call, or run your multi-agent workflow. Your application remains the orchestrator.

What do you need before you send a hop?​

You need a running Enterprise platform deployment (a licence is optional), an active workspace API key, and an active target agent registered in that same workspace.

Register the peer from Agents → A2A Agents in DVARA Flightdeck. Give it a stable agent ID, its endpoint URL, an authentication scheme, and any skills that the workspace may discover. Workspace-supplied endpoint URLs are checked before they are saved; loopback, private, link-local, and cloud-metadata addresses are refused.

From 1.8.5 a peer's endpoint must be https://, as the A2A specification requires. A hop carries the workspace's stored credential or the caller's delegated token, and over http:// both would cross the network in clear text. The Flightdeck, the Flightdeck and a GitOps import refuse an active agent whose endpoint is not https://, with A2A_ENDPOINT_NOT_HTTPS. The gateway refuses to send a hop, open a stream or fetch an Agent Card for such an agent, and never contacts the peer: message:send and task calls answer 502 A2A_ENDPOINT_NOT_HTTPS, and a stream answers one error event, a2a_endpoint_not_https.

An agent saved over http:// before 1.8.5 is not changed. Flightdeck names each active one in a warning at startup, and the Flightdeck agent lists mark it Not HTTPS. Every hop to it is refused until you move it to https:// or disable it. A disabled or suspended agent may keep its http:// endpoint, because the gateway sends it nothing.

For a local stack whose stub peers have no certificate, set DVARA_A2A_ALLOW_PLAINTEXT_ENDPOINTS=true on Flightdeck and the Gateway. It works only on a development profile (dev, test, ci, local, or none set). On any other profile, such as the production profile the Compose file and the Helm chart set, it is ignored and a warning at startup says so.

DVARA does not follow redirects from an upstream A2A agent. Register the final URL: a 3xx response fails the governed hop, health check, or Agent Card discovery instead of being followed to another address.

From 1.8.4 an agent ID cannot contain ::. The Flightdeck, the Flightdeck and a GitOps import refuse such a new ID as A2A_AGENT_ID_INVALID; an agent that already has one keeps it. An ID the workspace already uses is refused on the form as A2A_AGENT_DUPLICATE, naming the ID, and the values you entered stay in the form.

Send a governed hop​

Replace <dvara-host> and <your-api-key>. This example calls the registered researcher agent and supplies an explicit skill for policy and approval matching.

curl --silent https://<dvara-host>/a2a/researcher/message:send \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--header 'X-Session-Id: case-1042' \
--data '{
"jsonrpc": "2.0",
"id": "hop-1042",
"method": "message/send",
"params": {
"metadata": { "skill": "summarize" },
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "Summarize the approved incident report for the support team."
}
]
}
}
}'

An allowed peer response stays inside the JSON-RPC envelope. The exact task fields come from the peer; a minimal result has this shape:

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

The response also includes X-Trace-Id. Use it with X-Session-Id to follow the hop through DVARA Flightdeck and your own service logs.

Verify what DVARA recorded​

Open Audit in Flightdeck, select the A2A plane, and find case-1042. The record shows the workspace, target agent, requested skill, operation, policy decision, authentication mode, status, latency, and PII flags.

The A2A audit chain writes A2A_HOP_INTENT before dispatch. An allowed response writes A2A_HOP_RESULT; a governance refusal writes A2A_HOP_DENIED. The A2A chain is HMAC-signed and hash-chained, so changes or gaps are detectable. The guarantee is tamper-evident.

From 1.8.2 a Gateway pod has no database, so this separate A2A chain is signed into the pod's local spool and drained to Flightdeck. It does not silently disappear on the way.

What happens without extra configuration?​

Control1.8 defaultWhat that means
API-key authenticationRequiredAnonymous A2A calls are refused
API-key scopesEmpty means unrestrictedA scoped key needs a2a:send for messages and cancellation, or a2a:read for task reads
A2A policyDefault allowInside a policy the first matching ALLOW or DENY rule decides; a DENY in any policy wins (from 1.9.0); no match allows the hop
PII actionLOG unless the workspace overrides itDetection is recorded and the original content continues
Delegation guardenforce, rule agent-skill, depth 8 (from 1.8.3)A hop that repeats the same agent and skill in its chain, or makes the chain longer than 8, is refused with 409. See the delegation guard
Loop detectionOn for hops with a session IDFixed 1.8 thresholds detect repetition, short cycles, and more than 60 hops per minute
Loop responseKill the sessionThe detected hop and later hops in that session are refused. From 1.8.5 the hop rate refuses with 429 and does not kill the session
Peer response size16 MiB for a whole answer, 64 MiB for a stream (from 1.8.5)A larger answer is refused with 502 A2A_RESPONSE_TOO_LARGE. See Response size
Push notificationsThrough the gateway (from 1.8.5)Needs A2A_PUSH_CALLBACK_BASE_URL; without it a push config is refused. See Receive push notifications through DVARA
Approval engineOn, with no matching rulesA synchronous hop does not pause until the workspace names required agents or skills
Approval timeout300 seconds, then denyA matching synchronous hop fails closed when nobody decides
Signed-card requirementOffDiscovery serves a valid card with its trust state unless strict verification is enabled

A suspended workspace is refused. A key with a nonempty scope list can perform only operations covered by those scopes. Agent Card discovery still requires a valid key, but it is not narrowed by a2a:read in 1.8.

Make skill policy match the request​

A2A, in v0.3 and v1.0, has no skill field on a message. DVARA reads the skill from:

  1. params.metadata.skill;
  2. params.message.metadata.skill; or
  3. params.skill.

Use params.metadata.skill, as in the example, and name one skill per message.

From 1.9.0, a message that names no skill, or names more than one, is never treated as "no skill rule applies":

  • No skill. If the workspace has a policy rule for a skill, a DENY or WARN rule on that hop's source and target applies as if the skill matched. An ALLOW rule for a skill never does. If any skill needs approval, the hop is held for approval. A workspace with no skill rule is not affected: the hop is governed by agent rules only.
  • Different skills. If the three places name different skills, or the skill is not a single string, the hop is refused with HTTP 403 and A2A_POLICY_DENIED. The same skill in two places counts as one.

Each of these writes an A2A_SKILL_UNRESOLVED audit event that says what was missing or conflicting and which rule acted. Task operations, such as getting or cancelling a task, name no skill and are not affected.

A2A policies are separate from LLM and MCP policies. They match source, target, and skill. Inside one policy the first matching ALLOW or DENY rule decides, and matching WARN rules before it add warnings. A policy denial returns HTTP 403 with A2A_POLICY_DENIED. An evaluation failure also fails closed with HTTP 403 and A2A_POLICY_ERROR.

When several policies match​

From 1.9.0, each A2A policy is read on its own, and then:

  • if any policy denies the hop, it is denied;
  • otherwise it is allowed, with the warnings of every policy.

So the order of your policies never changes the answer. A workspace policy that allows everything cannot switch off a platform policy that denies a hop. This is how LLM and MCP policies already work: a DENY anywhere wins.

To make an exception, put it in the same policy as the DENY it relaxes, above that rule:

rules:
- match: { source: "finance-bot", target: "payments-agent" }
effect: ALLOW
- match: { target: "payments-agent" }
effect: DENY
reason: "Only finance-bot may call payments-agent"

An ALLOW rule in a different policy does not relax another policy's DENY. Before 1.9.0 it could, depending on which policy was newer. If two policies deny the same hop, the refusal gives the reason of the same one every time, on every gateway.

Approval rules are separate from policies and are not changed by this. A hop that the policies allow is still held for approval when an approval rule names its agent or skill.

Choose how PII is handled​

DVARA scans text and data parts before dispatch and scans a successful peer reply before release. The workspace's PII action decides what happens. Its default is LOG.

ActionOutbound messagePeer response
LOGRecord detection and forward the original textRecord detection and release the original reply
REDACTReplace detected values before dispatchReplace newly detected values before release
TOKENIZEReplace provider-bound values with recoverable tokens when tokenization is availableIrreversibly redact newly detected response values
BLOCKReturn A2A_PII_DETECTED; do not contact the peerReturn A2A_PII_DETECTED; do not release the reply

If PII enforcement itself fails on content that requires protection, DVARA fails closed with A2A_PII_ENFORCEMENT_FAILED.

DVARA checks text parts and every string inside a data part. From 1.9.0 it checks streams the same way as single answers: data parts in a streamed message and in every event the peer streams back. With BLOCK, a stream that carries PII in a data part ends with one error event, and nothing after it is sent. If an event cannot be read, the stream ends with A2A_PII_ENFORCEMENT_FAILED and nothing more is sent.

Require approval for a risky hop​

Set approval.required-agents to exact target-agent IDs or approval.required-skills to skill globs in workspace settings. For example, require a person before an agent delegates a refund:

approval.required-agents: "payments"
approval.required-skills: "refund_*,issue_credit"
approval.timeout-seconds: 300
approval.default-action: "deny"

A match writes A2A_APPROVAL_REQUESTED and pauses a synchronous message:send. Approving releases the hop; denying returns HTTP 403 with A2A_APPROVAL_DENIED. The default timeout returns HTTP 408 with A2A_APPROVAL_TIMEOUT. If the approval service is unavailable, DVARA applies the configured default immediately and records A2A_APPROVAL_UNAVAILABLE.

Keep the default action set to deny unless the risk owner has accepted a fail-open approval path.

Stop a runaway delegation chain​

From 1.8.3, the A2A delegation guard refuses the first hop that loops back or goes too deep, using a signed chain that each agent forwards. The session loop detector below stays on as its backstop.

Send one stable X-Session-Id across the complete workflow. Without it, DVARA still applies the other controls but cannot join the hops for loop detection.

In 1.8, the detector uses fixed thresholds:

PatternThreshold
Same target agent and skill repeated consecutively5 hops
Cycle containing 2–4 different hops3 repetitions
Rate within one sessionMore than 60 hops per minute
Retained session history100 hops

A detection returns A2A_LOOP_DETECTED, writes an audit event, and kills that session. From 1.8.5 the rate rule is different: past 60 hops a minute a hop is refused with 429 A2A_SESSION_RATE_LIMITED, and the session is not killed, because that is overload, not a loop.

From 1.8.5 the history is shared by the gateways in one cluster. Two gateways refuse a loop at the same hop as one gateway does, so you no longer need session affinity for it. If the cluster does not answer within 50 ms, a gateway judges the hop on its own history, so a loop check never fails a call. Before 1.8.5 each pod kept its own history and caught a loop spread over pods later, or not at all.

From 1.8.5 calls to the same agent and skill that carry the same delegation chain count as one. Planner can ask Budget to price five options at once without the fifth being refused as a loop. Real recursion changes the chain on every round, so it is still refused at the same hop as before. Parallel calls from one chain still count toward the rate.

The kill is scoped to the workspace and session ID. Another workspace using the same caller-supplied ID is unaffected. From 1.8.5 a kill stops the session on every plane for 24 hours: its hops, its tool calls and its model calls. A running message:stream or tasks:resubscribe stream ends with one a2a_session_killed error event, and a hop held for approval is denied and refused with A2A_SESSION_KILLED. See Stop runaway agents.

The same task created again and again​

A finished A2A task cannot restart, so an agent that retries creates a new task each time, often from a new session. The session rules above never see those attempts together. From 1.9.0 DVARA also judges each new task: a message that names no task and gets a task back is compared with the tasks the same API key (and calling agent, when known) created in the last six hours. The same target agent, skill and message content five times running, or a repeating cycle, is a loop, whatever session or gateway each attempt used. A 0.3 caller and a 1.0 caller sending the same text send the same task.

Every such loop writes A2A_LOOP_DETECTED with scope task_creation and the task's ID. What happens next follows the workspace's loop settings, the same ones MCP uses:

Workspace settingWhat the caller sees
Auto-kill off (default)The task, as usual. The loop is only recorded
Auto-kill on429 A2A_LOOP_DETECTED. DVARA cancels the new task at the agent and kills the session. A stream ends with the same error

Polling a task (tasks/get), re-subscribing to it, cancelling it and sending more messages into it are never counted. Many different tasks a minute from one key is not a loop either. The six-hour window is dvara.mcp-gateway.agentic.loop-detection.task-creation-window, shared with MCP task creations.

Stream a long-running response​

Use POST /a2a/{agentId}/message:stream with JSON-RPC method message/stream. Streaming is on by default and uses Server-Sent Events.

Turning streaming off

From 1.8.1, a2a.streaming.enabled: false closes both streaming doorways. message:stream and tasks:resubscribe answer a single SSE error event, a2a_streaming_disabled, before the caller is authenticated, and startup logs a warning so the posture is visible. Earlier releases accepted the setting and ignored it, so the paths had to be blocked at the ingress instead.

The served Agent Card still advertises streaming, so a client that reads the card will try to stream and get the refusal.

Message size​

One outbound message may carry up to a2a.limits.max-message-length characters of text, counted across all of its parts. The default is 50,000, the same as the LLM plane. A larger message is refused with 413 A2A_MESSAGE_TOO_LARGE before policy and the PII scan run, on both message:send and message:stream, and the refusal is audited and recorded as a hop. Set the property to 0 to turn the cap off.

The cap is new in 1.8.1. Before it, the plane had no size limit, and a very large message could occupy a Gateway thread for minutes inside the PII scan.

Response size​

From 1.8.5 a peer's answer has a size limit too. The gateway counts the bytes as it reads them and stops before it parses an answer that is too large:

AnswerSettingDefault
A whole answer to message:send or a task call, and a fetched Agent Carda2a.response-limits.max-body-bytes16 MiB
A message:stream or tasks:resubscribe stream, counted from its first bytea2a.response-limits.max-stream-bytes64 MiB

A workspace can set its own values with the same keys in its metadata. Past a limit the call fails with 502 A2A_RESPONSE_TOO_LARGE, is not retried, and is recorded with policy decision DENY. A stream is cut and ends with an error event. An Agent Card over the limit gives A2A_CARD_UNAVAILABLE, or the cached card is served. Before 1.8.5 a peer's answer of any size was read whole.

In 1.8, response-stream enforcement on the A2A plane is PII-only. LLM guardrail and grounding actions do not run on an A2A peer response. Configure the workspace PII action when the peer reply must be withheld, transformed, or refused before release.

Before opening the peer stream, DVARA authenticates the caller, resolves the agent, evaluates policy, scans the outbound message, checks loops, and enforces the session kill state. In 1.8, this streaming path does not run the human approval or governed-call cap checks used by synchronous message:send.

Response delivery depends on the PII action:

  • LOG releases events as they arrive because it does not withhold content.
  • BLOCK, REDACT, and TOKENIZE hold the complete structured reply, enforce it once the peer stream ends, then release the safe result or an error.

Whole-response enforcement prevents a personal-data value split across several events from escaping detection, but it removes incremental delivery for a withholding action. The default hold limits are 1,000,000 text characters, 8,000,000 serialized bytes, and 20,000 events. Reaching a limit stops the stream instead of releasing partially checked content. The upstream connect timeout is 10 seconds and the read/idle timeout is 300 seconds. See the A2A streaming settings for every key and default.

From 1.8.5 a resumed stream is scanned as a continuation of the one it resumes. Each stream keeps the last 256 characters of every text body for its task (a2a.streaming.resume-carry-characters), in the pod's memory, for up to 10,000 tasks and one hour. A tasks:resubscribe stream is scanned together with it, so a value split at the break, such as alexander.hamilton@treasury before the drop and .example.com after it, is caught. REDACT and TOKENIZE redact the part the resumed stream delivers, BLOCK refuses it, and LOG records the value once. Nothing already delivered is changed. A resume is authorised again from scratch, so a policy change made while the caller was away applies.

Every completed or interrupted stream writes A2A_STREAM_SUMMARY with the enforcement outcome.

Public outcomeMeaning
A2A_STREAM_UNSCANNABLEDeferred delivery exceeded its character, byte, or event bound, so no partially checked reply was released.
A2A_PII_ENFORCEMENT_FAILEDA reply that required protection could not be parsed or safely scanned.
A2A_UPSTREAM_TRUNCATEDThe peer or its response reader failed before a clean end. From 1.8.5 a peer that breaks off a stream is recorded this way again; since 1.8.3 it had been recorded as a clean end.
A2A_STREAM_INTERRUPTEDThe client cancelled or abandoned delivery. This is not an upstream truncation. From 1.8.5 the gateway logs one INFO line naming the agent, the task and the trace id, and stops reading from the peer. It no longer logs an ERROR for each disconnect.

The A2A_STREAM_SUMMARY fields are not aliases for one generic failure. scan_incomplete marks an Immediate scan prefix that reached its bound, scan_failed marks a detector failure after Immediate delivery, truncated identifies an incomplete upstream reply, and error_code records the terminal outcome. A refusal can also be truncated: true when DVARA found a blocking issue in the partial reply received before the upstream failed.

Do not use the 1.8 streamed response-size value for chargeback

The persisted response-size value for A2A message streams and task resubscriptions records the event count instead of serialized bytes. Event count in A2A_STREAM_SUMMARY remains valid. Track the correction in the 1.8 known limitation.

Discover a peer's Agent Card​

Fetch a registered peer's public card through DVARA:

curl --silent https://<dvara-host>/a2a/researcher/.well-known/agent-card.json \
--header 'Authorization: Bearer <your-api-key>'

From 1.9.0 the card comes in the A2A version your client names in the A2A-Version header, and in A2A 1.0 when it names none, so that the official SDK of either version can read its security schemes. An A2A 0.3 client should send A2A-Version: 0.3. See Read the card in your version.

DVARA filters the advertised skills through that registry entry's allow list. An empty allow list exposes every skill in the peer's card. Cards are cached for 3,600 seconds by default; ?refresh=true forces a new fetch. If refresh fails and a cached card exists, DVARA serves the cached copy.

To list every agent your key may use, with each entry pointing at its card on DVARA, turn on the experimental ARD manifest at /a2a/.well-known/ard.json. See List the MCP servers and A2A agents a key may use.

From 1.9.0, Agent Cards use standard HTTP caching in both directions, as A2A 1.0 asks:

  • The card DVARA serves carries Cache-Control: private, max-age=60, a Vary header and an ETag. Send the ETag back in If-None-Match and DVARA answers 304 Not Modified with no body when the card has not changed. DVARA still checks your key and the card first, so a revoked key gets 401, never a 304. The card is marked private because it is built for your workspace: another workspace can see different skills, and gets a different ETag.
  • A peer's card is kept for as long as the peer's Cache-Control or Expires header says, between 30 seconds and one hour. A peer cannot keep an old card in DVARA longer than that, and cannot make DVARA fetch its card on every call. A peer that sends no caching headers is kept for the default 3,600 seconds, as before. When the time is up, DVARA asks the peer with the card's ETag. If the peer answers that nothing changed, DVARA keeps the card it already checked. Only a changed card is checked again, signature included.
SettingDefaultWhat it does
a2a.discovery.served-card-max-age-seconds60How long your client may reuse the card DVARA serves. 0 means ask every time with the ETag
a2a.discovery.card-max-ttl-seconds3600The longest DVARA keeps a peer's card, whatever the peer says
a2a.discovery.card-min-ttl-seconds30The shortest time a peer may ask for. 0 follows the peer exactly

Discovery labels the card VERIFIED, SELF_SIGNED, UNSIGNED, or INVALID. Strict verification is off by default. Set a2a.discovery.require-signed-cards: true to accept only a card verified against the public key pinned for that agent. A card whose signature fails against a pinned key is always refused, even when strict verification is off.

From 1.9.0, DVARA checks each signature the way the card's own A2A version defines. An A2A 1.0 card is checked as the 1.0 specification says: default values are removed before the check. An A2A 1.0 card is never read as a 0.3 card. Cards signed by the A2A reference SDK, for both 1.0 and 0.3, now verify. Before 1.9.0 DVARA checked the card exactly as it was served, so these cards were reported INVALID. With a pinned key, they were refused.

  • A peer that offers both versions can add 0.3 fields to its 1.0 card that it did not sign. When the 1.0 card verifies, DVARA governs and serves only the fields that were signed.
  • A signature that cannot be read is INVALID, never UNSIGNED.
  • A card whose structure does not match its version is treated like a failed fetch. It is not cached, and the last good card is served if there is one.
  • The card DVARA serves does not carry the peer's signatures. They signed the peer's card, not the filtered card DVARA serves. x-dvara-signature-trust says what DVARA found.

From 1.9.0, a pinned key means the card must be signed by that key:

  • If you pinned a public key for an agent, DVARA serves its card only when the card is VERIFIED. An unsigned card is refused with 502 a2a_card_signature_invalid, even when strict verification is off. Before 1.9.0 a peer could remove its signatures and DVARA served the card as UNSIGNED.
  • Each signature must name its key (kid) and its type (typ, set to JOSE), as the A2A 1.0 specification requires. A signature without them is not checked. The A2A reference SDK sets both.
  • DVARA never fetches a key from the URL a signature names (jku), unless you allow it for that agent (from 1.9.0, below). It trusts only the key you pinned.
  • Each refused card is recorded in the audit log as A2A_CARD_REFUSED, with the reason.

If an agent you pinned stops signing its card, its discovery fails until it signs again or you remove the pinned key.

From 1.9.0 you can pin a set of keys (a JWK Set, {"keys": [...]}) instead of one key. DVARA uses the key whose kid the signature names, so you can pin a peer's next key before it rotates.

From 1.9.0 you can also let DVARA fetch the key set a peer's signature points to (jku), with Fetch the card's key set (jku) on the agent's page. It is off by default. DVARA fetches the key set only when no key is pinned, only from the agent's registered address, and only through the same address checks as a call to the agent. A card checked with a key from it shows as SELF_SIGNED, never VERIFIED: a key the peer publishes itself does not prove who the peer is.

Check that a card came from DVARA​

From 1.9.0 DVARA signs every Agent Card it serves, once an owner has made a card signing key in Flightdeck (Agents → Card Signing). The signature uses the format the A2A specification defines, in the version your client asked for, so the official A2A SDKs can check it. Its jku points to the public keys at:

https://<dvara-host>/a2a/.well-known/jwks.json

That address needs no API key. After a key is rotated, the old key stays there for 30 days, so a card signed just before the rotation still checks. With no signing key, DVARA serves cards unsigned, as before.

Govern against the extended Agent Card​

A peer can give an authenticated caller a fuller card than its public one. From 1.8.4, when the public card declares such an extended card, DVARA fetches the extended card from the agent's endpoint with the agent's credential: the stored credential, or for a DELEGATED agent the caller's X-DVARA-Delegated-Authorization token on the discovery request. DVARA then checks that card's signature, filters its skills through the allow list, and serves it marked x-dvara-card-kind: EXTENDED. Before 1.8.4 discovery read only the public card, so the allow list was applied to the wrong card.

  • The extended card is cached per workspace, agent and credential. It is never served to another workspace or to a caller with another credential.
  • With no credential to present, DVARA serves the public card, and the A2A_CARD_DISCOVERED event says why the extended card was skipped.
  • If the extended card cannot be fetched, discovery answers 502 A2A_CARD_UNAVAILABLE rather than serving the public card.

You get the extended card from the same address as the public card: read the card with your key (and your own token for a DELEGATED agent). DVARA does not serve the A2A extended-card method (GetExtendedAgentCard, or agent/getAuthenticatedExtendedCard in 0.3). So the card DVARA serves always says the method is not offered: capabilities.extendedAgentCard (1.0) and supportsAuthenticatedExtendedCard (0.3) are false wherever the peer set them. An SDK that reads the card does not call a method DVARA would refuse. Before 1.9.0 the card kept the peer's true, and a client that followed it got an error.

A2A_CARD_DISCOVERED records card_kind, extended_card_declared, extended_card_skipped and extended_only_skill_count. For an extended card it also records public_card_trust_state beside the served card's card_trust_state.

Continue and manage a peer task​

The A2A Gateway exposes the v0.3 task operations beneath the registered agent:

EndpointJSON-RPC methodPurpose
POST /a2a/{agentId}/tasks:gettasks/getRead one task
POST /a2a/{agentId}/tasks:listtasks/listList tasks
POST /a2a/{agentId}/tasks:canceltasks/cancelAsk the peer to cancel a task
POST /a2a/{agentId}/tasks:resubscribetasks/resubscribeReattach to an SSE task stream

Task operations authenticate the caller, resolve the workspace-scoped agent, apply A2A policy, and write their own audit event. Task read responses are PII-scanned. These operations do not represent a new delegation, so they do not run message-loop detection or human approval. A tasks/resubscribe response uses the same PII-only streaming enforcement and limits as message:stream.

DVARA endpointForwarded A2A methodPurpose
POST /a2a/{agentId}/pushConfig:settasks/pushNotificationConfig/setCreate or replace a push destination; the URL passes the egress/SSRF check.
POST /a2a/{agentId}/pushConfig:gettasks/pushNotificationConfig/getRead one push configuration.
POST /a2a/{agentId}/pushConfig:listtasks/pushNotificationConfig/listList push configurations.
POST /a2a/{agentId}/pushConfig:deletetasks/pushNotificationConfig/deleteDelete one push configuration.

Send the A2A method's parameter object as the JSON body and use the same Bearer and session headers as tasks:*. DVARA refuses an unsafe webhook URL before forwarding set. From 1.8.5 the webhook must be https:// too.

Receive push notifications through DVARA​

A peer posts a task's result to the push-notification webhook the workspace set. Before 1.8.5 that result went from the peer straight to the workspace, with no PII scan, no audit record and no hop. From 1.8.5 the result comes through the gateway:

  1. On pushConfig:set, and on a push config inside message:send or message:stream, the gateway gives the peer a callback of its own and a notification token, in place of the workspace's webhook URL, token and credentials. pushConfig:get and pushConfig:list still show the workspace's own URL and token.
  2. When the peer posts, the gateway checks the sender: the callback it sealed, the notification token it issued, and, for an agent with a pinned key, a signed token.
  3. It drops a repeat, applies the workspace's PII action, writes an A2A_PUSH_NOTIFICATION audit event and records a hop.
  4. It delivers the notification to the workspace's webhook, trying up to 3 times. If no try succeeds, the peer gets 502 and can send again. The gateway stores nothing.

A notification is read only up to a2a.push.max-body-bytes (default 1 MiB).

Set the gateway's public URL, as peers reach it, in A2A_PUSH_CALLBACK_BASE_URL. In the Helm chart, set llmGatewayServer.a2a.pushCallbackBaseUrl. Until it is set, a push config is refused with 503 A2A_PUSH_NOT_AVAILABLE and never forwarded.

A workspace can keep the old direct delivery with the setting a2a.push.delivery=direct. The webhook URL is still checked and must be https://, but the peer then posts to it without the gateway, so those notifications are not scanned, audited or recorded.

Choose how DVARA authenticates to the peer​

Registered agents use STORED auth by default. DVARA resolves the agent's configured credential and presents it using API_KEY, HTTP, OAUTH2, OPENID_CONNECT, or MUTUAL_TLS, or, from 1.9.0, signs each request with AWS_SIGV4.

From 1.8.4 the Flightdeck agent forms offer From the Agent Card, the default for a new agent. Flightdeck reads the peer's card and takes the one kind of scheme it declares. If the card cannot be read, declares no scheme, or declares more than one kind, the save is refused and you choose the scheme. Nothing falls back to API_KEY.

A scheme you choose is compared with the card when you save the agent and each time a gateway fetches the card. A mismatch is a warning, never a refusal: the hop still uses the scheme you chose, so a working configuration keeps working. A mismatch is a scheme the card does not accept, an API key the card wants in another header, HTTP basic, or a DELEGATED bearer token to a peer that accepts no bearer scheme. The warning appears:

  • on the agent in the Flightdeck (a Card mismatch badge in the list, and the finding on the form);
  • on the served card as x-dvara-auth-scheme-finding; and
  • in the A2A_CARD_DISCOVERED, A2A_AGENT_CREATED and A2A_AGENT_UPDATED audit events.

Flightdeck also records, for each hop, the auth DVARA presented: the scheme, DELEGATED_BEARER, or NONE.

A stored OAuth2 or OpenID Connect credential is presented as a bearer token. From 1.9.0, an OAUTH2 or OPENID_CONNECT agent can instead let DVARA get the token itself with the OAuth 2.0 client credentials grant, renew it before it expires, and refuse the hop if no token can be obtained. A blank token URL is taken from the agent's Agent Card. See Let DVARA get OAuth tokens. MUTUAL_TLS presents a client certificate for each agent; see the next section.

A DELEGATED agent acts for the caller. From 1.9.0 it says how the caller's X-Dvara-Delegated-Authorization token reaches the peer: DVARA exchanges it for a token made for the peer (OAuth 2.0 token exchange), or, by your explicit choice, passes it through unchanged. An agent with neither is refused, and a caller's own API key in that header is never sent: the call is refused with A2A_TOKEN_PASSTHROUGH. See Exchange a caller's token.

AWS_SIGV4 is for an agent behind AWS IAM, such as one on Amazon Bedrock AgentCore Runtime. An Agent Card cannot declare it, so you choose it. DVARA signs every message, stream, task call and Agent Card request with AWS Signature Version 4, for the region and service you set, with a stored access key or (set by a platform administrator) the gateway's own AWS identity. A hop that cannot be signed is refused with A2A_UPSTREAM_SIGV4_FAILED and nothing is sent. See Sign requests behind AWS IAM.

DVARA reads an agent's Agent Card at /.well-known/agent-card.json on the endpoint's host. From 1.9.0, when the host has no card there, DVARA also looks at the same path under the endpoint's own path, which is where AgentCore Runtime serves it. It never reads a card from another host.

Present a client certificate to a peer that uses mutual TLS​

Some peers accept a call only from a caller that proves itself with a client certificate during the TLS handshake. Their Agent Card declares a mutualTLS scheme. From 1.9.0, DVARA presents a certificate to such a peer.

  1. On the Credentials page, add a credential that holds the client certificate and its private key. Paste the PEM text: the certificate (and any intermediate certificates), then the private key. A PKCS#12 file encoded in base64, with no password, also works. The credential is encrypted like any other, so the key needs no password of its own.
  2. On the agent, choose the MUTUAL_TLS auth scheme and set Credential Reference to that credential.

Flightdeck checks the certificate when you save the agent. A value that is not a certificate with its matching key, or a certificate that has expired, is refused with the reason. The agent page then shows the certificate's subject, issuer, expiry date and SHA-256 fingerprint. It never shows the key.

What DVARA does with it:

  • It presents the certificate only on connections to the agent's registered address: messages, streams, task calls and the extended Agent Card. It still checks the peer's own certificate as before.
  • When you rotate the credential, the next call uses the new certificate. No restart is needed.
  • It refuses the call, and sends nothing, when the agent has no certificate (A2A_CLIENT_CERT_REQUIRED) or the certificate cannot be used or has expired (A2A_CLIENT_CERT_INVALID). It also refuses with A2A_CLIENT_CERT_REQUIRED when the peer's Agent Card accepts only mutual TLS but the agent uses another scheme, since the peer would reject that call anyway.
  • Each call records the certificate's SHA-256 fingerprint (client_cert_sha256), never the key. Changing an agent's certificate writes A2A_CLIENT_CERT_CHANGED.

Thirty days before the certificate expires, the agent page shows a warning and Flightdeck raises a notification in the bell for the workspace. Rotate the credential before the date shown.

Use DELEGATED when the peer should enforce the calling user's permissions:

X-DVARA-Delegated-Authorization: Bearer <upstream-user-token>

Authorization still carries the DVARA API key. A delegated target called without the second token returns A2A_DELEGATED_TOKEN_REQUIRED. DVARA does not store or log the delegated token; the A2A audit chain records a fingerprint in A2A_DELEGATION_RECORDED.

Govern A2A extensions​

An A2A extension, named by a URI, can add data to an Agent Card, rules for messages, new methods and new task states. A client asks for extensions in the A2A-Extensions header, and an agent can mark an extension as required. The A2A specification says to treat extension data as untrusted.

From 1.9.0 DVARA uses an extension on a call only when the client asked for it, the workspace allows it and the agent's card declares it. Everything else is removed and recorded:

  • Nothing is allowed by default. An operator lists the extensions a workspace may use in its a2a.extensions.allowed setting, set by a GitOps import. An entry is an exact URI, which pins the extension's version, or a prefix ending in *, such as https://ext.example.com/*. The gateway setting of the same name, empty by default, applies to workspaces without their own.
  • Only the agreed extensions reach the agent. DVARA sends them in A2A-Extensions and removes other extension URIs from the extensions list of messages and artifacts, both ways. Each extension it did not use, and each URI it removed, is written to the audit log (A2A_EXTENSION_DENIED, A2A_EXTENSION_STRIPPED).
  • A required extension is never skipped. If the agent requires an extension the workspace does not allow, the agent cannot be reached from that workspace (a2a_extension_not_allowed). If the workspace allows it but the client did not ask, the client gets the specification's ExtensionSupportRequiredError.
  • The card shows only what will work. The Agent Card DVARA serves lists only the extensions the workspace allows.
  • New methods are not forwarded. DVARA cannot inspect a method it does not know, so it does not pass one on.
workspaces:
- name: research
metadata:
a2a.extensions.allowed: https://a2a-protocol.org/extensions/timestamp/v1, https://ext.example.com/*

See Ask for an A2A extension.

Know the 1.8 protocol boundary​

From 1.9.0 DVARA serves both A2A v0.3 and v1.0. A2A v1.0 renamed every method (message/send became SendMessage) and changed how messages, tasks and stream events are written. DVARA reads every request in the version it names, calls each peer in the first version and binding on that peer's Agent Card that it can speak (JSON-RPC, and from 1.9.0 also HTTP+JSON at A2A 1.0), and translates between the two when they differ, so a v1.0 client can call a v0.3 agent and the reverse. It reads every message into one form before any check runs, so governance covers every kind of message part in either version, and it refuses a part or value it does not understand rather than skip it. See Send A2A 1.0 messages, Name the method and How DVARA talks to each peer. A peer that lists nothing DVARA can speak is refused with a2a_peer_protocol_unsupported before anything is sent. From 1.9.0 DVARA calls the address the chosen entry lists, once it passes the registered endpoint's checks, and refuses the call with a2a_peer_url_refused when it does not (see Which address DVARA calls). The card DVARA serves names DVARA's own address and the versions DVARA serves, in both the v1.0 and v0.3 card shapes.

Where to go next​