Skip to main content

a2a-protocol

How agent-native apps discover, authenticate with, and call each other over A2A. Use when connecting one app to another, debugging a remote agent that won't authenticate, exposing skills to peers, or calling agents from scripts.

Informations de source

Dépôt
BuilderIO/agent-native
Dernière activité de la source
10 octobre 2026 à 15:53
Langue détectée de SKILL.md
anglais
Étoiles
7 119
Forks
646

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
a2a-protocol
description
How agent-native apps discover, authenticate with, and call each other over A2A. Use when connecting one app to another, debugging a remote agent that won't authenticate, exposing skills to peers, or calling agents from scripts.
scope
dev
metadata
{"internal":true}
# A2A Protocol (Agent-to-Agent) ## Rule Agents call other agents over A2A, a JSON-RPC protocol for discovery and delegation. Use it when work belongs to a different agent entirely — not the local agent chat. **No workarounds when A2A feels flaky.** The strong default is `ask_app` (or `call-agent`) working reliably, full stop — not apps reaching around it. Do not have app A generate and execute raw SQL against app B's database, and do not expose B's internal tools directly to A as a substitute for delegation. The receiving agent has context, skills, and guardrails the caller doesn't; bypassing it to work around a flaky A2A call reintroduces exactly the bugs A2A exists to prevent, and makes the real reliability problem invisible instead of fixing it. If A2A delegation is unreliable, fix A2A — file it as a bug in the delegation path (timeout handling, retries, typed terminal states), don't route around it app by app. Connecting app A to an Agent-Native app B is two independent things, and both must be true: 1. **B is registered on A** as a `remote-agents/<id>.json` resource. 2. **A and B share a secret**, so A's signed JWT verifies on B. Neither is a code change, and neither is symmetric. Registering B on A does not let B call A. A hosted peer uses provider-specific endpoint and credential metadata in place of the shared Agent-Native secret, but it still does not make the provider's model or SDK an A2A endpoint. ## A2A is already mounted `createAgentChatPlugin` calls `mountA2A` for every app, so a generated app already serves: - `GET /.well-known/agent-card.json` — public discovery, with a larger authenticated capability view for verified sibling callers - `POST /_agent-native/a2a` — JSON-RPC, authenticated Do not add a `mountA2A` server plugin to enable A2A; it is on. Hand-mounting is only for a bespoke card or a custom handler (see **Custom mount** below). The client falls back to `POST /a2a` for external peers that only expose that path. New agent-native apps should call `/_agent-native/a2a`. ## Registering a remote agent A remote agent is **a row in the resources table, not a file on disk**. Path `remote-agents/<id>.json`, owner `SHARED_OWNER`, content: ```json { "id": "analytics", "name": "Analytics", "description": "Queries analytics data across providers", "url": "https://analytics.example.com", "color": "#6B7280", "cardUrl": "https://analytics.example.com/.well-known/agent-card.json", "auth": { "type": "bearer", "credentialRef": "ANALYTICS_A2A_TOKEN" } } ``` `url` is the only required endpoint field. `cardUrl` is the optional discovery URL for providers that do not serve `/.well-known/agent-card.json`. The client reads the protocol version from the agent card. Pass `protocolVersion` directly to `A2AClient` when a provider's card omits it. The optional `auth` descriptor is non-secret connection metadata. Use `{ "type": "bearer", "credentialRef": "..." }` for a vault-backed bearer credential, or `{ "type": "oauth-client-credentials", "tokenUrl": "...", "clientId": "...", "clientSecretRef": "...", "scope": "..." }` when the peer issues OAuth client-credentials tokens. `credentialRef` and `clientSecretRef` are references, never secret values. Do not add `apiKey`, `env`, `skills`, or `token` values to a manifest. Resolve credentials server-side from the workspace connection or vault. A direct `A2AClient` call can pass `cardUrl` and `protocolVersion` while a provider adapter is being used, but browser code must never receive the credential. Four ways to create it, all writing the same row: | Surface | Where | | --- | --- | | Settings → Agent → Sub-agents → Connect agent (owners and admins) | any app with the Settings shell | | Agent page → Connections | apps mounting `AgentTabsPage` | | Dispatch → Agents → Add external agent | workspace dispatch | | The agent itself | `resources` tool, `action: "write"`, `--scope shared` | The last one matters for template apps with no Code tab: "connect me to https://analytics.example.com" is a complete instruction. `remote-agents/` is whitelisted as a durable control path, so the write is workspace-scoped, and it takes effect on the next `discoverAgents()` call with no restart. First-party templates are seeded automatically and overlaid on top of manifests, so `Mail`, `Calendar`, and friends appear without registration. In dev they resolve to `http://localhost:<devPort>`; a manifest pointing at localhost is ignored in production. `call-agent` also accepts a raw URL, which skips registration entirely — useful for one-off calls, useless for @-mentions. ### Checking a peer `GET /_agent-native/agents/probe?url=<base>` reads the peer's card and makes one authenticated no-op call, returning `reachable` and `authorized` as independent fields. Omit `url` to probe every registered peer. Reachable-but-unauthorized is the failure worth looking for: local dev runs the receiver unauthenticated, so a mismatched secret only surfaces after deploy. The Connected Agents settings section calls this on add and on open. ## Authentication A2A authenticates with a **short-lived JWT the caller signs**, not a stored bearer key. `Authorization: Bearer <jwt>`; HS256; 15-minute default TTL. There is no per-peer API key anywhere in the system, which is why the manifest has no field for one. A user identity assertion carries the authenticated caller's email and is signed only with the deployment `A2A_SECRET`. Mint it from authenticated request context; never accept an email from message params or metadata as identity. `verifyA2AToken` rejects MCP connect and OAuth tokens even though they can be signed with the same secret: they are long-lived MCP credentials, never A2A tokens. Callers bind signed tokens to the receiving endpoint with `aud`; every receiver, including a custom JWT verifier, must check that claim against its own canonical endpoint URL. Preserve compatibility for old tokens without `aud`, but reject an audience-bearing token when the receiver cannot derive or match its expected audience. A scoped resource route may require `aud` even for legacy callers; document and test that boundary when adding such a route. The framework supports two credential scopes: | Secret | Scope | How it is set | | --- | --- | --- | | `A2A_SECRET` | User identity across a deployment | env var, **never auto-generated** | | org `a2a_secret` | Organization principal only | auto-generated on org creation; Team page UI | An org-secret JWT proves only that the caller holds the organization's shared credential. Its `sub` or email claims are ignored; do not use it to read person-owned data, claim a user's task, invoke a user-scoped action, or carry a human approval. Organization-scoped operations can run with this principal. Use an `A2A_SECRET`-signed user identity for user-owned operations and approval continuations. MCP applies the same distinction. Organization IDs are local to each app's database. For calls to a peer app, bind the token to the trusted `org_domain` and the peer's canonical audience; do not put the caller app's local `org_id` in the token. The receiver resolves the domain to its own organization row and binds authorization to that local ID. An ID-only token is valid only when both sides intentionally share the same local organization ID, such as a credential scoped to one local database. The org secret is managed at **Team page → A2A secret** (owner only), where it can be revealed, copied, regenerated, and pushed to discovered peers. It is a fallback for organization-scoped calls, not a fallback user identity. `A2A_SECRET` is the deploy-level path. `workspace-deploy` refuses a production workspace deploy without it and prints the generator command. Peers that must trust user identity assertions need the *same* value on both sides. Keep it different from every org `a2a_secret`; a shared value cannot distinguish a user assertion from an organization credential. An org secret equal to `A2A_SECRET`, or a user token signed only with the org secret, is verified as an organization principal and reaches the caller as the typed `permanent_precondition` error ("no verified user"): an admin/ops fix, never a retry. Mismatched `A2A_SECRET` values are rejected with `401` (`a2a_auth_rejected`). With no secret configured at all: | Runtime | Result | | --- | --- | | local dev | open, one-time console warning | | production | `503` — "A2A authentication not configured" | | production, secret set, bad/absent token | `401` | "Production" is detected broadly (NODE_ENV, Netlify, Lambda, Vercel, Render, Fly, Cloud Run, …) and unrecognized deployed hosts fail closed unless the request is genuine loopback or `A2A_ALLOW_UNSIGNED_INTERNAL=1`. `A2AConfig.apiKeyEnv` still exists for static bearer auth against non-agent-native peers, but the framework's own mount never sets it. Do not reach for it when debugging a connection between two agent-native apps — the answer there is always the shared secret. For a provider that uses OAuth, resolve and refresh the access token in a server-side adapter. `A2A_SECRET` is not a substitute for the provider's Entra, Google, or other OAuth credential. ## Hosted providers Foundry, Gemini Enterprise, and other hosted services can be A2A peers only when they expose a compatible protocol endpoint. Foundry hosted agents run agent code in managed containers and can expose A2A through Agent Service. Keep the Agent-Native UI, actions, and PostgreSQL app on its normal host. Foundry callers use Microsoft Entra bearer tokens with Foundry Agent Consumer access, so configure `cardUrl` and obtain the token through the workspace credential provider. Foundry v1.0 is the GA JSON-RPC endpoint; v0.3 is the preview endpoint used when no version is selected. Use the v1 card URL `.../agents/{agent}/endpoint/protocols/a2a/agentCard/v1.0` when available, and pass `protocolVersion` only when the card omits it. Foundry v1 does not provide SSE streaming. See the [Foundry hosted agent overview](https://learn.microsoft.com/en-us/azure/foundry/agents/overview) and [A2A endpoint guidance](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/enable-agent-to-agent-endpoint). Gemini Enterprise managed assistants expose a standard A2A JSON-RPC endpoint under the assistant resource, such as `.../assistants/default_assistant/agents/{id}/a2a`, and can publish the card at a custom URL. Use the generic bearer client with that `cardUrl`; the caller's Google OAuth bearer needs the `discoveryengine.assist` permission. Custom A2A agent registration is Pre-GA, so verify that it is enabled in the target project before exposing it in the workspace picker. A model provider or SDK without an A2A endpoint still needs a server-side adapter before an Agent-Native app can call it. Anthropic Managed Agents has no inbound A2A endpoint. Use `createAnthropicManagedAgentsHandler()` as the `A2AConfig.handler` when the Agent-Native app should own the A2A task, workflow state, and approvals while Anthropic owns the session, context, and tools. Its configuration is a native provider kind on the existing remote-agent resource: ```json { "kind": { "provider": "anthropic-managed-agents", "agentId": "agt_01...", "environmentId": "env_01...", "credentialRef": "ANTHROPIC_API_KEY" } } ``` `credentialRef` is a vault reference, resolved in the caller's user and organization scope. The adapter calls `POST /v1/sessions`, sends `user.message` or `user.tool_confirmation` events to `POST /v1/sessions/{id}/events`, and reads `GET /v1/sessions/{id}/events/stream` with the `managed-agents-2026-04-01` beta header. A `session.status_idle` event whose stop reason is `requires_action` becomes A2A `input-required`; each blocking tool also emits the normal `approval-request` runtime event. Continue only with structured `{ toolUseId, result: "allow" | "deny" }` confirmations in the adapter metadata. Register the `anthropic-managed-agents` workspace connection provider to reuse existing per-app grants. Never hardcode either secret in source, docs, prompts, app state, action descriptions, client bundles, or examples. Read them from runtime config; never log or return them. ## Advertising what this agent can do Card `skills` are derived from actions marked `publicAgent`; do not maintain a second capability registry. Anonymous callers see only explicitly public-safe reads. Verified sibling callers see two concise kinds of capability: - Authenticated read-only actions selected by connector policy include their input schemas and may be invoked directly. - Authenticated writes marked `publicAgent: { expose: true, readOnly: false, requiresAuth: true }` are advertised without schemas as message-only capabilities. A sibling delegates an objective; the receiving agent chooses and validates its own local actions. `agentTool: false` and `externalAgents.denyActions` remove an action from both authenticated paths. An app that marks no actions still publishes `"skills": []`, and natural-language delegation still works because the receiver loads its own instructions, skills, data dictionary, credentials, and tools. Expose stable user-facing capabilities, not internal implementation actions. ## Calling another agent Natural-language delegation is the default for agent-to-agent work. Tell the receiving specialist the objective, relevant IDs/date range, and desired result shape. The receiver owns source selection, schemas, queries, joins, SQL, and provider-specific details. Do not switch to a direct action merely because a delegated run is slow or flaky; fix the A2A path instead. ### Simple: `callAgent()` (text in, text out) ```ts import { callAgent, resolveA2ACallerAuth } from "@agent-native/core/a2a"; const auth = await resolveA2ACallerAuth(); const answer = await callAgent( "https://analytics.example.com", "What were last week's signups?", auth, ); // answer is a plain string ``` `callAgent` signs nothing on its own — with no `userEmail`/`orgSecret` in opts it calls **unauthenticated**, which silently works in local dev and 401s in production. `resolveA2ACallerAuth()` pulls the authenticated email, org domain, and org secret out of request context; pass them explicitly only from CLI or cron, where there is no request. ### Explicit machine-contract exception: `invokeAgentAction()` Use direct invocation only when a trusted integration already specifies the exact receiver-owned semantic read action and complete arguments. It is an optional machine API, not an agent-performance shortcut or fallback for failed message delegation: ```ts import { invokeAgentAction } from "@agent-native/core/a2a"; const { result } = await invokeAgentAction({ target: "analytics", action: "gong-calls", input: { company: "Acme", days: 90, includeTranscripts: true }, userEmail, orgDomain, orgSecret, }); ``` The receiver still owns schema validation, credentials, access scoping, audit attribution, and exposure policy. Direct invocation is available only for cataloged, authenticated, explicitly exposed read-only actions that do not require approval. Its JWT is audience-bound to the receiving app's exact base URL, including a workspace path such as `/content`. Use normal message delegation whenever the receiver must interpret the request, choose a source, consult its data dictionary, plan, synthesize, join data, or perform a multi-step workflow. Inside an agent loop, `call-agent` exposes the same path with `action` + `input`; omit `message` and `taskId` in that mode. ### Retry safety and trace linkage `call-agent` automatically derives an owner-scoped idempotency key from the originating turn, target, and exact message. If a retry reaches the receiver after the caller timed out, asynchronous `message/send` returns the existing active or completed task instead of starting a duplicate agent run. Failed and canceled tasks release the key for an intentional retry; synchronous calls are never deduplicated. Lower-level clients may pass an `idempotencyKey` explicitly; keep it stable for the same logical submission and change it when the work changes. Dedupe is scoped to the JWT-authenticated owner and verified org, and keys are limited to 128 characters. The caller also forwards bounded correlation metadata (`callerApp`, `selectedReceiverApp`, `callerThreadId`, `parentRunId`, `parentTurnId`, and direct-read `invocationId`). `selectedReceiverApp` lets the matching receiver prioritize its declared local capabilities before loading cross-app tools; the other fields remain telemetry hints. Receivers must continue to derive identity, data ownership, org scope, access, and approval from the verified request context. Delegated model loops emit `$ai_generation` with A2A/MCP lineage, while direct reads emit the content-free `$a2a_read_invoke` event; neither event includes action arguments or results. ### Advanced: `A2AClient` (full control) ```ts import { A2AClient, resolveA2ACallerAuth } from "@agent-native/core/a2a"; // The second argument is the bearer token itself, not a signing secret. const { apiKey, apiKeyFallbacks } = await resolveA2ACallerAuth(); const client = new A2AClient("https://analytics.example.com", apiKey, { fallbackApiKeys: apiKeyFallbacks, }); // Discover agent capabilities const card = await client.getAgentCard(); // Send a message and get a task back const task = await client.send({ role: "user", parts: [{ type: "text", text: "What were last week's signups?" }], }); // task.status.state === "completed" // task.status.message.parts[0].text === "Last week: 1,247 signups..." // Stream responses for await (const update of client.stream({ role: "user", parts: [{ type: "text", text: "Detailed breakdown by day" }], })) { console.log(update.status.state, update.status.message); } ``` ### Agent activity in delegated chat Agent-Native peers attach a bounded `data` part with `kind: "agent-native/agent-activity"` to in-progress and terminal task status messages. It contains the same user-visible reasoning summaries shown in the receiving app, tool names and completion states, elapsed time, and progressive response text. It never includes tool inputs, tool results, credentials, or hidden provider reasoning. `call-agent` reads this optional part while it polls an asynchronous task and renders the remote work as a nested agent run. Unknown A2A peers do not need to implement the extension: their ordinary status and final text still render in the same nested block. Treat activity data as untrusted presentation content; never use it for identity, authorization, approval, routing, or artifact validation. ### Carrying explicit chat authorization When the authenticated caller has an exact consequential action that the user explicitly authorized in the originating chat, pass the tool name and complete input as `approvedActions`. The receiver accepts these grants only from a JWT-verified user identity, converts each one to the same content-addressed key as its local approval gate, and consumes it once: ```ts await client.send(message, { async: true, approvedActions: [ { tool: "send-email", input: { to, subject, body, attachments }, }, ], }); ``` Never infer authorization from request prose or broaden the input. A changed recipient, body, attachment, or tool produces a different key and follows the receiver's normal approval-required path. Static API keys and unsigned callers cannot carry these grants. ## JSON-RPC Methods | Method | Purpose | Auth required |
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub