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.
Not included in DVARA Open Source.
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:
- authenticates the API key and resolves its workspace;
- finds the target agent in that workspace's registry;
- records
A2A_HOP_INTENTbefore the peer is contacted; - evaluates matching A2A policy and scans the outbound message for PII;
- checks the session for a repeated hop, cycle, or excessive rate;
- applies any matching human-approval rule on a synchronous hop;
- authenticates to the peer and forwards an allowed request; and
- 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?
| Control | 1.8 default | What that means |
|---|---|---|
| API-key authentication | Required | Anonymous A2A calls are refused |
| API-key scopes | Empty means unrestricted | A scoped key needs a2a:send for messages and cancellation, or a2a:read for task reads |
| A2A policy | Default allow | Inside 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 action | LOG unless the workspace overrides it | Detection is recorded and the original content continues |
| Delegation guard | enforce, 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 detection | On for hops with a session ID | Fixed 1.8 thresholds detect repetition, short cycles, and more than 60 hops per minute |
| Loop response | Kill the session | The 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 size | 16 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 notifications | Through 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 engine | On, with no matching rules | A synchronous hop does not pause until the workspace names required agents or skills |
| Approval timeout | 300 seconds, then deny | A matching synchronous hop fails closed when nobody decides |
| Signed-card requirement | Off | Discovery 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:
params.metadata.skill;params.message.metadata.skill; orparams.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
DENYorWARNrule on that hop's source and target applies as if the skill matched. AnALLOWrule 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
403andA2A_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.
| Action | Outbound message | Peer response |
|---|---|---|
LOG | Record detection and forward the original text | Record detection and release the original reply |
REDACT | Replace detected values before dispatch | Replace newly detected values before release |
TOKENIZE | Replace provider-bound values with recoverable tokens when tokenization is available | Irreversibly redact newly detected response values |
BLOCK | Return A2A_PII_DETECTED; do not contact the peer | Return 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:
| Pattern | Threshold |
|---|---|
| Same target agent and skill repeated consecutively | 5 hops |
| Cycle containing 2–4 different hops | 3 repetitions |
| Rate within one session | More than 60 hops per minute |
| Retained session history | 100 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 setting | What the caller sees |
|---|---|
| Auto-kill off (default) | The task, as usual. The loop is only recorded |
| Auto-kill on | 429 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.
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:
| Answer | Setting | Default |
|---|---|---|
A whole answer to message:send or a task call, and a fetched Agent Card | a2a.response-limits.max-body-bytes | 16 MiB |
A message:stream or tasks:resubscribe stream, counted from its first byte | a2a.response-limits.max-stream-bytes | 64 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:
LOGreleases events as they arrive because it does not withhold content.BLOCK,REDACT, andTOKENIZEhold 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 outcome | Meaning |
|---|---|
A2A_STREAM_UNSCANNABLE | Deferred delivery exceeded its character, byte, or event bound, so no partially checked reply was released. |
A2A_PII_ENFORCEMENT_FAILED | A reply that required protection could not be parsed or safely scanned. |
A2A_UPSTREAM_TRUNCATED | The 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_INTERRUPTED | The 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.
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, aVaryheader and anETag. Send theETagback inIf-None-Matchand DVARA answers304 Not Modifiedwith no body when the card has not changed. DVARA still checks your key and the card first, so a revoked key gets401, never a304. The card is markedprivatebecause it is built for your workspace: another workspace can see different skills, and gets a differentETag. - A peer's card is kept for as long as the peer's
Cache-ControlorExpiresheader 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'sETag. If the peer answers that nothing changed, DVARA keeps the card it already checked. Only a changed card is checked again, signature included.
| Setting | Default | What it does |
|---|---|---|
a2a.discovery.served-card-max-age-seconds | 60 | How long your client may reuse the card DVARA serves. 0 means ask every time with the ETag |
a2a.discovery.card-max-ttl-seconds | 3600 | The longest DVARA keeps a peer's card, whatever the peer says |
a2a.discovery.card-min-ttl-seconds | 30 | The 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, neverUNSIGNED. - 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-trustsays 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 with502 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 asUNSIGNED. - Each signature must name its key (
kid) and its type (typ, set toJOSE), 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_DISCOVEREDevent says why the extended card was skipped. - If the extended card cannot be fetched, discovery answers
502 A2A_CARD_UNAVAILABLErather 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:
| Endpoint | JSON-RPC method | Purpose |
|---|---|---|
POST /a2a/{agentId}/tasks:get | tasks/get | Read one task |
POST /a2a/{agentId}/tasks:list | tasks/list | List tasks |
POST /a2a/{agentId}/tasks:cancel | tasks/cancel | Ask the peer to cancel a task |
POST /a2a/{agentId}/tasks:resubscribe | tasks/resubscribe | Reattach 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 endpoint | Forwarded A2A method | Purpose |
|---|---|---|
POST /a2a/{agentId}/pushConfig:set | tasks/pushNotificationConfig/set | Create or replace a push destination; the URL passes the egress/SSRF check. |
POST /a2a/{agentId}/pushConfig:get | tasks/pushNotificationConfig/get | Read one push configuration. |
POST /a2a/{agentId}/pushConfig:list | tasks/pushNotificationConfig/list | List push configurations. |
POST /a2a/{agentId}/pushConfig:delete | tasks/pushNotificationConfig/delete | Delete 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:
- On
pushConfig:set, and on a push config insidemessage:sendormessage: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:getandpushConfig:liststill show the workspace's own URL and token. - 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.
- It drops a repeat, applies the workspace's PII action, writes an
A2A_PUSH_NOTIFICATIONaudit event and records a hop. - It delivers the notification to the workspace's webhook, trying up to 3
times. If no try succeeds, the peer gets
502and 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_CREATEDandA2A_AGENT_UPDATEDaudit 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.
- 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.
- 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 withA2A_CLIENT_CERT_REQUIREDwhen 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 writesA2A_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.allowedsetting, set by a GitOps import. An entry is an exact URI, which pins the extension's version, or a prefix ending in*, such ashttps://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-Extensionsand removes other extension URIs from theextensionslist 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'sExtensionSupportRequiredError. - 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/*
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
- Stop runaway agents and approve risky actions for the shared agent-control model.
- Manage A2A governance in Flightdeck to register agents, inspect hops, and resolve approvals.
- Govern tool calls with the MCP Gateway for an agent's calls to tools rather than peer agents.