UNPKG

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
// 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