From e8e94a071e1cae39f04a6c4472a58d1556e8dd31 Mon Sep 17 00:00:00 2001 From: AntoineSakkalis Date: Sat, 15 Aug 2026 14:02:57 +0200 Subject: [PATCH] feat(typescript): add M2M Sentinel action provider for Base bytecode & proxy observations --- .../agentkit/src/action-providers/index.ts | 1 + .../action-providers/m2mSentinel/README.md | 26 +++ .../action-providers/m2mSentinel/constants.ts | 3 + .../src/action-providers/m2mSentinel/index.ts | 2 + .../m2mSentinelActionProvider.test.ts | 63 ++++++ .../m2mSentinel/m2mSentinelActionProvider.ts | 199 ++++++++++++++++++ .../action-providers/m2mSentinel/schemas.ts | 55 +++++ 7 files changed, 349 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/m2mSentinel/README.md create mode 100644 typescript/agentkit/src/action-providers/m2mSentinel/constants.ts create mode 100644 typescript/agentkit/src/action-providers/m2mSentinel/index.ts create mode 100644 typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/m2mSentinel/schemas.ts diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..ba6eed083 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -18,6 +18,7 @@ export * from "./erc721"; export * from "./erc8004"; export * from "./farcaster"; export * from "./jupiter"; +export * from "./m2mSentinel"; export * from "./messari"; export * from "./pyth"; export * from "./moonwell"; diff --git a/typescript/agentkit/src/action-providers/m2mSentinel/README.md b/typescript/agentkit/src/action-providers/m2mSentinel/README.md new file mode 100644 index 000000000..2af65f46b --- /dev/null +++ b/typescript/agentkit/src/action-providers/m2mSentinel/README.md @@ -0,0 +1,26 @@ +# M2M Sentinel Action Provider + +The M2M Sentinel Action Provider equips AgentKit agents with smart contract bytecode analysis, proxy detection, and token capability observations on Base Mainnet. + +## Capabilities + +- **`audit_contract`**: Inspect target contract bytecode capability observations, proxy implementation slots (EIP-1967/UUPS/Beacon), and known limitation heuristics in <35ms. +- **`get_gas_metrics`**: Get real-time Base network gas metrics and fee recommendations. +- **`get_token_price`**: Observe real-time Base DEX token prices for slippage checks and preflight valuation. +- **`get_service_status`**: Check operational status and trust quorum of M2M Sentinel verification rails. + +## Usage + +```typescript +import { AgentKit } from "@coinbase/agentkit"; +import { m2mSentinelActionProvider } from "@coinbase/agentkit/action-providers/m2mSentinel"; + +const agentKit = await AgentKit.from({ + walletProvider, + actionProviders: [ + m2mSentinelActionProvider({ + apiKey: process.env.M2M_SENTINEL_API_KEY, // Optional, free tier available at m2msentinel.vercel.app + }), + ], +}); +``` diff --git a/typescript/agentkit/src/action-providers/m2mSentinel/constants.ts b/typescript/agentkit/src/action-providers/m2mSentinel/constants.ts new file mode 100644 index 000000000..62aef73f6 --- /dev/null +++ b/typescript/agentkit/src/action-providers/m2mSentinel/constants.ts @@ -0,0 +1,3 @@ +export const SUPPORTED_NETWORKS = ["base-mainnet", "base-sepolia"]; + +export const DEFAULT_BASE_URL = "https://m2msentinel.vercel.app"; diff --git a/typescript/agentkit/src/action-providers/m2mSentinel/index.ts b/typescript/agentkit/src/action-providers/m2mSentinel/index.ts new file mode 100644 index 000000000..6fc679886 --- /dev/null +++ b/typescript/agentkit/src/action-providers/m2mSentinel/index.ts @@ -0,0 +1,2 @@ +export * from "./m2mSentinelActionProvider"; +export type { M2MSentinelConfig } from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.test.ts b/typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.test.ts new file mode 100644 index 000000000..3dc9f6981 --- /dev/null +++ b/typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { m2mSentinelActionProvider, M2MSentinelActionProvider } from "./m2mSentinelActionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { Network } from "../../network"; + +describe("M2MSentinelActionProvider", () => { + let provider: M2MSentinelActionProvider; + let mockWallet: EvmWalletProvider; + + beforeEach(() => { + provider = m2mSentinelActionProvider(); + mockWallet = {} as unknown as EvmWalletProvider; + vi.restoreAllMocks(); + }); + + it("should have the correct provider name", () => { + expect(provider.name).toBe("m2m_sentinel"); + }); + + it("should support Base networks", () => { + const baseMainnet: Network = { protocolFamily: "evm", chainId: "8453", networkId: "base-mainnet" }; + const baseSepolia: Network = { protocolFamily: "evm", chainId: "84532", networkId: "base-sepolia" }; + const solana: Network = { protocolFamily: "svm", chainId: "solana", networkId: "solana" }; + + expect(provider.supportsNetwork(baseMainnet)).toBe(true); + expect(provider.supportsNetwork(baseSepolia)).toBe(true); + expect(provider.supportsNetwork(solana)).toBe(false); + }); + + it("should expose all 4 actions", () => { + const actions = provider.getActions(mockWallet); + expect(actions).toHaveLength(4); + + const actionNames = actions.map((a) => a.name); + expect(actionNames).toContain("m2m_audit_contract"); + expect(actionNames).toContain("m2m_get_gas_metrics"); + expect(actionNames).toContain("m2m_get_token_price"); + expect(actionNames).toContain("m2m_get_service_status"); + }); + + it("should handle auditContract successfully", async () => { + const mockAudit = { + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + proxyResolution: { isProxy: true }, + provenance: { trustLevel: "HIGH_TRUST_PRIMARY" }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockAudit, + } as Response); + + const result = await provider.auditContract(mockWallet, { + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + }); + + const parsed = JSON.parse(result); + expect(parsed.status).toBe("SUCCESS"); + expect(parsed.notASafetyGuarantee).toBe(true); + expect(parsed.data.address).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); + }); +}); diff --git a/typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.ts b/typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.ts new file mode 100644 index 000000000..f44eb656c --- /dev/null +++ b/typescript/agentkit/src/action-providers/m2mSentinel/m2mSentinelActionProvider.ts @@ -0,0 +1,199 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { + AuditContractSchema, + GetGasMetricsSchema, + GetTokenPriceSchema, + GetServiceStatusSchema, + M2MSentinelConfig, +} from "./schemas"; +import { SUPPORTED_NETWORKS, DEFAULT_BASE_URL } from "./constants"; + +/** + * M2MSentinelActionProvider enables AI agents on Base to perform preflight bytecode + * inspection, EIP-1967/UUPS proxy detection, token capability observations, and network metrics. + */ +export class M2MSentinelActionProvider extends ActionProvider { + private readonly baseUrl: string; + private readonly apiKey?: string; + + /** + * Creates an instance of M2MSentinelActionProvider. + * + * @param config - Optional configuration object + */ + constructor(config?: M2MSentinelConfig) { + super("m2m_sentinel", []); + this.baseUrl = config?.baseUrl || DEFAULT_BASE_URL; + this.apiKey = config?.apiKey || process.env.M2M_SENTINEL_API_KEY; + } + + /** + * Checks if the target network is supported by M2M Sentinel. + * + * @param network - The network instance to validate + * @returns True if network is Base mainnet or Base Sepolia + */ + supportsNetwork(network: Network): boolean { + const chainId = String(network.chainId || network.networkId || ""); + const protocolFamily = String(network.protocolFamily || "evm").toLowerCase(); + return ( + protocolFamily === "evm" && + (SUPPORTED_NETWORKS.includes(network.networkId || "") || + chainId === "8453" || + chainId === "84532") + ); + } + + private async fetchApi(path: string, init?: RequestInit): Promise { + const headers: Record = { + Accept: "application/json", + "User-Agent": "CoinbaseAgentKit-M2MSentinel/1.1.0", + ...(init?.headers as Record), + }; + if (this.apiKey && !headers["x-api-key"]) { + headers["x-api-key"] = this.apiKey; + } + return fetch(`${this.baseUrl}${path}`, { + ...init, + headers, + }); + } + + /** + * Inspects a Base target contract for bytecode capability observations, proxy implementation slots, and limitations. + * + * @param _wallet - The wallet provider instance (not consumed for read operations) + * @param args - Arguments containing target contract address + * @returns Factual audit observations and proxy resolution + */ + @CreateAction({ + name: "audit_contract", + description: + "Inspect Base smart contract bytecode capability observations, proxy implementation slots, and limitations before executing transactions. Returns factual evidence, not a safety guarantee.", + schema: AuditContractSchema, + }) + async auditContract( + _wallet: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const res = await this.fetchApi(`/v1/audit/${encodeURIComponent(args.address)}`); + if (res.status === 402) { + return JSON.stringify({ + status: "PAYMENT_REQUIRED", + message: "Payment required. Authenticate with an M2M Sentinel API key or settle the x402 challenge.", + notASafetyGuarantee: true, + }); + } + if (!res.ok) { + const errBody = await res.json().catch(() => ({})); + return JSON.stringify({ + status: "ERROR", + statusCode: res.status, + message: errBody.message || `Audit request failed with HTTP ${res.status}`, + notASafetyGuarantee: true, + }); + } + const data = await res.json(); + return JSON.stringify({ + status: "SUCCESS", + data, + notASafetyGuarantee: true, + }); + } catch (err) { + return JSON.stringify({ + status: "ERROR", + message: err instanceof Error ? err.message : String(err), + notASafetyGuarantee: true, + }); + } + } + + /** + * Retrieves real-time Base network gas execution metrics. + * + * @param _wallet - The wallet provider instance + * @param _args - Empty arguments + * @returns Base network gas recommendations + */ + @CreateAction({ + name: "get_gas_metrics", + description: "Get real-time Base network gas execution metrics and fee recommendations before submitting transactions.", + schema: GetGasMetricsSchema, + }) + async getGasMetrics( + _wallet: EvmWalletProvider, + _args: z.infer, + ): Promise { + try { + const res = await this.fetchApi("/v1/gas/fees"); + if (!res.ok) { + return JSON.stringify({ status: "ERROR", statusCode: res.status, message: "Failed to retrieve gas fees" }); + } + return JSON.stringify(await res.json()); + } catch (err) { + return JSON.stringify({ status: "ERROR", message: err instanceof Error ? err.message : String(err) }); + } + } + + /** + * Observes real-time Base DEX token price for slippage check and preflight valuation. + * + * @param _wallet - The wallet provider instance + * @param args - Token symbol argument + * @returns Token price observation + */ + @CreateAction({ + name: "get_token_price", + description: "Observe real-time Base DEX token price for slippage verification and valuation.", + schema: GetTokenPriceSchema, + }) + async getTokenPrice( + _wallet: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const res = await this.fetchApi(`/v1/token/price/${encodeURIComponent(args.symbol.toUpperCase())}`); + if (!res.ok) { + return JSON.stringify({ status: "ERROR", statusCode: res.status, message: `Failed to retrieve price for ${args.symbol}` }); + } + return JSON.stringify(await res.json()); + } catch (err) { + return JSON.stringify({ status: "ERROR", message: err instanceof Error ? err.message : String(err) }); + } + } + + /** + * Checks operational status of M2M Sentinel verification rails. + * + * @param _wallet - The wallet provider instance + * @param _args - Empty arguments + * @returns Service status response + */ + @CreateAction({ + name: "get_service_status", + description: "Check operational status of M2M Sentinel upstream verification rails and quorum.", + schema: GetServiceStatusSchema, + }) + async getServiceStatus( + _wallet: EvmWalletProvider, + _args: z.infer, + ): Promise { + try { + const res = await this.fetchApi("/v1/status"); + if (!res.ok) { + return JSON.stringify({ status: "UNAVAILABLE", statusCode: res.status }); + } + return JSON.stringify(await res.json()); + } catch (err) { + return JSON.stringify({ status: "UNAVAILABLE", message: err instanceof Error ? err.message : String(err) }); + } + } +} + +export const m2mSentinelActionProvider = (config?: M2MSentinelConfig) => + new M2MSentinelActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/m2mSentinel/schemas.ts b/typescript/agentkit/src/action-providers/m2mSentinel/schemas.ts new file mode 100644 index 000000000..976766b29 --- /dev/null +++ b/typescript/agentkit/src/action-providers/m2mSentinel/schemas.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +/** + * Input schema for auditing a smart contract on Base. + */ +export const AuditContractSchema = z + .object({ + address: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid contract address format (must be 0x followed by 40 hex characters)") + .describe("Target Base mainnet smart contract address to inspect"), + }) + .strip() + .describe("Instructions for auditing a contract on Base"); + +/** + * Input schema for getting real-time Base network gas execution metrics. + */ +export const GetGasMetricsSchema = z + .object({}) + .strip() + .describe("Instructions for getting Base network gas metrics"); + +/** + * Input schema for retrieving Base DEX token prices. + */ +export const GetTokenPriceSchema = z + .object({ + symbol: z + .string() + .min(1, "Token symbol is required") + .describe("Token symbol on Base (e.g. USDC, WETH, cbBTC)"), + }) + .strip() + .describe("Instructions for retrieving Base token prices"); + +/** + * Input schema for retrieving M2M Sentinel service status. + */ +export const GetServiceStatusSchema = z + .object({}) + .strip() + .describe("Instructions for retrieving M2M Sentinel service operational status"); + +/** + * Configuration options for M2MSentinelActionProvider. + */ +export const M2MSentinelConfigSchema = z + .object({ + baseUrl: z.string().url().optional().describe("Custom M2M Sentinel base URL"), + apiKey: z.string().optional().describe("Optional M2M Sentinel API key for authenticated quota"), + }) + .optional(); + +export type M2MSentinelConfig = z.infer;