---
title: "Connect an outside A2A agent"
description: "Add a remote A2A service to your tenant catalog."
canonical_url: "https://docs.oz0.ai/build/apps/remote-agents/a2a"
---
# Connect an outside A2A agent

> Add a remote A2A service to your tenant catalog.

For: application developers.

Add a remote A2A service to your tenant catalog.

## Outside A2A agents

An agent that runs somewhere else and speaks [A2A](https://a2a-protocol.org/latest/specification/) (1.0, or 0.3 where its card offers only that) can be an agent of your tenant: jobs start it, flows and other agents delegate to it, people see it in the catalog. Its work happens at its own host; what stays yours is everything around it: the job's budget and approvals, the kill switches, the audit log, the taint of its answer and the recipient map.

Install the verified plugin `a2a-remote` once. It holds no keys and reaches no host of its own:

```bash
oz0 plugin install <git url of the repository> --path plugins/a2a-remote
```

Then define the outside agent in a plugin of yours, with its host in the plugin's `egress:` and its token in the plugin's secrets (`plugins/a2a-remote/templates/outside-agent.yaml` is one to copy):

```yaml [oz0-plugin.yaml]
name: acme-partners
egress: [agents.partner.example]       # installing the plugin grants this host
requires:
  secrets: [ACME_PARTNERS_TOKEN]
agents: [agents/]
```

```yaml [agents/acme-partners-translator.yaml]
name: acme-partners-translator
description: Translates text, at a partner's A2A agent
runtime: remote:a2a
input: text
output: text
harness:
  card: https://agents.partner.example/.well-known/agent-card.json
  skill: translate
  signature: required
  auth: {kind: bearer, secret: ACME_PARTNERS_TOKEN}
  task_usd: 0.05
limits:
  timeout: 10m
  max_steps: 5
```

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        card
      </code>
    </td>
    
    <td>
      The agent's card, https (http only to a loopback address the tenant's egress list names, for tests). <code>
        ${NAME}
      </code>
      
       in it, and in <code>
        auth
      </code>
      
      , is the plugin's <a href="https://docs.oz0.ai/admin/secrets/settings#settings-are-not-secrets">
        setting
      </a>
      
       of that name, so the address can be the tenant's to set.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        skill
      </code>
    </td>
    
    <td>
      A skill ID the card must list; the job fails when the card stops listing it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        signature
      </code>
    </td>
    
    <td>
      <code>
        required
      </code>
      
       refuses a card that is not signed, or whose signature cannot be checked. Without it a card's signature is checked whenever its key can be found, and a signature that does not verify always fails the job.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        key
      </code>
    </td>
    
    <td>
      The <code>
        kid
      </code>
      
       the card's signature must have: a pin on the key.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        jwks
      </code>
    </td>
    
    <td>
      Where the card's signing keys are, for a card whose signatures name only a <code>
        kid
      </code>
      
       (A2A makes <code>
        jku
      </code>
      
       optional): the URL of a JWK Set on the card's origin, or the set itself, <code>
        {keys: [...]}
      </code>
      
      , with public keys only, as the agent's host handed them to you. When set, it is used instead of each signature's <code>
        jku
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        version
      </code>
    </td>
    
    <td>
      <code>
        1.0
      </code>
      
       or <code>
        0.3
      </code>
      
      ; by default the version the card's interface names, 1.0 first.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        auth
      </code>
    </td>
    
    <td>
      <code>
        {kind: bearer, secret: NAME}
      </code>
      
       sends the plugin's secret as a bearer token; <code>
        {kind: oauth2_client_credentials, token_url, client_id, scope, secret: NAME, client_auth: post or basic}
      </code>
      
       gets a token from a token endpoint with the plugin's client secret, for each segment, and keeps it in memory only.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        task_usd
      </code>
    </td>
    
    <td>
      What one task costs at the outside agent: charged to the job when the task starts.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        poll
      </code>
    </td>
    
    <td>
      How often the task is read while it runs, 500ms to 1m (2s by default, slowing to 10s while nothing changes).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        session_header
      </code>
    </td>
    
    <td>
      A header the host wants a stable session ID in (AgentCore's <code>
        X-Amzn-Bedrock-AgentCore-Runtime-Session-Id
      </code>
      
      ): a value derived from the job, never the job's ID.
    </td>
  </tr>
