|
| 1 | +import { IncomingMessage } from "node:http"; |
| 2 | +import { AuthProvider, AuthResult } from "../types.js"; |
| 3 | + |
| 4 | +/** |
| 5 | + * Trust verification result from AgentPass |
| 6 | + */ |
| 7 | +export interface AgentPassTrustResult { |
| 8 | + /** Agent handle (e.g. payment-bot.cybersecai.agentpass) */ |
| 9 | + agent: string; |
| 10 | + /** Trust level (L0-L4) */ |
| 11 | + trustLevel: string; |
| 12 | + /** Numeric trust score (0-100) */ |
| 13 | + trustScore: number; |
| 14 | + /** Whether the agent has been identity-verified via ECDSA challenge-response */ |
| 15 | + identityVerified: boolean; |
| 16 | + /** AML sanctions screening status */ |
| 17 | + sanctionsStatus: string; |
| 18 | + /** Number of completed transactions */ |
| 19 | + transactionCount: number; |
| 20 | + /** Agent registration timestamp */ |
| 21 | + registeredAt?: string; |
| 22 | +} |
| 23 | + |
| 24 | +/** |
| 25 | + * Configuration for AgentPass trust verification |
| 26 | + */ |
| 27 | +export interface AgentPassConfig { |
| 28 | + /** |
| 29 | + * AgentPass API base URL |
| 30 | + * @default "https://agentpass.co.uk" |
| 31 | + */ |
| 32 | + apiUrl?: string; |
| 33 | + |
| 34 | + /** |
| 35 | + * Minimum trust score required (0-100) |
| 36 | + * Set to 0 to allow all agents but still annotate requests with trust data |
| 37 | + * @default 0 |
| 38 | + */ |
| 39 | + minTrustScore?: number; |
| 40 | + |
| 41 | + /** |
| 42 | + * Minimum trust level required (L0-L4) |
| 43 | + * L0: Untrusted, L1: Basic, L2: Verified, L3: Trusted, L4: Certified |
| 44 | + * @default "L0" |
| 45 | + */ |
| 46 | + minTrustLevel?: "L0" | "L1" | "L2" | "L3" | "L4"; |
| 47 | + |
| 48 | + /** |
| 49 | + * Require clean AML sanctions screening |
| 50 | + * When true, agents with sanctions violations are rejected |
| 51 | + * @default false |
| 52 | + */ |
| 53 | + requireCleanSanctions?: boolean; |
| 54 | + |
| 55 | + /** |
| 56 | + * Header name for agent identity |
| 57 | + * @default "x-agent-id" |
| 58 | + */ |
| 59 | + agentIdHeader?: string; |
| 60 | + |
| 61 | + /** |
| 62 | + * Behavior when agent identity is missing from request |
| 63 | + * - "reject": Return 401 |
| 64 | + * - "allow": Continue without trust data |
| 65 | + * @default "allow" |
| 66 | + */ |
| 67 | + onMissing?: "reject" | "allow"; |
| 68 | + |
| 69 | + /** |
| 70 | + * Cache TTL in milliseconds for trust score lookups |
| 71 | + * @default 300000 (5 minutes) |
| 72 | + */ |
| 73 | + cacheTtlMs?: number; |
| 74 | +} |
| 75 | + |
| 76 | +const TRUST_LEVEL_ORDER = ["L0", "L1", "L2", "L3", "L4"]; |
| 77 | + |
| 78 | +/** |
| 79 | + * AgentPass Trust Provider |
| 80 | + * |
| 81 | + * Verifies agent identity and trust scores via AgentPass -- the pre-payment |
| 82 | + * trust gateway for AI agents. Checks identity verification, behavioural |
| 83 | + * trust scoring (L0-L4), and AML sanctions status before allowing requests. |
| 84 | + * |
| 85 | + * AgentPass screens agents through cryptographic identity (ECDSA P-256), |
| 86 | + * behavioural trust scoring, and 75,784-entry AML sanctions databases |
| 87 | + * (UK HMT + OFAC SDN). Trust scores are publicly queryable. |
| 88 | + * |
| 89 | + * @see https://agentpass.co.uk |
| 90 | + * @see https://datatracker.ietf.org/doc/draft-sharif-agent-payment-trust/ |
| 91 | + * |
| 92 | + * @example |
| 93 | + * ```typescript |
| 94 | + * import { MCPServer, AgentPassProvider } from "mcp-framework"; |
| 95 | + * |
| 96 | + * const server = new MCPServer({ |
| 97 | + * auth: { |
| 98 | + * provider: new AgentPassProvider({ |
| 99 | + * minTrustScore: 50, |
| 100 | + * minTrustLevel: "L2", |
| 101 | + * requireCleanSanctions: true, |
| 102 | + * }), |
| 103 | + * }, |
| 104 | + * }); |
| 105 | + * ``` |
| 106 | + */ |
| 107 | +export class AgentPassProvider implements AuthProvider { |
| 108 | + private config: Required<AgentPassConfig>; |
| 109 | + private cache: Map<string, { result: AgentPassTrustResult; expiry: number }> = |
| 110 | + new Map(); |
| 111 | + |
| 112 | + constructor(config: AgentPassConfig = {}) { |
| 113 | + this.config = { |
| 114 | + apiUrl: config.apiUrl ?? "https://agentpass.co.uk", |
| 115 | + minTrustScore: config.minTrustScore ?? 0, |
| 116 | + minTrustLevel: config.minTrustLevel ?? "L0", |
| 117 | + requireCleanSanctions: config.requireCleanSanctions ?? false, |
| 118 | + agentIdHeader: config.agentIdHeader ?? "x-agent-id", |
| 119 | + onMissing: config.onMissing ?? "allow", |
| 120 | + cacheTtlMs: config.cacheTtlMs ?? 300_000, |
| 121 | + }; |
| 122 | + } |
| 123 | + |
| 124 | + async authenticate(req: IncomingMessage): Promise<boolean | AuthResult> { |
| 125 | + const agentId = this.extractAgentId(req); |
| 126 | + |
| 127 | + if (!agentId) { |
| 128 | + return this.config.onMissing === "allow" |
| 129 | + ? { data: { agentTrust: null } } |
| 130 | + : false; |
| 131 | + } |
| 132 | + |
| 133 | + const trust = await this.queryTrust(agentId); |
| 134 | + |
| 135 | + if (!trust) { |
| 136 | + return this.config.onMissing === "allow" |
| 137 | + ? { data: { agentTrust: null } } |
| 138 | + : false; |
| 139 | + } |
| 140 | + |
| 141 | + // Check minimum trust score |
| 142 | + if (trust.trustScore < this.config.minTrustScore) { |
| 143 | + return false; |
| 144 | + } |
| 145 | + |
| 146 | + // Check minimum trust level |
| 147 | + const agentLevelIndex = TRUST_LEVEL_ORDER.indexOf(trust.trustLevel); |
| 148 | + const requiredLevelIndex = TRUST_LEVEL_ORDER.indexOf( |
| 149 | + this.config.minTrustLevel |
| 150 | + ); |
| 151 | + if (agentLevelIndex < requiredLevelIndex) { |
| 152 | + return false; |
| 153 | + } |
| 154 | + |
| 155 | + // Check sanctions status |
| 156 | + if ( |
| 157 | + this.config.requireCleanSanctions && |
| 158 | + trust.sanctionsStatus !== "CLEAR" |
| 159 | + ) { |
| 160 | + return false; |
| 161 | + } |
| 162 | + |
| 163 | + return { |
| 164 | + data: { |
| 165 | + agentTrust: trust, |
| 166 | + }, |
| 167 | + }; |
| 168 | + } |
| 169 | + |
| 170 | + getAuthError(): { |
| 171 | + status: number; |
| 172 | + message: string; |
| 173 | + headers?: Record<string, string>; |
| 174 | + } { |
| 175 | + return { |
| 176 | + status: 403, |
| 177 | + message: "Agent trust verification failed", |
| 178 | + headers: { |
| 179 | + "X-Trust-Required": `min-score=${this.config.minTrustScore},min-level=${this.config.minTrustLevel}`, |
| 180 | + "X-Trust-Info": "https://agentpass.co.uk", |
| 181 | + }, |
| 182 | + }; |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * Extract agent ID from request headers |
| 187 | + */ |
| 188 | + private extractAgentId(req: IncomingMessage): string | null { |
| 189 | + // Check custom header first |
| 190 | + const headerValue = req.headers[this.config.agentIdHeader]; |
| 191 | + if (headerValue) { |
| 192 | + return Array.isArray(headerValue) ? headerValue[0] : headerValue; |
| 193 | + } |
| 194 | + |
| 195 | + // Check Authorization header for agent token |
| 196 | + const auth = req.headers.authorization; |
| 197 | + if (auth?.startsWith("Agent ")) { |
| 198 | + return auth.slice(6).trim(); |
| 199 | + } |
| 200 | + |
| 201 | + // Check AgentPass passport ID header |
| 202 | + const passportId = req.headers["x-agent-passport-id"]; |
| 203 | + if (passportId) { |
| 204 | + return Array.isArray(passportId) ? passportId[0] : passportId; |
| 205 | + } |
| 206 | + |
| 207 | + return null; |
| 208 | + } |
| 209 | + |
| 210 | + /** |
| 211 | + * Query AgentPass public trust API with caching |
| 212 | + */ |
| 213 | + private async queryTrust( |
| 214 | + agentId: string |
| 215 | + ): Promise<AgentPassTrustResult | null> { |
| 216 | + // Check cache |
| 217 | + const cached = this.cache.get(agentId); |
| 218 | + if (cached && cached.expiry > Date.now()) { |
| 219 | + return cached.result; |
| 220 | + } |
| 221 | + |
| 222 | + try { |
| 223 | + const response = await fetch( |
| 224 | + `${this.config.apiUrl}/api/trust/${encodeURIComponent(agentId)}`, |
| 225 | + { |
| 226 | + method: "GET", |
| 227 | + headers: { |
| 228 | + Accept: "application/json", |
| 229 | + "User-Agent": "mcp-framework-agentpass/1.0", |
| 230 | + }, |
| 231 | + signal: AbortSignal.timeout(5000), |
| 232 | + } |
| 233 | + ); |
| 234 | + |
| 235 | + if (!response.ok) { |
| 236 | + return null; |
| 237 | + } |
| 238 | + |
| 239 | + const data = (await response.json()) as Record<string, unknown>; |
| 240 | + const result: AgentPassTrustResult = { |
| 241 | + agent: (data.agent as string) ?? agentId, |
| 242 | + trustLevel: (data.trustLevel as string) ?? "L0", |
| 243 | + trustScore: (data.trustScore as number) ?? 0, |
| 244 | + identityVerified: (data.identityVerified as boolean) ?? false, |
| 245 | + sanctionsStatus: (data.sanctionsStatus as string) ?? "UNKNOWN", |
| 246 | + transactionCount: (data.transactionCount as number) ?? 0, |
| 247 | + registeredAt: data.registeredAt as string | undefined, |
| 248 | + }; |
| 249 | + |
| 250 | + // Cache result |
| 251 | + this.cache.set(agentId, { |
| 252 | + result, |
| 253 | + expiry: Date.now() + this.config.cacheTtlMs, |
| 254 | + }); |
| 255 | + |
| 256 | + return result; |
| 257 | + } catch { |
| 258 | + return null; |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Clear the trust score cache |
| 264 | + */ |
| 265 | + clearCache(): void { |
| 266 | + this.cache.clear(); |
| 267 | + } |
| 268 | +} |
0 commit comments