@kalkilabs/dhruva-agent-tools
v0.1.3
Published
Framework-agnostic tools for Dhruva Agent Identity (ERC-8004) — EIP-712 registration signing, EIP-191 identity proofs, and on-chain status lookup. Works with Mastra, LangChain, Vercel AI SDK, or plain function calling.
Readme
Framework agnostic tools for Dhruva Agent Identity (ERC-8004 compliant: EIP-8004 spec) focused on:
- EIP-712 registration signing (owner request verification + agent consent signature)
- EIP-191 identity proof generation (
personal_sign) and verification
Depends only on viem and zod. No agent framework is required. Optional Mastra adapter available via subpath import.
Installation
npm install @kalkilabs/dhruva-agent-toolsCore API Reference
Registration Signing
| Export | Description |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DhruvaSigningPolicy | Class encapsulating all security checks: owner whitelist, EIP-712 domain validation, deadline bounds, one-time signing guard, owner signature verification, URI binding |
| generateRegistrationSignature(account, params) | Sign the agent-side RegisterAgentWallet EIP-712 message (includes agentURIHash) |
| verifyOwnerSignature(params) | Verify the owner's RegistrationRequest EIP-712 signature |
| accountFromPrivateKey(hex) | Derive a viem LocalAccount from a hex private key |
Produce the agent’s EIP-712 signature (agent)
If you only need the raw agent signature, call generateRegistrationSignature directly:
import {
accountFromPrivateKey,
generateRegistrationSignature,
} from "@kalkilabs/dhruva-agent-tools";
import type { Address, Hex } from "viem";
const account = accountFromPrivateKey(process.env.AGENT_PRIVATE_KEY as Hex);
const agentSignature = await generateRegistrationSignature(account, {
nftOwner: "0xOwner..." as Address,
agentWallet: account.address,
agentURI: "ipfs://bafy.../agent.json",
deadline: 1712345678n,
registryAddress: "0xRegistry..." as Address,
chainId: 80002,
});If you want the recommended “safe by default” flow (whitelist + deadline bounds + one-time guard), use DhruvaSigningPolicy.evaluate(...) (see the examples below).
Recommended signing policy flow (agent)
DhruvaSigningPolicy.evaluate(...) accepts a base64-encoded JSON challenge produced by the Dhruva client. The decoded payload includes the EIP-712 domain + message the owner is asking the agent to co-sign.
- The policy enforces: whitelist, chain/domain binding, deadline bounds, and one-time signing guard.
- On success it returns the agent’s
RegisterAgentWalletEIP-712 signature (agentSignature).
Identity Proof
| Export | Description |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| signIdentityProof(account, config) | Generate an EIP-191 personal_sign identity proof. The message is entirely agent-generated -- no user input enters the signed payload. |
| verifyIdentityProof(proof, maxAgeSeconds?) | Verify an identity proof: prefix, field integrity, signature recovery, and expiration check |
| buildIdentityProofMessage(...) | Build the structured plaintext message for an identity proof |
Verifying an identity proof (server/verifier)
signIdentityProof(...) returns a structured object containing the signed plaintext message and metadata (agent address, timestamp, nonce, registryAddress, chainId). A verifier should call verifyIdentityProof(...) and enforce a max age.
signIdentityProof(account, config) takes:
chainId: requiredregistryAddress: optional (defaults toDHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS=0x2C6AB393D01DBC882640f7C83B420a4859616307)
import { verifyIdentityProof } from "@kalkilabs/dhruva-agent-tools";
// `proof` is the JSON object returned by the agent from signIdentityProof(...)
const result = await verifyIdentityProof(proof, 300); // 5 minutes max age
if (!result.valid) throw new Error(`Invalid identity proof: ${result.reason}`);
// Recovered wallet address that produced the signature
console.log("agentAddress =", result.agentAddress);Constants
| Export | Description |
| --------------------------------------------------- | --------------------------------------------------------- |
| buildDhruvaEIP712Domain(registryAddress, chainId) | Build the EIP-712 domain separator |
| DHRUVA_EIP712_NAME / DHRUVA_EIP712_VERSION | Domain name ("DhruvaAgentIdentity") and version ("1") |
| REGISTRATION_REQUEST_TYPES | Owner-side EIP-712 type (includes agentURIHash) |
| REGISTER_AGENT_WALLET_TYPES | Agent-side EIP-712 type (includes agentURIHash) |
| DHRUVA_IDENTITY_PROOF_PREFIX | Prefix for EIP-191 identity proof messages |
| DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS | Default registry address used in identity proofs |
Integration Guides
Mastra
The @kalkilabs/dhruva-agent-tools package is framework-agnostic and does not ship a Mastra adapter folder. Use the core exports and wrap them with Mastra’s createTool() in your app.
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import {
DhruvaSigningPolicy,
signIdentityProof,
accountFromPrivateKey,
DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS,
getRegistrationStatus,
} from "@kalkilabs/dhruva-agent-tools";
import type { Address, Hex } from "viem";
const agentPrivateKey = process.env.AGENT_PRIVATE_KEY as Hex;
const account = accountFromPrivateKey(agentPrivateKey);
const registryAddress = process.env.DHRUVA_REGISTRY_ADDRESS as Address;
const chainId = Number(process.env.DHRUVA_CHAIN_ID);
const identityProofTool = createTool({
id: "dhruva-identity-proof",
description: "Generate a cryptographic identity proof for this agent.",
inputSchema: z.object({}),
outputSchema: z.any(),
execute: async () => {
return await signIdentityProof(account, {
registryAddress: DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS,
chainId,
});
},
});
const policy = new DhruvaSigningPolicy({
agentPrivateKey,
chainId,
whitelistedOwners: (process.env.DHRUVA_WHITELISTED_OWNERS ?? "")
.split(",")
.filter(Boolean) as Address[],
});
const registrationTool = createTool({
id: "dhruva-registration-signer",
description: "Verify owner request and produce the agent consent signature.",
inputSchema: z.object({
base64Challenge: z.string(),
chainId: z.number(),
}),
outputSchema: z.any(),
execute: async (input) => {
return await policy.evaluate({
base64Challenge: input.base64Challenge,
chainId: input.chainId,
});
},
});
const statusTool = createTool({
id: "dhruva-registration-status",
description: "Check this agent’s on-chain registration status.",
inputSchema: z.object({}),
outputSchema: z.any(),
execute: async () => {
return await getRegistrationStatus({
agentPrivateKey,
registryAddress,
chainId,
rpcUrl: process.env.DHRUVA_RPC_URL as string,
});
},
});
const agent = new Agent({
name: "my-agent",
model: openai("gpt-4o"),
tools: {
"dhruva-registration-signer": registrationTool,
"dhruva-identity-proof": identityProofTool,
"dhruva-registration-status": statusTool,
},
});LangChain / LangGraph
Use the core functions with DynamicStructuredTool:
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import {
DhruvaSigningPolicy,
signIdentityProof,
accountFromPrivateKey,
} from "@kalkilabs/dhruva-agent-tools";
import type { Address, Hex } from "viem";
const account = accountFromPrivateKey(process.env.AGENT_PRIVATE_KEY as Hex);
const chainId = Number(process.env.DHRUVA_CHAIN_ID);
const identityProofTool = new DynamicStructuredTool({
name: "dhruva-identity-proof",
description:
"Generate a cryptographic identity proof proving the agent controls its wallet address.",
schema: z.object({}),
func: async () => {
const proof = await signIdentityProof(account, {
chainId,
});
return JSON.stringify(proof);
},
});
const policy = new DhruvaSigningPolicy({
agentPrivateKey: process.env.AGENT_PRIVATE_KEY as Hex,
chainId,
whitelistedOwners: (process.env.DHRUVA_WHITELISTED_OWNERS ?? "")
.split(",")
.filter(Boolean) as Address[],
});
const registrationTool = new DynamicStructuredTool({
name: "dhruva-registration-signer",
description:
"Handle Dhruva Agent Identity registration requests with EIP-712 mutual authentication.",
schema: z.object({
base64Challenge: z.string(),
chainId: z.number(),
}),
func: async (input) => {
const result = await policy.evaluate({
base64Challenge: input.base64Challenge,
chainId: input.chainId,
});
return JSON.stringify(result);
},
});Vercel AI SDK
Use the core functions with the tool() helper:
import { tool } from "ai";
import { z } from "zod";
import {
signIdentityProof,
accountFromPrivateKey,
DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS,
} from "@kalkilabs/dhruva-agent-tools";
import type { Address, Hex } from "viem";
const account = accountFromPrivateKey(process.env.AGENT_PRIVATE_KEY as Hex);
const chainId = Number(process.env.DHRUVA_CHAIN_ID);
const identityProofTool = tool({
description:
"Generate a cryptographic identity proof proving the agent controls its wallet.",
parameters: z.object({}),
execute: async () => {
return await signIdentityProof(account, {
registryAddress: DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS,
chainId,
});
},
});
Plain Function Calling (No Framework)
import {
DhruvaSigningPolicy,
signIdentityProof,
verifyIdentityProof,
accountFromPrivateKey,
DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS,
} from "@kalkilabs/dhruva-agent-tools";
import type { Address, Hex } from "viem";
const account = accountFromPrivateKey(process.env.AGENT_PRIVATE_KEY as Hex);
const chainId = Number(process.env.DHRUVA_CHAIN_ID);
// Generate an identity proof
const proof = await signIdentityProof(account, {
registryAddress: DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS,
chainId,
});
// Verify a proof (verifier side)
const result = await verifyIdentityProof(proof, 300);
console.log(result.valid, result.agentAddress);
Security Model
| Protection | Mechanism |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Private key isolation | All config interfaces accept typed parameters sourced from process.env at initialization time. Private keys never appear in tool input schemas or LLM-visible data. |
| Owner whitelist | DhruvaSigningPolicy rejects registration requests from non-whitelisted owners |
| One-time signing guard | Agent can only consent to registration once per DhruvaSigningPolicy instance |
| EIP-712 domain separation | Signatures are bound to a specific registry address and chain ID, preventing cross-chain replay |
| URI binding | agentURIHash is included in both RegistrationRequest and RegisterAgentWallet EIP-712 types, binding signatures to a specific registration file |
| Deadline validation | Rejects expired deadlines and deadlines too far in the future |
| Identity proof anti-replay | Each proof includes a cryptographic nonce (crypto.getRandomValues) and timestamp; verifier checks expiration |
| Agent-controlled messages | Identity proofs are entirely agent-generated -- no user input enters the signed payload, preventing social engineering |
License
MIT