</tbody>
</table>

`plugin lint` checks the options, and warns when the card's host is in neither the plugin's nor the agent's `egress:`.

### An outside agent is its card's origin

An outside agent is known by its card's origin (scheme, host and port) and its name: `translator@https://agents.partner.example/.well-known/agent-card.json`. Two agents of one name at two hosts are two agents. Everything a job sends goes to the card's origin and nowhere else:

- every request (the card, its key set, the token endpoint, every message) is checked against the tenant's egress list and the plugin's, as a sandboxed tool's are: names resolved once, internal addresses refused, TLS verified, no redirect followed;
- a card whose interface is on another origin is refused, and so is a signature whose key (`jku`) is elsewhere;
- a card that moves to another host is another agent: its host needs its own egress entry, so its plugin's next version needs a new install grant.

The card is read again at every segment, so a card that changed its interface's origin, its key or its skills stops the job at the next one.

How the card was checked is in the audit log, on the first entry of each segment (`card`): `signed <kid>`, `unsigned`, or `unverified: <why>` for a card signed by a key that cannot be found (no `jku` and no `jwks`). An unverified card is taken, as an unsigned one is, unless the definition sets `signature: required`, `key` or `jwks`. A card's name loses control and formatting characters before it names the agent anywhere.

### What happens in a job

The job's input is the first message; the task's ID is the session's, so a task outlives a node that was following it. The node reads the task every few seconds; streaming and push notifications are never used, so the outside agent never gets an address to call back.

