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
6 changes: 6 additions & 0 deletions .changeset/neat-sites-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@agentcommercekit/ack-id": minor
"agentcommercekit": minor
---

Update ack-a2a handshake to include ownership VC exchange
31 changes: 25 additions & 6 deletions demos/identity-a2a/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
generateKeypair
} from "agentcommercekit"
import { createAgentCardServiceEndpoint } from "agentcommercekit/a2a"
import { issueCredential } from "./issuer"
import type {
AgentCard,
AgentExecutor,
Expand All @@ -35,16 +36,24 @@ import type {
DidUri,
JwtSigner,
Keypair,
KeypairAlgorithm
KeypairAlgorithm,
Verifiable,
W3CCredential
} from "agentcommercekit"

type AgentConfig = {
agentCard: AgentCard
algorithm: KeypairAlgorithm
controller: DidUri
}
export abstract class Agent implements AgentExecutor {
constructor(
public agentCard: AgentCard,
public keypair: Keypair,
public did: DidUri,
public jwtSigner: JwtSigner,
public didDocument: DidDocument
public didDocument: DidDocument,
public vc: Verifiable<W3CCredential>
) {}

static async create<T extends Agent>(
Expand All @@ -53,11 +62,13 @@ export abstract class Agent implements AgentExecutor {
keypair: Keypair,
did: DidUri,
jwtSigner: JwtSigner,
didDocument: DidDocument
didDocument: DidDocument,
vc: Verifiable<W3CCredential>
) => T,
agentCard: AgentCard,
algorithm: KeypairAlgorithm = "secp256k1"
config: AgentConfig
) {
const { agentCard, algorithm, controller } = config

const baseUrl = agentCard.url
const agentCardUrl = `${baseUrl}/.well-known/agent.json`
const keypair = await generateKeypair(algorithm)
Expand All @@ -78,7 +89,15 @@ export abstract class Agent implements AgentExecutor {
colors.dim(Buffer.from(keypair.publicKey).toString("hex"))
)

return new this(agentCard, keypair, did, jwtSigner, didDocument)
const vc = await issueCredential({
subject: did,
controller
})

console.log("Generated sample VC for ownership attestation")
console.log("VC:", colors.dim(JSON.stringify(vc, null, 2)))

return new this(agentCard, keypair, did, jwtSigner, didDocument, vc)
}

async onMessageSend(
Expand Down
32 changes: 20 additions & 12 deletions demos/identity-a2a/src/bank-client-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import { colors, createLogger, waitForEnter } from "@repo/cli-tools"
import { A2AClient, Role } from "a2a-js"
import { getDidResolver, resolveDid } from "agentcommercekit"
import {
getDidResolver,
resolveDid,
verifyParsedCredential
} from "agentcommercekit"
import {
createA2AHandshakeMessage,
createSignedA2AMessage,
Expand All @@ -12,6 +16,7 @@ import { messageSchema } from "agentcommercekit/a2a/schemas/valibot"
import { v4 as uuidV4 } from "uuid"
import * as v from "valibot"
import { Agent } from "./agent"
import { didResolverWithIssuer, issuerDid } from "./issuer"
import { fetchUrlFromAgentCardUrl } from "./utils/fetch-agent-card"
import { startAgentServer } from "./utils/server-utils"
import type { AgentCard, Message } from "a2a-js"
Expand Down Expand Up @@ -286,7 +291,8 @@ export class BankClientAgent extends Agent {
did: this.did,
jwtSigner: this.jwtSigner,
alg: this.keypair.algorithm,
expiresIn: 5 * 60
expiresIn: 5 * 60,
vc: this.vc
}
)

Expand All @@ -299,7 +305,7 @@ export class BankClientAgent extends Agent {
const authResponse = await client.sendTask(identityParams)

// Step 3: Verify bank teller response
const { nonce: bankNonce } = await verifyA2AHandshakeMessage(
const { nonce: bankNonce, vc: bankVc } = await verifyA2AHandshakeMessage(
authResponse,
{
// Validate that this is intended for our DID
Expand All @@ -309,6 +315,11 @@ export class BankClientAgent extends Agent {
}
)

await verifyParsedCredential(bankVc, {
resolver: didResolverWithIssuer,
trustedIssuers: [issuerDid]
})

// Check that bank teller included our nonce
if (bankNonce !== nonce) {
throw new Error("❌ Bank teller nonce mismatch")
Expand Down Expand Up @@ -403,13 +414,10 @@ const agentCard: AgentCard = {
skills: []
}

if (import.meta.url === `file://${process.argv[1]}`) {
BankClientAgent.create(agentCard, "Ed25519")
.then(async (agent) => {
await agent.requestBankingServices()
})
.catch((error: unknown) => {
logger.log("Error:", error as Error)
process.exit(1)
})
export async function getClientAgent() {
return BankClientAgent.create({
agentCard,
algorithm: "Ed25519",
controller: "did:web:builder.ack.com"
})
}
38 changes: 22 additions & 16 deletions demos/identity-a2a/src/bank-teller-agent.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { colors, createLogger, waitForEnter } from "@repo/cli-tools"
import { Role } from "a2a-js"
import { isDidUri } from "agentcommercekit"
import { isDidUri, verifyParsedCredential } from "agentcommercekit"
import {
createA2AHandshakeMessage,
verifyA2AHandshakeMessage,
verifyA2ASignedMessage
} from "agentcommercekit/a2a"
import { Agent } from "./agent"
import { didResolverWithIssuer, issuerDid } from "./issuer"
import { startAgentServer } from "./utils/server-utils"
import type {
AgentCard,
Expand Down Expand Up @@ -117,10 +118,18 @@ class BankTellerAgent extends Agent {
"Press Enter to verify customer's cryptographic proof..."
)

const { nonce: clientNonce, iss: clientDid } =
await verifyA2AHandshakeMessage(request.params.message, {
did: this.did
})
const {
nonce: clientNonce,
iss: clientDid,
vc: clientVc
} = await verifyA2AHandshakeMessage(request.params.message, {
did: this.did
})

await verifyParsedCredential(clientVc, {
resolver: didResolverWithIssuer,
trustedIssuers: [issuerDid]
})

console.log(
colors.yellow(
Expand All @@ -144,7 +153,8 @@ class BankTellerAgent extends Agent {
requestNonce: clientNonce,
did: this.did,
jwtSigner: this.jwtSigner,
alg: this.keypair.algorithm
alg: this.keypair.algorithm,
vc: this.vc
}
)

Expand Down Expand Up @@ -220,21 +230,17 @@ const agentCard: AgentCard = {
}
}

export async function startServer() {
export async function startTellerServer() {
// Create bank teller agent with did:web instead of did:key
const bankTellerAgent = await BankTellerAgent.create(agentCard, "secp256k1")
const bankTellerAgent = await BankTellerAgent.create({
agentCard,
algorithm: "secp256k1",
controller: "did:web:bank.com"
})

// Start the server using shared utility
return startAgentServer(bankTellerAgent, {
logger,
port: 3001
})
}

// Only start server if this file is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
startServer().catch((error: unknown) => {
logger.log("Error starting bank teller server:", error as Error)
process.exit(1)
})
}
48 changes: 48 additions & 0 deletions demos/identity-a2a/src/issuer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
createControllerCredential,
createDidDocumentFromKeypair,
createDidKeyUri,
createJwtSigner,
generateKeypair,
getDidResolver,
signCredential
} from "agentcommercekit"
import type { DidUri } from "agentcommercekit"

const issuerKeypair = await generateKeypair("Ed25519")

export const issuerDid = createDidKeyUri(issuerKeypair)
export const issuerDidDocument = createDidDocumentFromKeypair({
did: issuerDid,
keypair: issuerKeypair
})

const signer = createJwtSigner(issuerKeypair)

const resolver = getDidResolver()
resolver.addToCache(issuerDid, issuerDidDocument)

export const didResolverWithIssuer = resolver

export async function issueCredential({
subject,
controller
}: {
subject: DidUri
controller: DidUri
}) {
const credential = createControllerCredential({
subject,
controller,
issuer: issuerDid
})

const { verifiableCredential } = await signCredential(credential, {
did: issuerDid,
signer,
alg: "Ed25519",
resolver
})

return verifiableCredential
}
52 changes: 6 additions & 46 deletions demos/identity-a2a/src/run-demo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from "child_process"
import { colors, waitForEnter } from "@repo/cli-tools"
import type { ChildProcess } from "child_process"
import { getClientAgent } from "./bank-client-agent"
import { startTellerServer } from "./bank-teller-agent"

async function main() {
console.log("🚀 Starting A2A Bank Identity Verification Demo...\n")
Expand Down Expand Up @@ -32,9 +32,7 @@ async function main() {
)
)
console.log("")
const server: ChildProcess = spawn("tsx", ["./src/bank-teller-agent.ts"], {
stdio: "inherit"
})
const server = await startTellerServer()

await new Promise((resolve) => setTimeout(resolve, 1000))

Expand All @@ -48,50 +46,12 @@ async function main() {
console.log("")
await waitForEnter("Press Enter to start the customer agent...")
console.log("")
const client: ChildProcess = spawn("tsx", ["./src/bank-client-agent.ts"], {
stdio: "inherit"
})

client.on("close", (code: number | null) => {
console.log("")
console.log(
colors.yellow(
"🎉 DEMO COMPLETE: Secure Agent-to-Agent Communication Established!"
)
)
console.log(colors.yellow(" Key achievements:"))
console.log(colors.yellow(" ✓ No passwords or shared secrets were used"))
console.log(
colors.yellow(" ✓ Cross-algorithm compatibility (secp256k1 ↔ Ed25519)")
)
console.log(colors.yellow(" ✓ Cryptographic proof of identity ownership"))
console.log(
colors.yellow(
" ✓ Decentralized identity verification via DID documents"
)
)
console.log("\n✅ Banking demo completed!")
server.kill()
process.exit(code ?? 0)
})

client.on("error", (err: Error) => {
console.error("Client error:", err)
server.kill()
process.exit(1)
})
const bankClientAgent = await getClientAgent()

server.on("error", (err: Error) => {
console.error("Server error:", err)
process.exit(1)
})
await bankClientAgent.requestBankingServices()

// Handle cleanup
process.on("SIGINT", () => {
console.log("\n🛑 Shutting down...")
server.kill()
process.exit(0)
})
server.close()
}

// Start the demo
Expand Down
Loading