Skip to content

Commit 70b3fc9

Browse files
authored
Implement basic A2A demo for ACK-ID identity exchange (#11)
* Implement basic A2A demo for ACK-ID identity exchange * Remove empty setup step from identity-a2a * Export valibot, zod schemas for a2a
1 parent d49907e commit 70b3fc9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2586
-93
lines changed

.changeset/red-cloths-grow.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"agentcommercekit": minor
3+
"@agentcommercekit/ack-pay": minor
4+
"@agentcommercekit/ack-id": minor
5+
"@agentcommercekit/jwt": minor
6+
---
7+
8+
Add A2A message support to ACK-ID packages

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ npm-debug.log*
4242
*.db
4343
*.sqlite
4444
*.sqlite3
45+
46+
# Claude
47+
.claude/*.local.*

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ As you can see from the demo, DIDs and Verifiable Credentials can be extremely p
6161

6262
By providing a standardized way to prove ownership chains, agents can securely verify that their counterparty is who they claim to be, such as an e-commerce store. By also exposing service endpoints, agents can broadcast how and where they want to be contacted, as well as other key endpoints they may offer, such as secure KYC exchange APIs.
6363

64+
#### A2A (Agent2Agent)
65+
66+
The ACK-ID protocol works seamlessly with Google's A2A (Agent2Agent) protocol. You can see a demo of two agents securely exchanging identity information in our `identity-a2a` demo.
67+
68+
To use this demo, run the following command:
69+
70+
```sh
71+
pnpm demo:identity-a2a
72+
```
73+
74+
You can see the code for this demo in [`./demos/identity-a2a`](./demos/identity-a2a).
75+
6476
#### Going Forward
6577

6678
The ACK-ID primitives provide the building blocks for a robust future for agentic Identity. We imagine protocol extensions to support Agent Discovery via registries, reputation scoring, secure end-to-end encrypted communication between agents, controlled authorization, and much more.

demos/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
In this directory you'll find several Demos walking you through different parts of the Agent Commerce Kit.
44

5-
We recommend running the demos from the root of the project with the command below, but each demo can also be run directly from its project directory with `pnpm start`.
5+
We recommend running the demos from the root of the project with the command below, but each demo can also be run directly from its project directory with `pnpm run demo`.
66

77
## Demos
88

demos/e2e/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pnpm run demo:e2e
1919
Alternatively, you can run the demo from this directory with:
2020

2121
```sh
22-
pnpm start
22+
pnpm run demo
2323
```
2424

2525
## Overview of the demo

demos/e2e/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
"scripts": {
2020
"check:types": "tsc --noEmit",
2121
"clean": "git clean -fdX .turbo",
22+
"demo": "tsx ./src/index.ts",
2223
"lint": "eslint .",
2324
"lint:fix": "eslint . --fix",
24-
"start": "tsx ./src/index.ts",
2525
"test": "vitest"
2626
},
2727
"dependencies": {

demos/identity-a2a/README.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# ACK-ID: A2A Identity & Auth Demo
2+
3+
**ACK-ID** is a protocol built on W3C Standards designed to bring verifiable, secure, compliant identity, reputation, and service discovery to agents.
4+
5+
**A2A** (Agent2Agent) is a protocol developed by Google to standardize communication between multiple agents.
6+
7+
This interactive command-line demo showcases how two A2A-compatible agents can use ACK-ID to verify each other's identity and trust that they are communicating with the expected agent.
8+
9+
## Getting started
10+
11+
Before starting, please follow the [Getting Started](../../README.md#getting-started) guide at the root of this monorepo.
12+
13+
### Running the demo
14+
15+
You can use the demo by running the following command from the root of this repository:
16+
17+
```sh
18+
pnpm run demo:identity-a2a
19+
```
20+
21+
Alternatively, you can run the demo from this directory with:
22+
23+
```sh
24+
pnpm run demo
25+
```
26+
27+
## Overview
28+
29+
This demo showcases mutual authentication flow between a Bank Customer Agent and a Bank Teller Agent using ACK-ID DIDs and JWTs exchanged within A2A message bodies. The demo walks through the following authentication flow.
30+
31+
### 1. Initial Contact - Customer Agent Initiates
32+
33+
The Customer Agent sends an authentication request as an A2A message containing a signed JWT with a nonce:
34+
35+
```jsonc
36+
{
37+
"role": "user",
38+
"kind": "message",
39+
"messageId": "f1f54f9d-6db2-4d78-8b38-4e50d77c8b19",
40+
"parts": [
41+
{
42+
"type": "data",
43+
"data": {
44+
"jwt": "<signed-JWT-from-customer>"
45+
}
46+
}
47+
]
48+
}
49+
```
50+
51+
The JWT payload includes:
52+
53+
```jsonc
54+
{
55+
"iss": "did:web:customer.example.com", // Customer's DID
56+
"aud": "did:web:bank.example.com", // Bank's expected DID
57+
"nonce": "c-128bit-random", // Customer's random nonce
58+
"iat": 1718476800,
59+
"jti": "0e94d7ec-...", // Unique JWT ID
60+
"exp": 1718477100 // 5-minute expiry
61+
}
62+
```
63+
64+
### 2. Bank Teller Agent Response
65+
66+
The Bank Teller Agent verifies the customer's JWT signature and responds with its own signed JWT, including both the customer's nonce and a new server nonce:
67+
68+
```jsonc
69+
{
70+
"role": "agent",
71+
"kind": "message",
72+
"messageId": "f1f54f9d-6db2-4d78-8b38-4e50d77c8b19",
73+
"parts": [
74+
{
75+
"type": "data",
76+
"data": {
77+
"jwt": "<signed-JWT-from-bank>"
78+
}
79+
}
80+
]
81+
}
82+
```
83+
84+
The Bank's JWT payload:
85+
86+
```jsonc
87+
{
88+
"iss": "did:web:bank.example.com", // Bank's DID
89+
"aud": "did:web:customer.example.com", // Customer's DID
90+
"nonce": "c-128bit-random", // Echo customer's nonce
91+
"replyNonce": "b-128bit-random", // Bank's new nonce
92+
"jti": "1f85c8fa-...", // Unique JWT ID
93+
"iat": 1718476805,
94+
"exp": 1718477105 // Short expiry
95+
}
96+
```
97+
98+
### 3. Subsequent Communications
99+
100+
After successful mutual authentication, all subsequent messages include a signature in the metadata:
101+
102+
```jsonc
103+
{
104+
"role": "user",
105+
"kind": "message",
106+
"messageId": "89f2e11b-5b0a-4c3b-b49d-14628e5d30fb",
107+
"parts": [
108+
{
109+
"type": "text",
110+
"text": "Please check the balance for account #12345"
111+
}
112+
],
113+
"metadata": {
114+
"sig": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...Q" // JWT signature of the parts array
115+
}
116+
}
117+
```
118+
119+
The signature is a JWT with the payload of `{ "message": <the-message-object-without-metadata> }`, with `aud` and `iss` properly set for the counterparty and sender's DID, respectively.
120+
121+
### Security Benefits
122+
123+
This authentication flow provides several security advantages:
124+
125+
- **Mutual Authentication:** Both parties prove their identity through cryptographic signatures
126+
- **Replay Attack Prevention:** Nonces and JWT IDs ensure messages cannot be replayed
127+
- **Man-in-the-Middle (MITM) Protection:** The aud and iss fields are pinned in the JWTs, preventing tampering. An attacker cannot modify requests or responses without invalidating the signatures
128+
- **Short-lived Tokens:** 5-minute expiry limits the window for potential attacks
129+
- **Verifiable Identity:** DID-based authentication ensures cryptographic proof of identity
130+
131+
## Learn More
132+
133+
- [Agent Commerce Kit](https://www.agentcommercekit.com) Documentation
134+
- [ACK-ID](https://www.agentcommercekit.com/ack-id) Documentation
135+
- [A2A](https://github.com/google-a2a/A2A) Documentation
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// @ts-check
2+
3+
import { config } from "@repo/eslint-config/base"
4+
5+
export default config({
6+
root: import.meta.dirname
7+
})

demos/identity-a2a/package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@demos/identity-a2a",
3+
"version": "0.0.1",
4+
"private": true,
5+
"homepage": "https://github.com/agentcommercekit/ack#readme",
6+
"bugs": "https://github.com/agentcommercekit/ack/issues",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/agentcommercekit/ack.git",
10+
"directory": "demos/identity-a2a"
11+
},
12+
"license": "MIT",
13+
"author": {
14+
"name": "Catena Labs",
15+
"url": "https://catenalabs.com"
16+
},
17+
"type": "module",
18+
"scripts": {
19+
"check:types": "tsc --noEmit",
20+
"clean": "git clean -fdX .turbo",
21+
"demo": "tsx ./src/run-demo.ts",
22+
"lint": "eslint .",
23+
"lint:fix": "eslint . --fix",
24+
"test": "vitest"
25+
},
26+
"dependencies": {
27+
"@repo/cli-tools": "workspace:*",
28+
"a2a-js": "^0.2.0",
29+
"agentcommercekit": "workspace:*",
30+
"jose": "^6.0.11",
31+
"safe-stable-stringify": "^2.5.0",
32+
"uuid": "^11.1.0",
33+
"valibot": "^1.1.0"
34+
},
35+
"devDependencies": {
36+
"@repo/eslint-config": "workspace:*",
37+
"@repo/typescript-config": "workspace:*",
38+
"@types/express": "^5.0.3",
39+
"@types/node": "^22",
40+
"eslint": "^9.27.0",
41+
"tsx": "^4.19.4",
42+
"typescript": "^5",
43+
"vitest": "^3.1.4"
44+
}
45+
}

demos/identity-a2a/src/agent.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/* eslint-disable @typescript-eslint/require-await */
2+
/**
3+
* Agent base class with DID-first architecture
4+
*
5+
* Architecture:
6+
* - DID is the top-level identifier for each agent
7+
* - AgentCard is referenced as a service endpoint in the DID document
8+
* - Clients discover agents by resolving DID documents and finding AgentCard services
9+
* - Authentication uses DID-based JWT signing and verification
10+
*/
11+
12+
import { colors } from "@repo/cli-tools"
13+
import { Role } from "a2a-js"
14+
import {
15+
createDidDocumentFromKeypair,
16+
createDidWebUri,
17+
createJwtSigner,
18+
generateKeypair
19+
} from "agentcommercekit"
20+
import { createAgentCardServiceEndpoint } from "agentcommercekit/a2a"
21+
import type {
22+
AgentCard,
23+
AgentExecutor,
24+
CancelTaskRequest,
25+
CancelTaskResponse,
26+
Message,
27+
SendMessageRequest,
28+
SendMessageResponse,
29+
SendMessageStreamingRequest,
30+
SendMessageStreamingResponse,
31+
TaskResubscriptionRequest
32+
} from "a2a-js"
33+
import type {
34+
DidDocument,
35+
DidUri,
36+
JwtSigner,
37+
Keypair,
38+
KeypairAlgorithm
39+
} from "agentcommercekit"
40+
41+
export abstract class Agent implements AgentExecutor {
42+
constructor(
43+
public agentCard: AgentCard,
44+
public keypair: Keypair,
45+
public did: DidUri,
46+
public jwtSigner: JwtSigner,
47+
public didDocument: DidDocument
48+
) {}
49+
50+
static async create<T extends Agent>(
51+
this: new (
52+
agentCard: AgentCard,
53+
keypair: Keypair,
54+
did: DidUri,
55+
jwtSigner: JwtSigner,
56+
didDocument: DidDocument
57+
) => T,
58+
agentCard: AgentCard,
59+
algorithm: KeypairAlgorithm = "secp256k1"
60+
) {
61+
const baseUrl = agentCard.url
62+
const agentCardUrl = `${baseUrl}/.well-known/agent.json`
63+
const keypair = await generateKeypair(algorithm)
64+
const jwtSigner = createJwtSigner(keypair)
65+
const did = createDidWebUri(baseUrl)
66+
const didDocument = createDidDocumentFromKeypair({
67+
did,
68+
keypair,
69+
service: [createAgentCardServiceEndpoint(did, agentCardUrl)]
70+
})
71+
72+
console.log(
73+
`🌐 Generated ${algorithm} keypair with did:web for ${this.name}`
74+
)
75+
console.log(" DID:", colors.dim(did))
76+
console.log(
77+
" Public key:",
78+
colors.dim(Buffer.from(keypair.publicKey).toString("hex"))
79+
)
80+
81+
return new this(agentCard, keypair, did, jwtSigner, didDocument)
82+
}
83+
84+
async onMessageSend(
85+
request: SendMessageRequest
86+
): Promise<SendMessageResponse> {
87+
const response: Message = {
88+
role: Role.Agent,
89+
parts: [{ type: "text", text: "Message received and processed" }]
90+
}
91+
92+
return {
93+
jsonrpc: "2.0",
94+
id: request.id,
95+
result: response
96+
}
97+
}
98+
99+
async onCancel(request: CancelTaskRequest): Promise<CancelTaskResponse> {
100+
return {
101+
jsonrpc: "2.0",
102+
id: request.id,
103+
error: { code: -32601, message: "Operation not supported" }
104+
}
105+
}
106+
107+
onMessageStream(
108+
_request: SendMessageStreamingRequest
109+
): AsyncGenerator<SendMessageStreamingResponse, void, unknown> {
110+
throw new Error("Method not implemented.")
111+
}
112+
onResubscribe(
113+
_request: TaskResubscriptionRequest
114+
): AsyncGenerator<SendMessageStreamingResponse, void, unknown> {
115+
throw new Error("Method not implemented.")
116+
}
117+
}

0 commit comments

Comments
 (0)