@treeship/a2a
Drop-in Treeship attestation for A2A (Agent2Agent) servers and clients.
A2A solves how agents communicate. It does not solve what proof exists that they did what they claimed. The @treeship/a2a package fills that gap. Every A2A task receipt, completion, and handoff becomes a signed Treeship artifact, and every outbound A2A artifact carries a receipt URL the receiving agent can verify.
A2A makes agents interoperable. Treeship makes that interoperability trustworthy and auditable.
Install
npm install @treeship/a2aThe package is framework-agnostic, it does not import any specific A2A SDK. You wire its hooks into whichever A2A server you already run.
What gets attested
| Phase | Artifact created |
|---|---|
| Task arrives | Intent, who sent it, which skill, A2A task and message IDs |
| Task completes | Receipt chained to the intent, elapsed time, status, artifact digest |
| Outbound artifact | treeship_artifact_id and treeship_receipt_url injected into metadata |
| Handoff to another A2A agent | Signed handoff, from-agent, to-agent, context |
AgentCard at /.well-known/agent.json | A treeship.dev/extensions/attestation/v1 extension publishing your ship ID |
Publish a Treeship-attested AgentCard
import { buildAgentCard } from '@treeship/a2a';
app.get('/.well-known/agent.json', (_req, res) => {
res.json(
buildAgentCard(
{
name: 'OpenClaw Research Agent',
version: '1.2.0',
url: 'https://openclaw.example/a2a',
capabilities: { streaming: true, pushNotifications: true },
skills: [
{ id: 'web-research', name: 'Web Research' },
],
},
{
ship_id: process.env.TREESHIP_SHIP_ID!,
verification_key: 'ed25519:abc123...',
},
),
);
});The output AgentCard contains the canonical Treeship extension:
{
"extensions": [
{
"uri": "treeship.dev/extensions/attestation/v1",
"required": false,
"params": {
"ship_id": "shp_4a9f2c1d",
"receipt_endpoint": "https://treeship.dev/receipt",
"verification_key": "ed25519:abc123..."
}
}
]
}verification_key appears only when you supplied one; receipt_endpoint defaults to https://treeship.dev/receipt.
Wrap your task handler
import { TreeshipA2AMiddleware } from '@treeship/a2a';
const treeship = new TreeshipA2AMiddleware({
shipId: process.env.TREESHIP_SHIP_ID!,
});
app.post('/a2a/tasks', async (req, res) => {
const { taskId, skill, from, messageId } = req.body;
await treeship.onTaskReceived({ taskId, skill, fromAgent: from, messageId });
const start = Date.now();
let status: 'completed' | 'failed' = 'completed';
let artifact;
try {
artifact = await runMyAgent(req.body);
} catch (e) {
status = 'failed';
throw e;
} finally {
const result = await treeship.onTaskCompleted({
taskId,
elapsedMs: Date.now() - start,
status,
artifactDigest: artifact ? TreeshipA2AMiddleware.digestArtifact(artifact) : undefined,
});
if (artifact) artifact = treeship.decorateArtifact(artifact, result);
}
res.json(artifact);
});The artifact your peer receives now carries a verifiable trail:
{
"artifactId": "research-output-001",
"parts": [{ "kind": "text", "text": "Research findings..." }],
"metadata": {
"treeship_artifact_id": "art_7f8e9d0a1b2c3d4e",
"treeship_receipt_url": "https://treeship.dev/receipt/art_7f8e9d0a1b2c3d4e",
"treeship_session_id": "ssn_01HR9W2D4Q4M7A0C",
"treeship_ship_id": "shp_4a9f2c1d"
}
}treeship_receipt_url is <receiptBaseUrl>/<receipt-artifact-id> — the path segment is the receipt artifact ID (art_…, the same value as treeship_artifact_id), not the session ID. The base defaults to https://treeship.dev/receipt and is configurable via the middleware's receiptBaseUrl option.
Only digests and metadata enter the artifact. Raw task content is never stored. The digest proves which data was involved without exposing the data itself.
Verify a peer before delegating
import { fetchAgentCard, hasTreeshipExtension, verifyArtifact } from '@treeship/a2a';
const card = await fetchAgentCard('https://partner-agent.example');
if (!hasTreeshipExtension(card)) {
throw new Error('Refusing to delegate: peer is not Treeship-attested');
}
// ... send your A2A task ... and when the artifact comes back:
const verification = await verifyArtifact(remoteArtifact.metadata);
if (!verification || !verification.withinDeclaredBounds) {
throw new Error('Peer artifact failed Treeship verification');
}verifyArtifact fetches the receipt from treeship_receipt_url and runs the structural WASM checks. Read the result precisely:
structurallyConsistent— the receipt's Merkle structure and inclusion proofs recompute (structural-pass). This does not establish who signed it.cryptographicallyVerified— true only if envelope signatures were actually verified; for a URL-fetched receipt this isfalse.withinDeclaredBounds—true/falseonly when the receipt is structurally consistent and carries a declaration;undefinedotherwise (never a default "true"). The!verification.withinDeclaredBoundsgate above therefore fails closed on undeclared or inconsistent receipts.
Record a handoff
await treeship.onHandoff({
toAgent: 'agent://openclaw',
taskId: 'a2a-task-7f8e9d',
context: 'Research phase delegated: find comparable Merkle MMR implementations',
messageId: 'msg_abc123',
});This is the same artifact treeship attest handoff produces from the CLI, it appears in the parent session's receipt as a delegation boundary.
Environment variables
| Variable | Effect |
|---|---|
TREESHIP_DISABLE=1 | Skips all attestation. Hooks return undefined. |
TREESHIP_SESSION_ID | Inherited from treeship session start; auto-included in payloads. |
TREESHIP_DEBUG=1 | Logs attestation failures to stderr. |
Design rules
- Treeship errors never fail the underlying A2A handler.
- Intent attestation is awaited so the proof exists before the agent runs.
- The middleware has zero runtime dependencies and is framework-agnostic.
- Handoffs and AgentCard extensions are opt-in but on by default.