# 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

- A target AgentRuntime in the same namespace with a `type: a2a` facade whose
  `a2a.agentCard.skills` lists 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](/how-to/security/configure-authentication/#add-client-keys).

## Store the credential

Put the target's token in a Secret, under the key `token`:

```bash
kubectl create secret generic billing-agent-key \
  --context my-cluster -n my-namespace \
  --from-literal=token="<token minted for the caller>"
```

## Declare the client

Add the target to the caller's `spec.a2aClients`:

```yaml
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: AgentRuntime
metadata:
  name: support
spec:
  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](/reference/policies/toolpolicy/).

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

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

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. Only `oidc` means 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

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_caller` to an agent
  (`agent:<namespace>/<name>`, or `<namespace>/<name>`) accepts only tokens
  whose `act.sub` is 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

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

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

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

Check that every client resolved:

```bash
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

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

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`

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

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.

## Related pages

- [AgentRuntime `a2aClients`](/reference/core/agentruntime/#a2aclients)
- [Configure authentication](/how-to/security/configure-authentication/)
- [Tool execution model](/explanation/agents/tool-execution-model/)