npm package: @kalkilabs/dhruva-agent-tools
Description: 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.
Latest version: 0.1.3

Framework agnostic tools for Dhruva Agent Identity (**ERC-8004 compliant**: [EIP-8004 spec](https://eips.ethereum.org/EIPS/eip-8004)) 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](https://viem.sh) and [zod](https://zod.dev). No agent framework is required. Optional Mastra adapter available via subpath import.

## Installation

```bash
npm install @kalkilabs/dhruva-agent-tools
```

## Core 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:

```typescript
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 `RegisterAgentWallet` EIP-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`**: required
- **`registryAddress`**: optional (defaults to `DHRUVA_IDENTITY_PROOF_REGISTRY_ADDRESS` = `0x2C6AB393D01DBC882640f7C83B420a4859616307`)

```typescript
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.

```typescript
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`:

```typescript
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:

```typescript
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)

```typescript
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