diff --git a/docs/docs-package/satp-agent-trust.md b/docs/docs-package/satp-agent-trust.md new file mode 100644 index 0000000..822c342 --- /dev/null +++ b/docs/docs-package/satp-agent-trust.md @@ -0,0 +1,117 @@ +# SATP Agent Trust Verification + +Verify agent identity and behavioral trust scores using [AgentFolio/SATP](https://github.com/brainAI-bot/satp-solana-sdk) (Solana Agent Trust Protocol). + +## Overview + +The `SATPProvider` is an auth provider that checks an agent's on-chain trust score before allowing tool execution. It answers: **"Should I trust this agent for this task?"** + +## Quick Start + +```typescript +import { MCPServer, SATPProvider } from "mcp-framework"; + +const server = new MCPServer({ + auth: { + provider: new SATPProvider({ + minTrustScore: 50, + onMissing: "allow", // Don't break unidentified agents + }), + }, +}); +``` + +No API keys needed — the AgentFolio API is public. + +## Configuration + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `apiUrl` | `string` | `"https://api.agentfolio.bot"` | AgentFolio API base URL | +| `minTrustScore` | `number` | `0` | Minimum trust score (0-100) to allow access | +| `requireVerified` | `boolean` | `false` | Require on-chain verification | +| `agentIdHeader` | `string` | `"x-agent-id"` | Header name for agent identity | +| `onMissing` | `"allow" \| "reject"` | `"allow"` | Behavior when agent identity is missing | +| `cacheTtlMs` | `number` | `300000` | Cache TTL in ms (default 5 min) | + +## How It Works + +1. Agent sends request with `x-agent-id` header (or `Authorization: Agent `) +2. Provider queries AgentFolio for the agent's trust data +3. If trust score meets threshold → request proceeds with trust data attached +4. If below threshold → request rejected with `403` and `X-Trust-Required` header + +## Modes + +### Annotation mode (default) +```typescript +new SATPProvider({ onMissing: "allow", minTrustScore: 0 }) +``` +All requests pass through. Trust data is attached to `AuthResult.data.agentTrust` for your tool handlers to use (or ignore). + +### Enforcement mode +```typescript +new SATPProvider({ + minTrustScore: 50, + requireVerified: true, + onMissing: "reject" +}) +``` +Only verified agents with trust score ≥ 50 can access tools. Unidentified requests are rejected. + +### Graduated trust +```typescript +// In your tool handler, use the trust data for risk-based decisions +const trust = request.context?.agentTrust; + +if (trust?.trustScore > 80) { + // High trust: allow sensitive operations +} else if (trust?.trustScore > 30) { + // Medium trust: allow read-only operations +} else { + // Low/no trust: sandbox mode +} +``` + +## Testing + +```bash +# Test with a verified agent +curl -H "x-agent-id: brainGrowth" http://localhost:3000/mcp + +# Test without identity (annotation mode passes through) +curl http://localhost:3000/mcp +``` + +## Trust Data Shape + +```typescript +interface AgentTrustResult { + agentId: string; // Agent identifier + trustScore: number; // 0-100 + verified: boolean; // On-chain verification status + name?: string; // Display name + capabilities?: string[]; // Capability tags + lastVerified?: string; // ISO timestamp +} +``` + +## Composing with Other Providers + +SATPProvider works alongside JWT, OAuth, or API key providers. Use it as a secondary check after authentication: + +```typescript +// Your custom composed provider +class ComposedProvider implements AuthProvider { + private jwt = new JWTProvider(jwtConfig); + private satp = new SATPProvider({ minTrustScore: 30 }); + + async authenticate(req: IncomingMessage) { + const jwtResult = await this.jwt.authenticate(req); + if (!jwtResult) return false; + + const satpResult = await this.satp.authenticate(req); + return satpResult; // Trust data in result.data.agentTrust + } +} +``` diff --git a/src/auth/index.ts b/src/auth/index.ts index f0cd8da..25441c2 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -2,6 +2,7 @@ export * from "./types.js"; export * from "./providers/jwt.js"; export * from "./providers/apikey.js"; export * from "./providers/oauth.js"; +export * from "./providers/satp.js"; export type { AuthProvider, AuthConfig, AuthResult } from "./types.js"; export type { JWTConfig } from "./providers/jwt.js"; diff --git a/src/auth/providers/satp.ts b/src/auth/providers/satp.ts new file mode 100644 index 0000000..7b5fdd4 --- /dev/null +++ b/src/auth/providers/satp.ts @@ -0,0 +1,216 @@ +import { IncomingMessage } from "node:http"; +import { AuthProvider, AuthResult, DEFAULT_AUTH_ERROR } from "../types.js"; + +/** + * Trust verification result from SATP/AgentFolio + */ +export interface AgentTrustResult { + /** Agent identifier */ + agentId: string; + /** Trust score (0-100) */ + trustScore: number; + /** Whether the agent is verified on-chain */ + verified: boolean; + /** Agent display name */ + name?: string; + /** Capabilities tags */ + capabilities?: string[]; + /** Last verification timestamp */ + lastVerified?: string; +} + +/** + * Configuration for SATP agent trust verification + */ +export interface SATPConfig { + /** + * AgentFolio API base URL + * @default "https://api.agentfolio.bot" + */ + apiUrl?: string; + + /** + * Minimum trust score required (0-100) + * Set to 0 to allow all agents but still annotate requests with trust data + * @default 0 + */ + minTrustScore?: number; + + /** + * Require on-chain verification + * @default false + */ + requireVerified?: boolean; + + /** + * Header name for agent identity + * @default "x-agent-id" + */ + agentIdHeader?: string; + + /** + * Behavior when agent identity is missing from request + * - "reject": Return 401 + * - "allow": Continue without trust data + * @default "allow" + */ + onMissing?: "reject" | "allow"; + + /** + * Cache TTL in milliseconds for trust score lookups + * @default 300000 (5 minutes) + */ + cacheTtlMs?: number; +} + +/** + * SATP Agent Trust Provider + * + * Verifies agent identity and trust scores via AgentFolio/SATP. + * Can be used standalone or composed with other auth providers. + * + * @example + * ```typescript + * import { MCPServer, SATPProvider } from "mcp-framework"; + * + * const server = new MCPServer({ + * auth: { + * provider: new SATPProvider({ + * minTrustScore: 50, + * requireVerified: true, + * }), + * }, + * }); + * ``` + */ +export class SATPProvider implements AuthProvider { + private config: Required; + private cache: Map = + new Map(); + + constructor(config: SATPConfig = {}) { + this.config = { + apiUrl: config.apiUrl ?? "https://api.agentfolio.bot", + minTrustScore: config.minTrustScore ?? 0, + requireVerified: config.requireVerified ?? false, + agentIdHeader: config.agentIdHeader ?? "x-agent-id", + onMissing: config.onMissing ?? "allow", + cacheTtlMs: config.cacheTtlMs ?? 300_000, + }; + } + + async authenticate(req: IncomingMessage): Promise { + const agentId = this.extractAgentId(req); + + if (!agentId) { + return this.config.onMissing === "allow" ? { data: { agentTrust: null } } : false; + } + + const trust = await this.queryTrust(agentId); + + if (!trust) { + return this.config.onMissing === "allow" ? { data: { agentTrust: null } } : false; + } + + // Check minimum trust score + if (trust.trustScore < this.config.minTrustScore) { + return false; + } + + // Check verification requirement + if (this.config.requireVerified && !trust.verified) { + return false; + } + + return { + data: { + agentTrust: trust, + }, + }; + } + + getAuthError(): { status: number; message: string; headers?: Record } { + return { + status: 403, + message: "Agent trust verification failed", + headers: { + "X-Trust-Required": `min-score=${this.config.minTrustScore}`, + }, + }; + } + + /** + * Extract agent ID from request headers or MCP metadata + */ + private extractAgentId(req: IncomingMessage): string | null { + // Check custom header first + const headerValue = req.headers[this.config.agentIdHeader]; + if (headerValue) { + return Array.isArray(headerValue) ? headerValue[0] : headerValue; + } + + // Check Authorization header for agent token + const auth = req.headers.authorization; + if (auth?.startsWith("Agent ")) { + return auth.slice(6).trim(); + } + + return null; + } + + /** + * Query AgentFolio API for agent trust data with caching + */ + private async queryTrust(agentId: string): Promise { + // Check cache + const cached = this.cache.get(agentId); + if (cached && cached.expiry > Date.now()) { + return cached.result; + } + + try { + const response = await fetch( + `${this.config.apiUrl}/v1/agents/${encodeURIComponent(agentId)}/trust`, + { + method: "GET", + headers: { + Accept: "application/json", + "User-Agent": "mcp-framework-satp/1.0", + }, + signal: AbortSignal.timeout(5000), + } + ); + + if (!response.ok) { + return null; + } + + const data = (await response.json()) as AgentTrustResult; + const result: AgentTrustResult = { + agentId: data.agentId ?? agentId, + trustScore: data.trustScore ?? 0, + verified: data.verified ?? false, + name: data.name, + capabilities: data.capabilities, + lastVerified: data.lastVerified, + }; + + // Cache result + this.cache.set(agentId, { + result, + expiry: Date.now() + this.config.cacheTtlMs, + }); + + return result; + } catch { + return null; + } + } + + /** + * Clear the trust score cache + */ + clearCache(): void { + this.cache.clear(); + } +}