APOA authorization for A2A agent-to-agent communication. Scoped delegation tokens, capability attenuation, audit trails.
A2A handles authentication (who are you?). This package adds authorization (what can you do, on whose behalf, for how long?).
npm install @apoa/a2aimport { attachToken, apoaHeaders } from '@apoa/a2a';
import { APOA, generateKeyPair } from '@apoa/core';
const keys = await generateKeyPair();
const apoa = new APOA({ privateKey: keys.privateKey });
const token = await apoa.tokens.createGrant({
principal: 'did:apoa:jane',
agent: 'did:apoa:travel-planner',
service: 'flights',
scopes: ['book', 'search'],
expiresIn: '24h',
});
const message = {
messageId: 'msg-001',
role: 'user',
parts: [{ kind: 'text', text: 'Book me a flight to Helsinki' }],
};
// Attach token to message metadata (keyed by APOA extension URI)
attachToken(message, token.raw);
// Send with APOA extension header
await fetch('https://agent.example.com/message:send', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...apoaHeaders() },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'SendMessage', params: { message } }),
});import { createA2AGuard } from '@apoa/a2a';
const guard = createA2AGuard({
key: publicKey,
mappings: {
'book-flight': 'flights:book',
'search-flights': 'flights:search',
'cancel-flight': 'flights:cancel',
},
});
// In your A2A agent's message handler:
const result = await guard.authorize(incomingMessage, 'book-flight');
if (!result.authorized) {
// Transition task to AUTH_REQUIRED or reject
}import { apoaExtension, apoaSkillRequirement } from '@apoa/a2a';
const agentCard = {
name: 'Travel Agent',
version: '1.0.0',
capabilities: {
extensions: [apoaExtension()],
},
skills: [
{
id: 'book-flight',
name: 'Flight Booking',
description: 'Books flights on behalf of the user',
securityRequirements: [apoaSkillRequirement(['flights:book'])],
},
],
// ...
};- Client attaches an APOA token to the A2A message's
metadata, keyed by the APOA extension URI - Client sends the
A2A-Extensionsheader to activate the APOA extension - Server extracts the token from message metadata
- Server maps the target skill to an APOA
service:scopepair - Server verifies: signature, expiration, revocation, scope, constraints, rules
- If authorized, the skill executes. If not, the task transitions to
AUTH_REQUIREDor is rejected
Simple format:
createA2AGuard({
key: publicKey,
mappings: {
'book-flight': 'flights:book',
'search-flights': 'flights:search',
},
});Auto-mapping (no config):
createA2AGuard({ key: publicKey });
// book-flight -> book-flight:invokeWhen Agent A delegates a task to Agent B, it can include an attenuated APOA token:
import { delegate } from '@apoa/core';
import { attachToken } from '@apoa/a2a';
// Agent A delegates narrower permissions to Agent B
const childToken = await delegate(parentToken, {
agent: { id: 'agent-b' },
services: [{ service: 'flights', scopes: ['search'] }], // narrower than parent
}, signingOptions);
// Attach to the A2A message with the delegation chain
const message = { messageId: 'msg-002', role: 'user', parts: [{ kind: 'text', text: 'Search for flights' }] };
attachToken(message, childToken.raw, [parentToken.jti]);Agent B's server verifies the token and checks the delegation chain for revocation.
| Capability | A2A Native | @apoa/a2a |
|---|---|---|
| Transport auth (OAuth, API keys) | Yes | N/A (complementary) |
| Per-task scoped authorization | No ("implementation-specific") | Yes |
| Delegation chains with attenuation | No | Yes |
| Constraint checking | No | Yes |
| Hard/soft rules | No | Yes |
| Cascade revocation | No | Yes |
| Audit trail | No (recommended, not specified) | Yes |
- APOA Spec
- @apoa/core (TypeScript SDK)
- @apoa/mcp (MCP integration)
- apoa (Python SDK)
Apache-2.0