From 9ffa95fb8e0542c35ff7ebbab5a92920c7804a34 Mon Sep 17 00:00:00 2001 From: aetos53t Date: Fri, 20 Feb 2026 11:20:57 -0500 Subject: [PATCH] feat(multisig): add multisig action provider for external coordination Add MultisigActionProvider enabling AgentKit agents to participate in multi-agent multisig wallets (2-of-3, 3-of-5, etc.) with external coordination protocols. New Actions: - sign_digest: Sign raw 32-byte digests for external multisig coordination - sign_safe_transaction: Sign Safe (Gnosis Safe) transaction hashes - get_multisig_pubkey: Get public key for registering with coordinators Use cases: - Multi-agent treasuries across different wallet providers - Cross-provider coordination (AgentKit + aibtc + Claw Cash) - Safe multisig participation Security considerations documented - agents should validate full transaction details before signing digests. Includes tests and integration documentation. --- .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/multisig/README.md | 141 +++++++++++ .../src/action-providers/multisig/index.ts | 2 + .../multisig/multisigActionProvider.test.ts | 142 +++++++++++ .../multisig/multisigActionProvider.ts | 236 ++++++++++++++++++ .../src/action-providers/multisig/schemas.ts | 36 +++ 6 files changed, 558 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/multisig/README.md create mode 100644 typescript/agentkit/src/action-providers/multisig/index.ts create mode 100644 typescript/agentkit/src/action-providers/multisig/multisigActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/multisig/multisigActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/multisig/schemas.ts diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 7f2b0233a..c20d287e8 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -21,6 +21,7 @@ export * from "./messari"; export * from "./pyth"; export * from "./moonwell"; export * from "./morpho"; +export * from "./multisig"; export * from "./opensea"; export * from "./spl"; export * from "./superfluid"; diff --git a/typescript/agentkit/src/action-providers/multisig/README.md b/typescript/agentkit/src/action-providers/multisig/README.md new file mode 100644 index 000000000..b2bf8fef3 --- /dev/null +++ b/typescript/agentkit/src/action-providers/multisig/README.md @@ -0,0 +1,141 @@ +# Multisig Action Provider + +Enable AgentKit agents to participate in multi-agent multisig wallets. + +## Overview + +This action provider allows AgentKit agents to: +- Sign digests for external multisig coordination +- Sign Safe (Gnosis Safe) transaction hashes +- Share their public key with multisig coordinators + +## Use Cases + +### Multi-Agent Treasuries + +Multiple AI agents from different providers can share a multisig wallet: + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ AgentKit │ │ aibtc │ │Claw Cash │ +│ Agent │ │ Agent │ │ Agent │ +└────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ + └───────────────┼───────────────┘ + │ + ┌──────▼──────┐ + │ 2-of-3 │ + │ Multisig │ + │ (Safe) │ + └─────────────┘ +``` + +### Cross-Provider Coordination + +The coordination flow: +1. Each agent registers their public key with a coordinator +2. Coordinator generates a multisig address +3. Anyone can fund the multisig +4. When spending, coordinator creates a transaction proposal +5. Each agent validates and signs their portion +6. Coordinator assembles signatures and broadcasts + +## Actions + +### `sign_digest` + +Signs a raw 32-byte digest for external coordination protocols. + +```typescript +const result = await agent.run({ + action: "sign_digest", + args: { + digest: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } +}); +``` + +**Security:** Only sign digests after validating the full transaction details from the coordinator. + +### `sign_safe_transaction` + +Signs a Safe (Gnosis Safe) transaction hash. + +```typescript +const result = await agent.run({ + action: "sign_safe_transaction", + args: { + safeAddress: "0x...", + safeTxHash: "a1b2c3d4..." + } +}); +``` + +### `get_multisig_pubkey` + +Gets the agent's public key for registering with a multisig. + +```typescript +const result = await agent.run({ + action: "get_multisig_pubkey", + args: {} +}); +``` + +## Integration Example + +With [Agent Multisig Coordination API](https://github.com/aetos53t/agent-multisig-api): + +```typescript +import { AgentKit, multisigActionProvider } from "@coinbase/agentkit"; + +// Initialize agent with multisig actions +const agent = new AgentKit({ + actionProviders: [multisigActionProvider()] +}); + +// 1. Get public key for registration +const pubkeyResult = await agent.run({ action: "get_multisig_pubkey" }); + +// 2. Register with coordination API +await fetch("https://api.agentmultisig.dev/v1/agents", { + method: "POST", + body: JSON.stringify({ + id: "my-agent", + publicKey: pubkeyResult.address, + provider: "agentkit" + }) +}); + +// 3. When a proposal needs signing, sign the digest +const signResult = await agent.run({ + action: "sign_digest", + args: { digest: proposalSighash } +}); + +// 4. Submit signature to coordinator +await fetch(`https://api.agentmultisig.dev/v1/proposals/${id}/sign`, { + method: "POST", + body: JSON.stringify({ + agentId: "my-agent", + signature: signResult.signature + }) +}); +``` + +## Security Considerations + +1. **Never blind sign**: Always validate the full transaction before signing a digest +2. **Verify coordinators**: Only work with trusted coordination protocols +3. **Check transaction details**: Ensure inputs, outputs, and amounts are correct +4. **Use threshold signing**: M-of-N schemes protect against single point of failure + +## Supported Networks + +Currently supports EVM networks (Ethereum, Base, Arbitrum, etc.). Solana support planned. + +## Related + +- [Agent Multisig Coordination API](https://github.com/aetos53t/agent-multisig-api) - Coordination layer +- [Safe Protocol](https://docs.safe.global/) - EVM multisig standard +- [aibtc MCP Server](https://github.com/aibtcdev/aibtc-mcp-server) - Bitcoin agent integration diff --git a/typescript/agentkit/src/action-providers/multisig/index.ts b/typescript/agentkit/src/action-providers/multisig/index.ts new file mode 100644 index 000000000..7021dfb3b --- /dev/null +++ b/typescript/agentkit/src/action-providers/multisig/index.ts @@ -0,0 +1,2 @@ +export { MultisigActionProvider, multisigActionProvider } from "./multisigActionProvider"; +export { SignDigestSchema, SignSafeTransactionSchema, GetPublicKeySchema } from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/multisig/multisigActionProvider.test.ts b/typescript/agentkit/src/action-providers/multisig/multisigActionProvider.test.ts new file mode 100644 index 000000000..677a78820 --- /dev/null +++ b/typescript/agentkit/src/action-providers/multisig/multisigActionProvider.test.ts @@ -0,0 +1,142 @@ +import { multisigActionProvider, MultisigActionProvider } from "./multisigActionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; + +describe("MultisigActionProvider", () => { + let provider: MultisigActionProvider; + let mockWalletProvider: jest.Mocked; + + beforeEach(() => { + provider = multisigActionProvider(); + + mockWalletProvider = { + getAddress: jest.fn().mockReturnValue("0x1234567890123456789012345678901234567890"), + getNetwork: jest.fn().mockReturnValue({ + protocolFamily: "evm", + networkId: "base-mainnet", + chainId: "8453", + }), + sign: jest.fn().mockResolvedValue( + "0x" + "ab".repeat(65), // Mock signature + ), + signMessage: jest.fn(), + signTypedData: jest.fn(), + signTransaction: jest.fn(), + sendTransaction: jest.fn(), + waitForTransactionReceipt: jest.fn(), + nativeTransfer: jest.fn(), + getBalance: jest.fn(), + getName: jest.fn().mockReturnValue("mock-wallet"), + readContract: jest.fn(), + getPublicClient: jest.fn(), + } as unknown as jest.Mocked; + }); + + describe("signDigest", () => { + const validDigest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + it("should sign a valid digest without 0x prefix", async () => { + const result = await provider.signDigest(mockWalletProvider, { digest: validDigest }); + + expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validDigest}`); + expect(result).toContain("Digest signed successfully"); + expect(result).toContain("Signature:"); + }); + + it("should sign a valid digest with 0x prefix", async () => { + const result = await provider.signDigest(mockWalletProvider, { + digest: `0x${validDigest}`, + }); + + expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validDigest}`); + expect(result).toContain("Digest signed successfully"); + }); + + it("should handle uppercase digest", async () => { + const result = await provider.signDigest(mockWalletProvider, { + digest: validDigest.toUpperCase(), + }); + + expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validDigest}`); + expect(result).toContain("Digest signed successfully"); + }); + + it("should reject digest with wrong length", async () => { + const result = await provider.signDigest(mockWalletProvider, { digest: "abc123" }); + + expect(mockWalletProvider.sign).not.toHaveBeenCalled(); + expect(result).toContain("Error: Digest must be exactly 32 bytes"); + }); + + it("should handle signing errors", async () => { + mockWalletProvider.sign.mockRejectedValueOnce(new Error("Signing failed")); + + const result = await provider.signDigest(mockWalletProvider, { digest: validDigest }); + + expect(result).toContain("Error signing digest"); + }); + }); + + describe("signSafeTransaction", () => { + const validSafeTxHash = "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"; + const safeAddress = "0xSafe1234567890123456789012345678901234"; + + it("should sign a valid Safe transaction hash", async () => { + const result = await provider.signSafeTransaction(mockWalletProvider, { + safeAddress, + safeTxHash: validSafeTxHash, + }); + + expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validSafeTxHash}`); + expect(result).toContain("Safe transaction signed successfully"); + expect(result).toContain(safeAddress); + }); + + it("should handle 0x prefix in safeTxHash", async () => { + const result = await provider.signSafeTransaction(mockWalletProvider, { + safeAddress, + safeTxHash: `0x${validSafeTxHash}`, + }); + + expect(mockWalletProvider.sign).toHaveBeenCalledWith(`0x${validSafeTxHash}`); + expect(result).toContain("Safe transaction signed successfully"); + }); + + it("should reject invalid safeTxHash length", async () => { + const result = await provider.signSafeTransaction(mockWalletProvider, { + safeAddress, + safeTxHash: "invalid", + }); + + expect(mockWalletProvider.sign).not.toHaveBeenCalled(); + expect(result).toContain("Error: safeTxHash must be exactly 32 bytes"); + }); + }); + + describe("getMultisigPubkey", () => { + it("should return wallet address and network info", async () => { + const result = await provider.getMultisigPubkey(mockWalletProvider, {}); + + expect(result).toContain("Multisig Public Key Info"); + expect(result).toContain("0x1234567890123456789012345678901234567890"); + expect(result).toContain("base-mainnet"); + expect(result).toContain("evm"); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for EVM networks", () => { + const network = { protocolFamily: "evm", networkId: "base-mainnet", chainId: "8453" }; + expect(provider.supportsNetwork(network)).toBe(true); + }); + + it("should return false for non-EVM networks", () => { + const network = { protocolFamily: "svm", networkId: "solana-mainnet" }; + expect(provider.supportsNetwork(network)).toBe(false); + }); + + it("should return false for Bitcoin networks", () => { + const network = { protocolFamily: "bitcoin", networkId: "bitcoin-mainnet" }; + expect(provider.supportsNetwork(network)).toBe(false); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/multisig/multisigActionProvider.ts b/typescript/agentkit/src/action-providers/multisig/multisigActionProvider.ts new file mode 100644 index 000000000..48388530d --- /dev/null +++ b/typescript/agentkit/src/action-providers/multisig/multisigActionProvider.ts @@ -0,0 +1,236 @@ +import { z } from "zod"; + +import { CreateAction } from "../actionDecorator"; +import { ActionProvider } from "../actionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { Network } from "../../network"; + +import { SignDigestSchema, SignSafeTransactionSchema, GetPublicKeySchema } from "./schemas"; + +/** + * MultisigActionProvider enables agents to participate in external multisig coordination. + * + * Use cases: + * - Multi-agent treasuries (2-of-3, 3-of-5, etc.) + * - Cross-provider coordination (AgentKit + aibtc + Claw Cash agents sharing a multisig) + * - Safe (Gnosis Safe) multisig participation + * - External coordination protocols + * + * Security note: These actions sign digests/hashes directly. The calling agent is responsible + * for validating what they're signing (e.g., verifying PSBT contents, Safe transaction details) + * before requesting a signature. The coordination protocol should provide full transaction + * visibility before signing. + */ +export class MultisigActionProvider extends ActionProvider { + /** + * Constructor for the MultisigActionProvider. + */ + constructor() { + super("multisig", []); + } + + /** + * Signs a raw 32-byte digest using the wallet's private key. + * + * This is the primitive for external multisig coordination. The coordinator provides + * a digest (e.g., BIP-341 sighash for Taproot, or keccak256 hash for EVM), and the + * agent signs it. + * + * SECURITY: Only sign digests from trusted coordination protocols that have shown + * you the full transaction details. Never sign arbitrary digests from unknown sources. + * + * @param walletProvider - The wallet provider to sign with. + * @param args - The digest to sign. + * @returns The signature and public key. + */ + @CreateAction({ + name: "sign_digest", + description: ` +Signs a raw 32-byte digest (hash) for external multisig coordination. + +Use this when: +- Participating in a multi-agent multisig (2-of-3, 3-of-5, etc.) +- A coordination protocol provides a sighash/digest to sign +- You've validated the underlying transaction and want to authorize it + +Input: +- digest: 32-byte hash as hex string (64 chars, with or without 0x prefix) + +Returns: +- signature: The ECDSA signature (hex) +- publicKey: Your public key for the coordinator to assemble the multisig + +SECURITY: Only sign digests after validating the full transaction from the coordinator. +`, + schema: SignDigestSchema, + }) + async signDigest( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + // Normalize digest: ensure 0x prefix, lowercase + let digest = args.digest.toLowerCase(); + if (!digest.startsWith("0x")) { + digest = `0x${digest}`; + } + + // Validate length (should be 32 bytes = 64 hex chars + 2 for 0x) + if (digest.length !== 66) { + return `Error: Digest must be exactly 32 bytes (64 hex characters). Got ${digest.length - 2} characters.`; + } + + try { + const signature = await walletProvider.sign(digest as `0x${string}`); + const address = walletProvider.getAddress(); + + return [ + "Digest signed successfully!", + "", + "Signature Details:", + `- Digest: ${digest}`, + `- Signature: ${signature}`, + `- Signer Address: ${address}`, + "", + "The coordinator can now include this signature in the multisig transaction.", + ].join("\n"); + } catch (error) { + return `Error signing digest: ${error}`; + } + } + + /** + * Signs a Safe (Gnosis Safe) transaction hash for multisig approval. + * + * Safe multisigs use EIP-712 typed data signing. This action signs the safeTxHash + * which can then be submitted to the Safe Transaction Service or used to execute + * the transaction on-chain. + * + * @param walletProvider - The wallet provider to sign with. + * @param args - The Safe address and transaction hash. + * @returns The signature for the Safe transaction. + */ + @CreateAction({ + name: "sign_safe_transaction", + description: ` +Signs a Safe (Gnosis Safe) multisig transaction hash. + +Use this when: +- You're a signer on a Safe multisig +- A transaction has been proposed and you want to approve it +- You have the safeTxHash from the Safe Transaction Service or coordinator + +Input: +- safeAddress: The Safe contract address +- safeTxHash: The Safe transaction hash to sign (32 bytes hex) + +Returns: +- signature: The EIP-712 signature for the Safe +- Signer address for verification + +After signing, the signature can be submitted to the Safe Transaction Service +or used to execute the transaction directly. +`, + schema: SignSafeTransactionSchema, + }) + async signSafeTransaction( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + // Normalize safeTxHash + let safeTxHash = args.safeTxHash.toLowerCase(); + if (!safeTxHash.startsWith("0x")) { + safeTxHash = `0x${safeTxHash}`; + } + + if (safeTxHash.length !== 66) { + return `Error: safeTxHash must be exactly 32 bytes (64 hex characters). Got ${safeTxHash.length - 2} characters.`; + } + + try { + // For Safe, we sign the safeTxHash directly (it's already the EIP-712 hash) + const signature = await walletProvider.sign(safeTxHash as `0x${string}`); + const address = walletProvider.getAddress(); + + return [ + "Safe transaction signed successfully!", + "", + "Signature Details:", + `- Safe Address: ${args.safeAddress}`, + `- Safe TX Hash: ${safeTxHash}`, + `- Signature: ${signature}`, + `- Signer: ${address}`, + "", + "Submit this signature to the Safe Transaction Service or use it to execute.", + ].join("\n"); + } catch (error) { + return `Error signing Safe transaction: ${error}`; + } + } + + /** + * Gets the wallet's public key/address for multisig registration. + * + * When joining a multisig, the coordinator needs each signer's public key + * to generate the multisig address. This returns the address that can be + * used for that purpose. + * + * @param walletProvider - The wallet provider. + * @param _ - Empty args object. + * @returns The wallet's address for multisig registration. + */ + @CreateAction({ + name: "get_multisig_pubkey", + description: ` +Gets your public key/address for registering with a multisig coordinator. + +Use this when: +- Joining a new multi-agent multisig +- A coordinator asks for your public key to generate the multisig address +- Setting up a new Safe or other multisig wallet + +Returns: +- Your wallet address (which can be used as your public identifier in multisigs) +- Network information +`, + schema: GetPublicKeySchema, + }) + async getMultisigPubkey( + walletProvider: EvmWalletProvider, + _: z.infer, + ): Promise { + try { + const address = walletProvider.getAddress(); + const network = walletProvider.getNetwork(); + + return [ + "Multisig Public Key Info:", + "", + `- Address: ${address}`, + `- Network: ${network.networkId || network.chainId}`, + `- Protocol: ${network.protocolFamily}`, + "", + "Share this address with the multisig coordinator to register as a signer.", + ].join("\n"); + } catch (error) { + return `Error getting public key: ${error}`; + } + } + + /** + * Checks if the multisig action provider supports the given network. + * Currently supports EVM networks only. + * + * @param network - The network to check. + * @returns True if the network is EVM-based. + */ + supportsNetwork = (network: Network): boolean => { + return network.protocolFamily === "evm"; + }; +} + +/** + * Factory function to create a new MultisigActionProvider instance. + * + * @returns A new MultisigActionProvider instance. + */ +export const multisigActionProvider = () => new MultisigActionProvider(); diff --git a/typescript/agentkit/src/action-providers/multisig/schemas.ts b/typescript/agentkit/src/action-providers/multisig/schemas.ts new file mode 100644 index 000000000..e8bfc755f --- /dev/null +++ b/typescript/agentkit/src/action-providers/multisig/schemas.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +/** + * Input schema for signing a digest (raw 32-byte hash) + */ +export const SignDigestSchema = z + .object({ + digest: z + .string() + .describe( + "The 32-byte digest to sign, as a hex string (64 characters, with or without 0x prefix)", + ), + }) + .strip() + .describe("Input schema for signing a raw digest for external multisig coordination"); + +/** + * Input schema for signing EIP-712 typed data (for Safe multisig) + */ +export const SignSafeTransactionSchema = z + .object({ + safeAddress: z.string().describe("The Safe multisig contract address"), + safeTxHash: z + .string() + .describe("The Safe transaction hash to sign (32 bytes hex, with or without 0x prefix)"), + }) + .strip() + .describe("Input schema for signing a Safe (Gnosis Safe) transaction hash"); + +/** + * Input schema for getting the agent's public key + */ +export const GetPublicKeySchema = z + .object({}) + .strip() + .describe("Input schema for getting the agent's public key for multisig coordination");