aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
145 lines • 4.95 kB
JavaScript
// AgentCard fetch + signature verification with a TTL cache.
// Closes the runtime side of #1253; the JCS / JWS / verify primitives are in
// `src/a2a/{jcs,jws}.ts`.
//
// Per A2A §8, agents publish their card at
// /agents/{instanceId}/.well-known/agent-card.json
// Sandbox v2 also exposes the authenticated extended card at
// /agents/{instanceId}/v1/extendedAgentCard
// with a legacy fallback at
// /agents/{instanceId}/v1/card.
// The card declares required + optional extensions, supported transports,
// and skills.
import { loadJwkSet, verifyAgentCardSignature } from './jws.js';
const DEFAULT_TTL_MS = 5 * 60 * 1000;
export class AgentCardCache {
ttlMs;
entries = new Map();
constructor(ttlMs = DEFAULT_TTL_MS) {
this.ttlMs = ttlMs;
}
get(key) {
const entry = this.entries.get(key);
if (!entry)
return undefined;
if (Date.now() >= entry.expiresAt) {
this.entries.delete(key);
return undefined;
}
return entry.verified;
}
set(key, verified) {
this.entries.set(key, { verified, expiresAt: Date.now() + this.ttlMs });
}
/** Invalidate a single instance (call this on `instance state change` events). */
invalidate(key) {
this.entries.delete(key);
}
clear() {
this.entries.clear();
}
size() {
return this.entries.size;
}
}
/**
* Fetch + verify an AgentCard. Bypasses any cache the caller may have.
* Throws on fetch failure or signature mismatch.
*/
export async function fetchAgentCard(host, instanceId, opts = {}) {
const fetchImpl = opts.fetch ?? fetch;
const base = host.replace(/\/+$/, '');
const encoded = encodeURIComponent(instanceId);
const urls = [
`${base}/agents/${encoded}/.well-known/agent-card.json`,
`${base}/agents/${encoded}/v1/extendedAgentCard`,
`${base}/agents/${encoded}/v1/card`,
];
const headers = { accept: 'application/json' };
if (opts.bearer)
headers.authorization = `Bearer ${opts.bearer}`;
const init = { method: 'GET', headers };
if (opts.signal)
init.signal = opts.signal;
let url = urls[0];
let resp;
for (const candidate of urls) {
const candidateResp = await fetchImpl(candidate, init);
if (candidateResp.status === 404 && candidate !== urls[urls.length - 1]) {
continue;
}
url = candidate;
resp = candidateResp;
break;
}
if (!resp || resp.status !== 200) {
throw new Error(`fetchAgentCard: ${url} returned ${resp?.status ?? 'no response'}`);
}
const raw = await resp.text();
let card;
try {
card = JSON.parse(raw);
}
catch (err) {
throw new Error(`fetchAgentCard: invalid JSON from ${url}: ${err.message}`);
}
if (opts.skipVerify) {
return { card, raw, verifiedAt: new Date().toISOString() };
}
let jwks = opts.jwks;
if (!jwks) {
if (!opts.jwksSource) {
throw new Error('fetchAgentCard: provide `jwks` or `jwksSource` for verification');
}
jwks = await loadJwks(opts.jwksSource, fetchImpl, opts.signal);
}
// Pull the kid before verifying (verify throws on mismatch).
const kid = card.signatures?.[0]?.header?.['kid'] ?? undefined;
verifyAgentCardSignature(raw, jwks);
const verified = {
card,
raw,
verifiedAt: new Date().toISOString(),
};
if (kid !== undefined)
verified.kid = kid;
return verified;
}
/**
* Cache-aware variant: returns the cached entry when fresh, otherwise
* fetches and stores. Cache key is `${host}|${instanceId}`.
*/
export async function fetchAgentCardCached(cache, host, instanceId, opts = {}) {
const key = `${host}|${instanceId}`;
const cached = cache.get(key);
if (cached)
return cached;
const verified = await fetchAgentCard(host, instanceId, opts);
cache.set(key, verified);
return verified;
}
async function loadJwks(source, fetchImpl, signal) {
if (/^https?:\/\//i.test(source)) {
const init = { method: 'GET' };
if (signal)
init.signal = signal;
const resp = await fetchImpl(source, init);
if (resp.status !== 200)
throw new Error(`loadJwks: ${source} returned ${resp.status}`);
return loadJwkSet(await resp.text());
}
const { readFile } = await import('node:fs/promises');
const text = await readFile(source, 'utf8');
return loadJwkSet(text);
}
/**
* Extract the required-set extension URIs from an AgentCard.
* Used by A2AClient to know which `A2A-Extensions: <URI>` values to inject
* on every mutating call (#1254).
*/
export function requiredExtensionUris(card) {
return (card.capabilities?.extensions ?? [])
.filter((e) => e.required === true)
.map((e) => e.uri);
}
//# sourceMappingURL=agent-card.js.map