# A2A

Source: https://permdock.com/docs/adapters/a2a

permdock/a2a emits A2A Agent Cards whose skills carry security requirements derived from permissions and filters the authenticated extended card by the caller's grants.

`permdock/a2a` maps A2A skills to permission references. From that map it emits the public Agent Card with `securitySchemes` and per-skill `securityRequirements`, and it builds the authenticated extended card for a specific caller by keeping only skills the caller may invoke. It is the A2A counterpart of `list_tools` filtering in [permdock/mcp](/docs/adapters/mcp).

## Purpose [#purpose]

A2A 1.0 ([specification](https://a2a-protocol.org/latest/specification/)) describes agents through Agent Cards: a public card lists skills and the security schemes required to call them, and an authenticated extended card may expose different skills per caller. Cards can be signed. Without a permission layer, the skill list is static and every caller sees the same card. With `permdock/a2a`, each skill's security requirement is the permission's `scope`, and the extended card is a per-caller view computed from the same policy that guards the skill at execution time.

## API [#api]

```ts
import { createPermDock } from "permdock/a2a";

const { agentCard, extendedAgentCard, protectSkill } = createPermDock(policy, {
  subject: (auth) => userFrom(auth), // caller principal from the A2A request auth
  card: {
    name: "Posts agent",
    url: "https://agent.example.com/a2a",
    version: "1.0.0",
  },
  securitySchemes: {
    oauth: {
      type: "oauth2",
      oauth2MetadataUrl:
        "https://auth.example.com/.well-known/oauth-authorization-server",
    },
  },
  skills: {
    summarise: {
      permission: permissions.post.read,
      description: "Summarise a post",
    },
    publish: {
      permission: permissions.post.publish,
      data: (task) => loadPost(task.postId),
    },
  },
});

app.get("/.well-known/agent-card.json", (c) => c.json(agentCard()));
app.get("/a2a/extended-card", (c) => c.json(extendedAgentCard(c.get("auth"))));
app.post(
  "/a2a/tasks",
  protectSkill((task) => task.skillId),
  handleTask,
);
```

* `skills` maps skill ids to a permission and optional metadata; `data` resolves the resource for instance-level actions from the task payload.
* `agentCard()` returns the public card in the A2A 1.0 wire form, which validates against the published `a2a.json`: `card.url` becomes the first `supportedInterfaces` entry (`protocolBinding` defaults to `JSONRPC`, `protocolVersion` is `1.0`), `description` defaults to `name`, `defaultInputModes` and `defaultOutputModes` default to `['text/plain']`, and `capabilities.extendedAgentCard` is `true`. `securitySchemes` takes OpenAPI-style input (`type` `oauth2`, `http`, `openIdConnect`, `mutualTLS` or `apiKey`) and emits the A2A one-of form (`{ oauth2SecurityScheme: { oauth2MetadataUrl } }`). Each skill carries `tags` (default: the permission's resource) and `securityRequirements` of the form `{ schemes: { <scheme>: { list: [scope] } } }`, naming the configured scheme and the permission's `scope` alone (`post:read`, `post:publish`), with no coarse agent scope added.
* `extendedAgentCard(auth)` builds a request-scoped `PermDock` and returns only skills with a grant for the caller; `securityRequirements` are unchanged. A skill with instance-level conditions stays listed when any grant could match, as with MCP `tools/list`, and filtered skills carry no `alternatives` hints.
* `protectSkill(selector)` is middleware for the task endpoint: it resolves the skill id, runs `decide`, and rejects the task when the outcome is not `granted`.
* Card signing stays with the host's key material: `sign(card, signPayload)` passes the RFC 8785 canonical JSON of the card without `signatures` to your callback, which returns a compact JWS over it (for example jose `CompactSign`); the adapter checks the payload, detaches it and appends `{ protected, signature }` to `signatures` (A2A 1.0 section 8.4), so the filtered extended card can be signed per response without the adapter holding a key.

## Request lifecycle [#request-lifecycle]

1. Discovery: a client fetches the public card. No authentication; scopes per skill are visible so the client can request them during OAuth.
2. Authenticated discovery: the client fetches the extended card with a bearer token. The adapter resolves the subject, fills `actor` from the token's client identity and `delegation` from scopes or `authorization_details`, and filters skills with `can(permission)` (collection) or any-grant (instance).
3. Task submission: `protectSkill` maps the task to a skill and permission, runs the skill's `data` loader, validates what it returns against the resource schema, and calls `decide`. A loader that throws, or returns nothing for an instance permission, is a denial.
4. Outcome: `granted` runs the task; `denied` and `approval-required` answer with an A2A task failure carrying the Decision (below).
5. `on('decision')` records the discovery filter and the task decision with `actor` and `delegation`.

## What it validates [#what-it-validates]

* The row a `data` loader returns, against the resource schema (`validate: 'boundary'`); the task body is untrusted and is never the row itself.
* A skill whose permission is instance-level must declare `data`; `createPermDock` throws a `TypeError` at startup otherwise.
* Scope presence: the caller's token must carry the skill's `scope`; a missing scope is reported as a `401`/`403` with `WWW-Authenticate` naming the required scope, matching the security requirement in the card.
* Card consistency: in development, the adapter asserts that every skill declares exactly one permission and that the `securitySchemes` referenced by `securityRequirements` exist.
* Caller identity is never taken from the task body; it comes from the transport authentication only.

## How denials surface [#how-denials-surface]

* In discovery: the skill is absent from the extended card. The public card is never filtered, so a caller can always learn what scopes to request.
* At execution: an A2A task in the failed state whose error is an RFC 9457 Problem Details object with `permission`, `denials` and `alternatives`. `status` follows the [Problem Details status matrix](/docs/concepts/errors): `401` with a `Bearer` `wwwAuthenticate` challenge for a caller with no subject, `429` for a rate limit, `503` when the limit store is unreachable, `403` otherwise. A loader that throws, or returns nothing for an instance permission, is a `403` denial with reason `validation`; a subject resolver that throws makes the caller anonymous.
* `approval-required`: the task enters an input-required state with the `Decision.token` and a human-readable prompt in the message, and the pending request is written to the `store`. When the caller continues the task, the adapter recomputes the token (or reads the one the verified token carries in `auth.extra.approval`) and resumes only if the store holds an approved record for the same permission, resource, principal and client; the approval is consumed, so a second run fails with `approval-consumed`. A token issued to another client never matches.
* An unknown skill id, including `__proto__`, `constructor` and other inherited names, fails with `403`; skills are looked up by own key only.
* Missing scope: standard OAuth step-up (`insufficient_scope`), not a denial.

## Example app [#example-app]

`apps/examples/a2a-agent`: Hono on `127.0.0.1:3471` with `GET /health`, public `/.well-known/agent-card.json`, and `POST /a2a/tasks`. Summarise is granted for a member; publish is denied.

## Related standards [#related-standards]

* [A2A](/docs/standards/a2a): Agent Card, `securitySchemes`, `securityRequirements`, signed cards, authenticated extended card.
* [OAuth agent delegation](/docs/standards/oauth-agent-delegation): scopes and `authorization_details` as delegated authority.
* [OpenAPI 3.2](/docs/standards/openapi): the same `oauth2MetadataUrl` security scheme shape.
* [Problem Details](/docs/standards/problem-details): task error body.
