Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions docs/specs/a2a-agent-card-external-evidence.md
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 80 additions & 0 deletions packages/bridge-a2a/__tests__/a2a-interceptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
A2AInterceptor,
AgentCardRegistry,
buildResourceUri,
getExternalEvidenceReferences,
isExternalEvidenceFresh,
type A2AAgentCard,
type A2ASendTaskParams,
} from "../src/index.js";
Expand Down Expand Up @@ -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>): A2ASendTaskParams {
return {
id: "task-nav-001",
Expand Down Expand Up @@ -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);
});
});
31 changes: 30 additions & 1 deletion packages/bridge-a2a/src/agent-card-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
4 changes: 4 additions & 0 deletions packages/bridge-a2a/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type {
A2AAgentCard,
A2ASkill,
A2AAuthScheme,
A2AExternalEvidenceReference,
A2AExternalEvidenceType,
A2ATask,
A2ATaskStatus,
A2AMessage,
Expand Down Expand Up @@ -69,6 +71,8 @@ export {
export {
AgentCardRegistry,
fetchAgentCard,
getExternalEvidenceReferences,
isExternalEvidenceFresh,
} from "./agent-card-registry.js";

// APS ↔ SINT interoperability mapping
Expand Down
47 changes: 47 additions & 0 deletions packages/bridge-a2a/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down