Archived · v0.13.2 · Advanced
Authenticated Extended AgentCard
An agent can expose a richer AgentCard to authenticated callers — extra skills, a longer description, private documentation URLs — without leaking any of it on the public /.well-known/agent.json. This is the A2A v0.3 agent/getAuthenticatedExtendedCard method, and A2X implements it as an extension on v1.0 too (the v1.0 spec doesn't bind a JSON-RPC name, so we reuse the v0.3 one).
Use this when:
- A skill is free to advertise publicly but only bookable by paying users.
- Documentation or support URLs shouldn't be harvested by crawlers.
- You want to tailor the card per-principal (enterprise tenant, user plan, role).
Wiring it up
Register a provider callback that, given the resolved AuthResult, returns a partial agent state to merge on top of the base:
import {
A2XAgent,
ApiKeyAuthorization,
type AuthResult,
type A2XAgentState,
} from '@a2x/sdk';
const a2xAgent = new A2XAgent({ taskStore, executor })
.setDefaultUrl('https://agent.example.com/a2a')
.setName('acme-agent')
.setDescription('Public description of Acme Agent.')
// Auth is required — the handler returns -32600 InvalidRequest on
// unauthenticated calls to the extended-card method (this method has
// no task-shaped response, so it cannot use the `auth-required`
// TaskState path).
.addSecurityScheme('apiKey', new ApiKeyAuthorization({
in: 'header',
name: 'x-api-key',
keys: [process.env.API_KEY!],
}))
.addSecurityRequirement({ apiKey: [] })
.setAuthenticatedExtendedCardProvider(async (auth: AuthResult): Promise<Partial<A2XAgentState>> => {
return {
description: 'Full Acme Agent description with private notes.',
skills: [
{ id: 'public_chat', name: 'Chat', description: 'General Q&A.', tags: ['chat'] },
{ id: 'premium_analysis', name: 'Deep Analysis', description: 'Subscribers only.', tags: ['premium'] },
],
documentationUrl: 'https://internal.example.com/acme-agent/docs',
};
});That's it. Calling the builder also flips the advertising capability on the base AgentCard:
- v0.3:
supportsAuthenticatedExtendedCard: true - v1.0:
capabilities.extendedAgentCard: true
Conforming clients see the flag, then call agent/getAuthenticatedExtendedCard with valid auth to fetch the enriched card.
Merge semantics
The partial state returned by your provider is merged over the base state this way:
| Field | Merge strategy |
|---|---|
Top-level scalars (name, description, defaultUrl, version, …) |
Overlay replaces. |
skills, interfaces, securityRequirements, defaultInputModes, defaultOutputModes |
Overlay replaces the whole array when provided. |
capabilities |
Deep-merged — missing keys keep base values. |
securitySchemes (Map) |
Merged — overlay wins on key collision. |
If you only want to add skills rather than replace them, build the merged array yourself from the base state:
.setAuthenticatedExtendedCardProvider(async (auth) => {
const base = a2xAgent.getAgentCard(); // current public card
return {
skills: [...base.skills, { id: 'premium', ... }],
};
});Error semantics
| Situation | JSON-RPC error | Code |
|---|---|---|
| Provider never registered | AuthenticatedExtendedCardNotConfiguredError |
-32007 |
Call arrives without auth (no context, failed auth, missing required scope) |
InvalidRequestError |
-32600 |
| Resolved name / description missing after merge | Internal error (your provider stripped required fields) | -32603 |
The auth check runs at the DefaultRequestHandler level via the normal security-scheme flow — you don't need to re-implement it inside the provider. The provider only runs after AuthResult.authenticated === true.
When AuthResult has a principal
Most schemes populate AuthResult.principal with whatever identity your validator returned (user ID, tenant ID, JWT sub, etc.). That's the hook for per-principal enrichment:
.setAuthenticatedExtendedCardProvider(async (auth) => {
const userId = (auth.principal as { sub: string }).sub;
const plan = await lookupUserPlan(userId);
return {
description: `Acme Agent — ${plan} tier.`,
skills: plan === 'enterprise'
? [...publicSkills, ...enterpriseSkills]
: publicSkills,
};
});Keep the callback cheap. It runs on every extended-card request, and the SDK does not cache the per-principal result (unlike the base card, which is cached by version).