- It asks something (`TASK_STATE_INPUT_REQUIRED`): an agent with `tools: [oz.ask_user]` asks a person the question; any other waits for the approval of `a2a.continue`, with the question as its input, through the [approvals](https://docs.oz0.ai/agents/approvals) ladder and deadline. A yes goes back as `{"approve": true}`; a no, a deadline or nobody answering cancels the task and fails the job, so a task never waits past its approval. A question asked again after it was answered, even word for word, is asked again: one decision answers one question.
- It asks for credentials (`TASK_STATE_AUTH_REQUIRED`): the task is cancelled and the job fails. A credential is never asked for or passed on at an outside agent's request.
- It answers: the task's artifacts are the job's answer (one data part as JSON, text parts joined, anything else as a list). An artifact with `metadata["ai.oz0/sha256"]` must match its parts' hash. File parts are refused.
- The job is cancelled, killed or out of budget: the task is cancelled at the outside agent (`CancelTask`), also between segments. A [kill switch](https://docs.oz0.ai/govern/kill-switches) on the agent, the tenant or `a2a.session` does it within a second.
- One message is sent at a time; the context the agent answers in is the job's own and never offered to another job.

Its answer is untrusted input: the job is [tainted](https://docs.oz0.ai/govern/taint-and-labels) with the source `a2a` from the task's start, as a harness session's is. In a shadow run the agent is not called at all.

### Budgets: what they bound, and what they cannot

<table>
<thead>
  <tr>
    <th>
      Bounded by the job
    </th>
    
    <th>
      How
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Time
    </td>
    
    <td>
      <code>
        limits.timeout
      </code>
      
      : the task is cancelled when it runs out
    </td>
  </tr>
  
  <tr>
    <td>
      Messages
    </td>
    
    <td>
      <code>
        limits.max_steps
      </code>
      
      : the messages the job sends the agent, the first and every answer, counted over the whole job, also when the task moves to another node
    </td>
  </tr>
  
  <tr>
    <td>
      Money
    </td>
    
    <td>
      <code>
        task_usd
      </code>
      
      , charged to the job (reported by the plugin <code>
        a2a-remote
      </code>
      
      , service <code>
        a2a
      </code>
      
      , unit <code>
        tasks
      </code>
      
      ) when the task starts: the job's budget and its owner's quota stop it as they stop a remote session
    </td>
  </tr>
</tbody>
</table>

An outside agent reports no tokens and makes no model call through your gateway, so token budgets and reservations do not reach it. Nothing here bounds what the outside agent spends, calls or keeps on its own side: that is its operator's, under your agreement with them.

### The audit log

Every exchange is in the [audit log](https://docs.oz0.ai/govern/audit-trail) before the segment that made it returns: `run.a2a.task` when a task starts or ends, with its state; `run.a2a.message` for each message sent or received; `run.a2a.artifact` for each artifact of the answer. Each names the outside agent, the task, the size and the SHA-256 of what was exchanged, never its content; the context's ID is in the entry's private part.

A segment whose exchange the log did not take fails the job, and a node whose supervisor is too old to record these exchanges refuses to start an outside agent's task at all: update the node.

### Calls back into this cluster

An outside agent may be another of your own agents, through this cluster's own `/a2a`. A message to one of the cluster's own addresses names the job it comes from (`metadata["ai.oz0/origin-job"]`), and the server refuses a start that would close a delegation loop ([hardening](https://docs.oz0.ai/build/apps/a2a#hardening)); the same holds for a plugin's [remote MCP tool](https://docs.oz0.ai/build/parts/tools) that points at the cluster's own `/mcp`. A call to any other address never carries a job's ID. The node trusts the cluster's CA for those calls.

The edge tells nodes the cluster's addresses: each edge's advertised API address, `--public-edge-url`, `--public-url`, the management port's advertised address where the same server serves it, the certificate's extra names (`--san`) at those ports, and every `--public-origin` you add, such as a load balancer's:

```bash
orchestrator-zero server start --public-origin https://agents.acme.se
```

A call that reaches the cluster through an address not on that list is a call to a third party: it carries no marker, and a loop through it is bounded by budgets only.

### Amazon Bedrock AgentCore and Microsoft Foundry

Both host agents as A2A servers, so they are configuration on the same harness, as the verified plugins `a2a-agentcore` and `a2a-foundry` (install `a2a-remote` too):

- `a2a-agentcore`: an agent on [AgentCore Runtime's A2A hosting](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-a2a.html), with a token from the identity provider its inbound JWT authorization trusts (client credentials) and AgentCore's session header. Settings `A2A_AGENTCORE_REGION`, `A2A_AGENTCORE_RUNTIME_ARN` (URL-escaped), `A2A_AGENTCORE_OAUTH_ENDPOINT`, `A2A_AGENTCORE_CLIENT_ID`; secret `A2A_AGENTCORE_CLIENT_SECRET`. SigV4 is not supported yet.
- `a2a-foundry`: an agent with [incoming A2A](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/enable-agent-to-agent-endpoint) enabled, with a Microsoft Entra token for `https://ai.azure.com/.default` from a service principal that has the Foundry Agent Consumer role on it. Settings `A2A_FOUNDRY_ACCOUNT`, `A2A_FOUNDRY_PROJECT`, `A2A_FOUNDRY_AGENT`, `A2A_FOUNDRY_DIRECTORY`, `A2A_FOUNDRY_CLIENT_ID`; secret `A2A_FOUNDRY_CLIENT_SECRET`. Foundry answers text only.

Neither plugin names a wildcard host: allow the hosts your settings point at on the tenant's egress allowlist (`oz0 tenant egress <tenant> <hosts...>`, whose hosts replace the list): for AgentCore `bedrock-agentcore.<region>.amazonaws.com` and your token endpoint's host, for Foundry `<account>.services.ai.azure.com` (Microsoft Entra's `login.microsoftonline.com` is in the plugin).

Both are tested against a stand-in shaped from that documentation, not against the services themselves (see [integration maturity](https://docs.oz0.ai/reference/compatibility#integrations-awaiting-live-validation)). Copy their agent files into a plugin of your own to have more than one such agent.

### What leaves your network

To the card's origin: the job's input, the answers people give the agent's questions, an approval's yes, and the plugin's one token; to the token endpoint, the plugin's client ID and secret. Nothing else of the tenant. The [recipient map](https://docs.oz0.ai/govern/inventory) lists each outside agent (kind `outside_agent`) with its host, its auth kind and its token endpoint. An outside agent is an [outbound connection](https://docs.oz0.ai/concepts/outbound-connections) you configure; nothing reaches one until a plugin that defines it is installed and a job runs it.

<span id="what-happens-in-a-job-1">

Section on this page

</span>

<span id="what-leaves-your-network-1">

Section on this page

</span>


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
