Call other agents over A2A
Give an agent the skills of other agents as tools. The calling agent can serve
any facade — WebSocket, REST or A2A. The agent it calls must serve an a2a
facade, or be an external A2A agent with a URL.
Prerequisites
Section titled “Prerequisites”- A target AgentRuntime in the same namespace with a
type: a2afacade whosea2a.agentCard.skillslists what it offers. Each skill becomes one tool. - A credential the target accepts, when the target sets
spec.externalAuth. For a client key, see Configure authentication.
Store the credential
Section titled “Store the credential”Put the target’s token in a Secret, under the key token:
kubectl create secret generic billing-agent-key \ --context my-cluster -n my-namespace \ --from-literal=token="<token minted for the caller>"Declare the client
Section titled “Declare the client”Add the target to the caller’s spec.a2aClients:
apiVersion: omnia.altairalabs.ai/v1alpha1kind: AgentRuntimemetadata: name: supportspec: facades: - type: websocket a2aClients: - name: billing agentRuntimeRef: name: billing-agent exposeAsTools: true authentication: secretRef: name: billing-agent-key timeout: 2m # promptPackRef, providers, ...For an agent outside the cluster, set url to its base URL in place of
agentRuntimeRef. Set timeout above the longest turn the target takes: a
call that runs longer fails here while the target keeps working.
With exposeAsTools: true, the runtime fetches the target’s agent card when a
conversation opens and offers each skill as a2a__<agent>__<skill>. The pack
does not need to list these tools.
On an Enterprise agent, the policy broker decides each call under the
registry a2a. Write a ToolPolicy with selector.registry: a2a that permits
the calls; see ToolPolicy.
Rotate the credential
Section titled “Rotate the credential”Update the Secret’s token. The runtime reads the token file on every call,
so the next call after the kubelet syncs the Secret uses the new value — within
about a minute, with no restart. Keep the old token valid on the target until
then.
How calls share a conversation
Section titled “How calls share a conversation”The first call a conversation makes to a target starts a remote conversation.
Every later call from that conversation to that target sends the target’s
contextId, so the target sees one session with its history.
Calls from one conversation to one target run one at a time, even when the model asks for several in one turn, so two turns never run at once on one remote conversation. Calls to other targets, and calls from other conversations, run in parallel.
The runtime keeps these ids in memory, per pod. It drops a conversation’s ids when the conversation ends, after 50 minutes without a call, and when a call fails. A runtime restart, or a conversation’s next message landing on another replica, drops them too. In every case the next call starts a new remote conversation. When the target refuses a remembered id as expired, the runtime sends the call again without it, and the model sees one successful call.
Two clients that resolve to one endpoint are refused: the second is reported not ready.
Headers a ToolPolicy injects are added to each request.
Call on behalf of the end user
Section titled “Call on behalf of the end user”A call to an agentRuntimeRef target with exposeAsTools carries a
delegation token on the X-Omnia-Delegation header, as tool calls to sandboxes
and MCP servers do. The runtime asks the operator for one on every call; the
operator’s client caches it for up to about five minutes. It names:
sub: the end user of the calling conversation;act.sub: the calling agent (agent:<namespace>/<name>);aud: the target’s facade,agent-facade:<namespace>/<target>;origin: how the caller established the user. Onlyoidcmeans the operator verified them;del: how many more agent hops the delegation allows.
The operator mints one only for a target the calling agent lists as an
agentRuntimeRef client in its own namespace. A call to a url target never
carries a token. When the operator has no delegation mint configured, calls
carry none. When the mint refuses, the call fails.
What the target checks
Section titled “What the target checks”The target verifies the token against the operator’s JWKS: the signature, its own audience, the expiry, a calling agent and a hop left. It also checks the credential the call arrived with:
- The delegation is accepted only on a client key or the shared token, the credentials an agent calls with. On an OIDC, edge-asserted or dashboard credential it is refused.
- A client key whose scopes set
a2a_callerto an agent (agent:<namespace>/<name>, or<namespace>/<name>) accepts only tokens whoseact.subis that agent. The binding is opt-in, and recommended: set it on every key you issue to a calling agent. Without it the target can only check that the credential is an agent’s kind, not that it is that agent’s.
The operator mints a delegation token only for an agent it can identify from its ServiceAccount. Agents that share one ServiceAccount (a workspace-wide runtime ServiceAccount) cannot make delegated calls: the mint refuses them, and those calls fail.
A token that fails any check is a 401 with the body invalid delegation token; the reason is in the target’s debug log. The call is never served as
the client credential instead. Without the header, the target sees only the
client credential, as before.
What a valid token does
Section titled “What a valid token does”origin: oidc (verified) | Any other origin (asserted) | |
|---|---|---|
Session owner and identity.endUser | The user | The user, for a new conversation |
identity.origin | oidc | a2a-asserted |
| Continue the user’s own sessions on the target | Yes | No: refused as expired |
| Continue a conversation another calling agent opened for the user | No | No |
| Continue the conversation this caller opened | Yes | Yes, with a session archive |
| User-tier memory | Yes | No |
A verified user whose proof the target cannot obtain from the operator keeps
the session but is presented to policy as a2a-asserted and gets no
user-tier memory. A verified delegation from any agent that lists the target
as a client can continue that user’s own sessions on the target, as long as
those sessions are not bound to another calling agent.
The calling agent gains no authority on the target. The target’s own
ToolPolicy applies to the user and the token’s wrole is not carried over.
identity.origin describes the user, so to require a verified user test
identity.origin == "oidc". The calling client’s credential is kept apart, so
its role is never read as the user’s. The target records the calling agent on
the session’s state (a2a.delegation.actor, a2a.delegation.origin) and
passes these claims to its ToolPolicy:
| Claim | Value |
|---|---|
identity.claims.a2a_delegation_actor | The calling agent |
identity.claims.a2a_delegation_origin | The user’s origin at the caller |
identity.claims.a2a_delegation_depth | Agent hops left for an onward call |
identity.claims.a2a_delegation_caller_origin | The calling client’s credential origin (client-key, shared-token) |
identity.claims["a2a_delegation_caller.<name>"] | Each claim of the calling client’s credential, such as its role |
A rule written for undelegated A2A calls that tests identity.origin == "client-key" or identity.claims.role does not match a delegated call. Test
the a2a_delegation_caller* claims for those.
A credential on any surface that carries a claim named a2a_delegation_* has
it dropped.
Limit the depth
Section titled “Limit the depth”By default a user’s delegation allows one agent hop: agent A may call B for
the user, and B may not call C for that user. The operator’s mint refuses the
token for the hop past the limit, so B’s call to C fails. Raise the limit with
the operator flag --delegation-max-a2a-depth.
The mint enforces the limit for a verified user. For an asserted user each calling agent passes the hops left on to the mint, which can only lower the limit, so the bound holds only while the calling agents’ pods are honest.
A call that carries a delegation token never follows an HTTP redirect.
Reach a target through a proxy
Section titled “Reach a target through a proxy”A2A calls use the runtime’s HTTPS_PROXY, HTTP_PROXY and NO_PROXY, as
MCP and HTTP tool calls do. When you set a proxy in spec.runtime.extraEnv,
add .svc and .svc.cluster.local to NO_PROXY, or calls to in-cluster
targets go through the proxy too.
Verify
Section titled “Verify”Check that every client resolved:
kubectl get agentruntime support --context my-cluster -n my-namespace \ -o jsonpath='{.status.a2a.clients}'Each entry has ready: true and a resolvedURL. The A2AClientsResolved
condition is True.
Troubleshooting
Section titled “Troubleshooting”A client is not ready
Section titled “A client is not ready”status.a2a.clients[].error says why. target AgentRuntime has no a2a facade
means the target needs a type: a2a facade. cross-namespace agentRuntimeRef is not supported means the ref names another namespace. An unresolved client
is left out of the runtime.
The agent has no a2a__ tools
Section titled “The agent has no a2a__ tools”The runtime logs a2a agent tools not registered when it cannot fetch the
target’s card, and a2a agent exposes no tools when the card lists no skills.
Check that exposeAsTools is true.
Calls fail with a2a client auth
Section titled “Calls fail with a2a client auth”The runtime cannot read the token file. Check that the Secret exists and has a
token key. The pod starts without the Secret; its file appears once the
Secret exists and the kubelet next syncs the volume. A token key that is
empty also fails each call.
The resource is refused as mutually exclusive
Section titled “The resource is refused as mutually exclusive”The agent sets both spec.a2aClients and the deprecated
spec.facades[].a2a.clients. Move every client to spec.a2aClients and remove
clients from the facade.