Trustless governance for Agent2Agent
A2A already gives you transport auth and a task-lifecycle state machine — what it explicitly leaves as "application-level responsibility" is spend limits, a policy engine, an audit trail, and any notion of accountability surviving a task handoff. agentsafe-a2a-guard closes that: the RECEIVING agent independently re-verifies a caller's signed request against the caller's own signed policy bundle, before the delegated task ever runs.
How it works
The calling agent signs a MAGP authorize request and carries it in the outgoing Message's metadata, declared via A2A's own extension mechanism — not a workaround bolted onto task content. Your receiving agent verifies the signature against the key in the caller's DID, re-evaluates the request with the same deterministic policy-core the gate runs, and only then invokes the skill's real handler. A caller that skips its own guard can't get your agent to act anyway.
One deliberate difference from MCP
No mutual DID handshake. A2A already has transport-level auth and a discovery-time AgentCard exchange, so a second identity proof on top would duplicate work the base protocol already does. Identity rides entirely in the per-message signed envelope instead.
What the A2A guard gives you
- guardA2ATask() — wrap one skill's handler, pinned to that skill's own id
- verifyRequest() — trustless re-evaluation, same as the MCP guard
- A decision becomes a TaskState, never a thrown error — escalate → INPUT_REQUIRED, a signature failure → AUTH_REQUIRED, a policy refusal → REJECTED
- An ESCALATE hold carries its escalationId in the returned task's own message, so your existing escalation-status polling keeps working unmodified
See it in code
import { createA2aGuard } from "@metamynd/agentsafe-a2a-guard";
const guard = createA2aGuard({ issuerApi: "https://metamynd.ai/api/v1" });
// Wrap the handler for one skill — permitted only after trustless re-verification.
const raisePurchaseOrder = guard.guardA2ATask("raise-purchase-order", async (message, task) => {
return { po: "PO-10231" };
});
// A refusal is a returned TaskStatus, not a thrown error — A2A tasks have a
// formal state machine, so this fits it rather than fighting it.