Skip to main content
Version: 1.8.0

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.

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 allowThe first matching ALLOW or DENY rule decides; 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 v0.3 has no first-class skill field on message/send. DVARA reads the first nonempty value from:

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

Use params.metadata.skill, as in the example. If the request carries no skill, agent-scoped rules can still match, but skill-scoped rules and approval patterns cannot.

A2A policies are separate from LLM and MCP policies. They match source, target, and skill, use first-match-wins for ALLOW and DENY, and accumulate matching WARN rules. A policy denial returns HTTP 403 with A2A_POLICY_DENIED. An evaluation failure also fails closed with HTTP 403 and A2A_POLICY_ERROR.

Choose how PII is handled​

DVARA scans text 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.

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.

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>'

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.

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.

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.

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.

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.

In 1.8, stored OAuth2 and OpenID Connect credentials are presented as bearer tokens; DVARA does not run a grant flow. MUTUAL_TLS uses the deployment's TLS context rather than a separate client certificate for each agent.

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.

Know the 1.8 protocol boundary​

DVARA sends A2A v0.3 method names and message shapes to peers. A peer that accepts only A2A v1.0 methods such as SendMessage cannot receive a hop from DVARA 1.8. Signed Agent Card verification is supported, but the served card still uses the v0.3 shape.

Where to go next​