diff --git a/docs/index.md b/docs/index.md index edcf178b..08a6d074 100644 --- a/docs/index.md +++ b/docs/index.md @@ -69,6 +69,7 @@ limits, and tamper-evident audit evidence before execution. - MITRE ATLAS candidate mappings: [Security/MITRE ATLAS](./security/mitre-atlas-agent-technique-mappings.md) - Agent commerce governance profile: [Spec](./specs/agent-commerce-governance-profile-v1.md) - Regulated agent runtime profile: [Spec](./specs/regulated-agent-runtime-profile-v1.md) +- A2A Agent Card external evidence: [Spec](./specs/a2a-agent-card-external-evidence.md) - Persona AI shipyard safety brief: [Community Brief](./community/persona-ai-shipyard-safety-brief.md) - NIST submission bundle report: [Report](./reports/nist-submission-bundle.md) - Latest security bulletin: [July 2026](./security-bulletins/2026-07.md) diff --git a/docs/specs/a2a-agent-card-external-evidence.md b/docs/specs/a2a-agent-card-external-evidence.md new file mode 100644 index 00000000..d01991dd --- /dev/null +++ b/docs/specs/a2a-agent-card-external-evidence.md @@ -0,0 +1,35 @@ +# A2A Agent Card External Evidence + +SINT treats an A2A Agent Card as a stable identity and capability descriptor. +The card can prove that a subject published a specific card, but it should not +also become the mutable place for live authority, behavioral trust, or +counterparty safety state. + +The A2A bridge therefore supports optional `externalEvidence` references on +`A2AAgentCard`. These records compose alongside card identity: + +- `authority-receipt` for action-time authorization, approval, or restraint + evidence. +- `tool-surface-scan` for freshness-bounded scan results over exposed skills, + tools, or endpoints. +- `signed-tool-definition` for canonical tool definition signatures that detect + drift or poisoning. +- `counterparty-safety` for verifier-issued safety findings. +- `verification-state` for offline-verifiable state or verdict records. + +Each reference carries a subject, issuer, canonical evidence digest, optional +URI, optional signature, and optional freshness window. SINT consumers can +filter these records by type, subject, and freshness, then decide under local +policy whether the evidence is admissible for the requested interaction. + +This preserves the failure boundaries: + +- If the Agent Card signature or identity proof fails, the card is not trusted. +- If authority evidence is stale or missing, the action should fail closed, but + the underlying Agent Card identity may still be valid. +- If tool-surface evidence is stale or mismatched, the runtime should refuse to + connect to that surface, but it should not rewrite the card identity. + +In SINT, this model connects the A2A bridge to existing evidence-producing +surfaces such as the MCP scanner, signed MCP tool-definition registry, policy +gateway decision receipts, and evidence ledger. diff --git a/packages/bridge-a2a/__tests__/a2a-interceptor.test.ts b/packages/bridge-a2a/__tests__/a2a-interceptor.test.ts index 45a969c3..979b6f44 100644 --- a/packages/bridge-a2a/__tests__/a2a-interceptor.test.ts +++ b/packages/bridge-a2a/__tests__/a2a-interceptor.test.ts @@ -17,6 +17,8 @@ import { A2AInterceptor, AgentCardRegistry, buildResourceUri, + getExternalEvidenceReferences, + isExternalEvidenceFresh, type A2AAgentCard, type A2ASendTaskParams, } from "../src/index.js"; @@ -45,6 +47,37 @@ const FLEET_MANAGER_CARD: A2AAgentCard = { streaming: true, }; +const CARD_WITH_EXTERNAL_EVIDENCE: A2AAgentCard = { + ...FLEET_MANAGER_CARD, + externalEvidence: [ + { + type: "tool-surface-scan", + subject: "skill:navigate", + issuer: "sint:mcp-scanner", + uri: "sint://evidence/tool-surface/navigate", + hash: { + alg: "sha256", + digest: "a".repeat(64), + }, + issuedAt: "2026-07-24T00:00:00.000Z", + freshUntil: "2026-07-25T00:00:00.000Z", + scope: "connect-time", + }, + { + type: "authority-receipt", + subject: FLEET_MANAGER_CARD.url, + issuer: "sint:policy-gateway", + hash: { + alg: "sha256", + digest: "b".repeat(64), + }, + issuedAt: "2026-07-20T00:00:00.000Z", + freshUntil: "2026-07-21T00:00:00.000Z", + scope: "pre-action", + }, + ], +}; + function makeNavTask(overrides?: Partial): A2ASendTaskParams { return { id: "task-nav-001", @@ -305,4 +338,51 @@ describe("AgentCardRegistry", () => { reg.register(FLEET_MANAGER_CARD); expect(reg.size).toBe(1); }); + + it("preserves external evidence references without treating them as identity", () => { + const reg = new AgentCardRegistry(); + reg.register(CARD_WITH_EXTERNAL_EVIDENCE); + + const card = reg.get(FLEET_MANAGER_CARD.url); + expect(card?.url).toBe(FLEET_MANAGER_CARD.url); + expect(card?.externalEvidence).toHaveLength(2); + }); + + it("filters fresh external evidence by type and subject", () => { + const evidence = getExternalEvidenceReferences(CARD_WITH_EXTERNAL_EVIDENCE, { + type: "tool-surface-scan", + subject: "skill:navigate", + now: new Date("2026-07-24T12:00:00.000Z"), + }); + + expect(evidence).toHaveLength(1); + expect(evidence[0]?.issuer).toBe("sint:mcp-scanner"); + }); + + it("excludes stale external evidence unless explicitly requested", () => { + const now = new Date("2026-07-24T12:00:00.000Z"); + + expect(getExternalEvidenceReferences(CARD_WITH_EXTERNAL_EVIDENCE, { + type: "authority-receipt", + now, + })).toHaveLength(0); + + expect(getExternalEvidenceReferences(CARD_WITH_EXTERNAL_EVIDENCE, { + type: "authority-receipt", + now, + includeExpired: true, + })).toHaveLength(1); + }); + + it("treats missing freshness as admissible to local policy", () => { + expect(isExternalEvidenceFresh({ + type: "verification-state", + subject: FLEET_MANAGER_CARD.url, + issuer: "verifier:example", + hash: { + alg: "sha256", + digest: "c".repeat(64), + }, + })).toBe(true); + }); }); diff --git a/packages/bridge-a2a/src/agent-card-registry.ts b/packages/bridge-a2a/src/agent-card-registry.ts index b83b9be9..42697538 100644 --- a/packages/bridge-a2a/src/agent-card-registry.ts +++ b/packages/bridge-a2a/src/agent-card-registry.ts @@ -13,7 +13,7 @@ * @module @sint/bridge-a2a/agent-card-registry */ -import type { A2AAgentCard } from "./types.js"; +import type { A2AAgentCard, A2AExternalEvidenceReference } from "./types.js"; /** * In-memory registry of A2A Agent Cards. @@ -108,3 +108,32 @@ export async function fetchAgentCard( clearTimeout(timer); } } + +/** Filter Agent Card evidence references without treating them as card identity. */ +export function getExternalEvidenceReferences( + card: A2AAgentCard, + options?: { + readonly type?: A2AExternalEvidenceReference["type"]; + readonly subject?: string; + readonly now?: Date; + readonly includeExpired?: boolean; + }, +): readonly A2AExternalEvidenceReference[] { + const now = options?.now ?? new Date(); + return (card.externalEvidence ?? []).filter((evidence) => { + if (options?.type && evidence.type !== options.type) return false; + if (options?.subject && evidence.subject !== options.subject) return false; + if (!options?.includeExpired && !isExternalEvidenceFresh(evidence, now)) return false; + return true; + }); +} + +/** Freshness check for evidence that rides alongside an Agent Card. */ +export function isExternalEvidenceFresh( + evidence: A2AExternalEvidenceReference, + now = new Date(), +): boolean { + if (!evidence.freshUntil) return true; + const deadline = Date.parse(evidence.freshUntil); + return Number.isFinite(deadline) && deadline >= now.getTime(); +} diff --git a/packages/bridge-a2a/src/index.ts b/packages/bridge-a2a/src/index.ts index 11107fde..2f1a8f0d 100644 --- a/packages/bridge-a2a/src/index.ts +++ b/packages/bridge-a2a/src/index.ts @@ -28,6 +28,8 @@ export type { A2AAgentCard, A2ASkill, A2AAuthScheme, + A2AExternalEvidenceReference, + A2AExternalEvidenceType, A2ATask, A2ATaskStatus, A2AMessage, @@ -69,6 +71,8 @@ export { export { AgentCardRegistry, fetchAgentCard, + getExternalEvidenceReferences, + isExternalEvidenceFresh, } from "./agent-card-registry.js"; // APS ↔ SINT interoperability mapping diff --git a/packages/bridge-a2a/src/types.ts b/packages/bridge-a2a/src/types.ts index 7916cb63..6dbdc951 100644 --- a/packages/bridge-a2a/src/types.ts +++ b/packages/bridge-a2a/src/types.ts @@ -38,6 +38,53 @@ export interface A2AAgentCard { readonly streaming?: boolean; /** Whether this agent supports push notifications. */ readonly pushNotifications?: boolean; + /** + * Optional external evidence references keyed to this card, its authority, + * or its exposed tool/skill surface. + * + * These records are not part of Agent Card identity. A consuming runtime + * decides whether each freshness-bounded evidence item is admissible for a + * specific interaction. + */ + readonly externalEvidence?: readonly A2AExternalEvidenceReference[]; +} + +/** What an external evidence reference claims about an Agent Card or surface. */ +export type A2AExternalEvidenceType = + | "authority-receipt" + | "tool-surface-scan" + | "signed-tool-definition" + | "counterparty-safety" + | "verification-state" + | (string & {}); + +/** Content-addressed evidence that composes alongside, not inside, Agent Card identity. */ +export interface A2AExternalEvidenceReference { + /** Evidence type understood by the consuming runtime. */ + readonly type: A2AExternalEvidenceType; + /** Subject this evidence covers, such as an agent URL, skill id, or tool digest. */ + readonly subject: string; + /** Evidence issuer, for example a scanner, gateway, verifier DID, or JWKS subject. */ + readonly issuer: string; + /** Optional dereferenceable evidence location. */ + readonly uri?: string; + /** Digest over canonical evidence bytes. */ + readonly hash: { + readonly alg: "sha256" | (string & {}); + readonly digest: string; + }; + /** Optional detached or envelope signature over the evidence. */ + readonly signature?: { + readonly alg: "Ed25519" | "ES256" | (string & {}); + readonly kid?: string; + readonly value: string; + }; + /** When this evidence was issued. */ + readonly issuedAt?: ISO8601; + /** Last time the evidence should be accepted without refresh. */ + readonly freshUntil?: ISO8601; + /** Optional local policy scope, such as connect-time, pre-action, or audit-only. */ + readonly scope?: string; } /** A skill offered by an A2A agent. */