MCP Policy 深度解析
本页只拆一个模块面,用真实源码行号解释数据流、边界和失败模式。
配置 schema 是第一道防线
▸schema 收紧输入形状
MCP 子系统的契约从 schema 开始:packages/mcp-runtime/src/config.ts:4 与源码证据 packages/mcp-runtime/src/config.ts:5 与源码证据 packages/mcp-runtime/src/config.ts:6 与源码证据 packages/mcp-runtime/src/config.ts:7 定义全局上限,packages/mcp-runtime/src/config.ts:9 与源码证据 packages/mcp-runtime/src/config.ts:24 与源码证据 packages/mcp-runtime/src/config.ts:31 约束 id 和 credential reference,避免任意字符串直接进入 runtime。
1import path from 'node:path'; 2import { z } from 'zod'; 3 4export const MCP_REGISTRY_SCHEMA_VERSION = 1; 5export const MCP_MAX_SERVERS = 100; 6export const MCP_MAX_CONFIG_ENTRIES = 128; 7export const MCP_MAX_OAUTH_SCOPES = 64; 8 9const SAFE_ID_PATTERN = /^[a-z0-9]+(?:[-_.][a-z0-9]+)*$/; 10const SAFE_FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/; 11const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; 12const SENSITIVE_KEY_PARTS = [ 13 'AUTH', 14 'COOKIE', 15 'CREDENTIAL', 16 'KEY', 17 'PASSWORD', 18 'PRIVATE', 19 'SECRET', 20 'SESSION', 21 'TOKEN', 22] as const; 23 24export const mcpServerIdSchema = z 25 .string() 26 .trim() 27 .min(1) 28 .max(80) 29 .regex(SAFE_ID_PATTERN); 30 31export const mcpCredentialReferenceSchema = z 32 .object({ 33 kind: z.literal('credential'), 34 credentialId: z.string().trim().min(1).max(120).regex(SAFE_ID_PATTERN), 35 field: z.string().trim().min(1).max(120).regex(SAFE_FIELD_PATTERN), 36 }) 37 .strict(); 38 39export const mcpLiteralValueSchema = z 40 .object({ 41 kind: z.literal('literal'), 42 value: z 43 .string() 44 .max(16_384) 45 .refine((value) => !value.includes('\0'), 'Value may not contain NUL'), 46 }) 47 .strict(); 48 49export const mcpConfigValueSchema = z.discriminatedUnion('kind', [ 50 mcpLiteralValueSchema, 51 mcpCredentialReferenceSchema, 52]); 53 54export type McpCredentialReference = z.infer< 55 typeof mcpCredentialReferenceSchema 56>; 57export type McpConfigValue = z.infer<typeof mcpConfigValueSchema>; 58
stdio 配置是本地 MCP 最危险的入口之一,因此 packages/mcp-runtime/src/config.ts:59 与源码证据 packages/mcp-runtime/src/config.ts:68 与源码证据 packages/mcp-runtime/src/config.ts:71 与源码证据 packages/mcp-runtime/src/config.ts:79 与源码证据 packages/mcp-runtime/src/config.ts:95 与源码证据 packages/mcp-runtime/src/config.ts:100 对 args 里的 secret、password、Bearer token 做拦截,并把敏感配置推向 credential reference。
59const stdioArgsSchema = z 60 .array( 61 z 62 .string() 63 .max(16_384) 64 .refine((value) => !value.includes('\0'), 'Argument may not contain NUL'), 65 ) 66 .max(128) 67 .default([]) 68 .superRefine((args, context) => { 69 for (const [index, argument] of args.entries()) { 70 if ( 71 /^--?(?:api[-_]?key|auth|authorization|cookie|password|secret|token)=/i.test( 72 argument, 73 ) 74 ) { 75 context.addIssue({ 76 code: z.ZodIssueCode.custom, 77 path: [index], 78 message: 79 'Secrets may not be embedded in MCP command arguments; use an environment credential reference', 80 }); 81 } 82 if ( 83 /^--?(?:api[-_]?key|auth|authorization|cookie|password|secret|token)$/i.test( 84 argument, 85 ) && 86 index + 1 < args.length 87 ) { 88 context.addIssue({ 89 code: z.ZodIssueCode.custom, 90 path: [index + 1], 91 message: 92 'Secrets may not be embedded in MCP command arguments; use an environment credential reference', 93 }); 94 } 95 if (/\bBearer\s+\S+/i.test(argument)) { 96 context.addIssue({ 97 code: z.ZodIssueCode.custom, 98 path: [index], 99 message: 100 'Bearer credentials may not be embedded in MCP command arguments', 101 }); 102 } 103 } 104 }); 105 106const configValuesSchema = z 107 .record(z.string().min(1).max(256), mcpConfigValueSchema) 108 .superRefine((values, context) => { 109 if (Object.keys(values).length > MCP_MAX_CONFIG_ENTRIES) { 110 context.addIssue({ 111 code: z.ZodIssueCode.custom, 112 message: `Configuration may contain at most ${MCP_MAX_CONFIG_ENTRIES} entries`, 113 }); 114 } 115 for (const [key, value] of Object.entries(values)) { 116 if ( 117 value.kind === 'literal' && 118 (isSensitiveMcpConfigKey(key) || looksLikeMcpSecretValue(value.value)) 119 ) { 120 context.addIssue({ 121 code: z.ZodIssueCode.custom, 122 path: [key], 123 message: 124 'Sensitive configuration values must use a credential reference', 125 }); 126 } 127 } 128 });
远程 MCP 走 URL,因此 packages/mcp-runtime/src/config.ts:154 与源码证据 packages/mcp-runtime/src/config.ts:165 与源码证据 packages/mcp-runtime/src/config.ts:166 与源码证据 packages/mcp-runtime/src/config.ts:171 与源码证据 packages/mcp-runtime/src/config.ts:174 与源码证据 packages/mcp-runtime/src/config.ts:177 与源码证据 packages/mcp-runtime/src/config.ts:186 与源码证据 packages/mcp-runtime/src/config.ts:190 强制非 loopback 使用 HTTPS、禁止 URL credentials、禁止敏感 query 参数持久化。
154const remoteUrlSchema = z 155 .string() 156 .url() 157 .max(4_096) 158 .superRefine((value, context) => { 159 const url = new URL(value); 160 const isLoopback = 161 url.hostname === 'localhost' || 162 url.hostname === '127.0.0.1' || 163 url.hostname === '[::1]' || 164 url.hostname === '::1'; 165 if ( 166 url.protocol !== 'https:' && 167 !(url.protocol === 'http:' && isLoopback) 168 ) { 169 context.addIssue({ 170 code: z.ZodIssueCode.custom, 171 message: 'Remote MCP URLs must use HTTPS unless they target loopback', 172 }); 173 } 174 if (url.username || url.password) { 175 context.addIssue({ 176 code: z.ZodIssueCode.custom, 177 message: 'Credentials may not be embedded in an MCP URL', 178 }); 179 } 180 if (url.hash) { 181 context.addIssue({ 182 code: z.ZodIssueCode.custom, 183 message: 'MCP URLs may not contain fragments', 184 }); 185 } 186 for (const key of url.searchParams.keys()) { 187 if (isSensitiveMcpConfigKey(key)) { 188 context.addIssue({ 189 code: z.ZodIssueCode.custom, 190 message: `Sensitive URL parameter "${key}" must not be persisted`, 191 }); 192 } 193 } 194 });
源码证据 packages/mcp-runtime/src/config.ts:12 与源码证据 packages/mcp-runtime/src/config.ts:24 与源码证据 packages/mcp-runtime/src/config.ts:31 与源码证据 packages/mcp-runtime/src/config.ts:49 与源码证据 packages/mcp-runtime/src/config.ts:59 与源码证据 packages/mcp-runtime/src/config.ts:68 与源码证据 packages/mcp-runtime/src/config.ts:95 与源码证据 packages/mcp-runtime/src/config.ts:106 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 packages/mcp-runtime/src/config.ts:130 与源码证据 packages/mcp-runtime/src/config.ts:154 与源码证据 packages/mcp-runtime/src/config.ts:165 与源码证据 packages/mcp-runtime/src/config.ts:174 与源码证据 packages/mcp-runtime/src/config.ts:186 与源码证据 packages/mcp-runtime/src/config.ts:210 与源码证据 packages/mcp-runtime/src/config.ts:266 与源码证据 packages/mcp-runtime/src/config.ts:377 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 packages/mcp-runtime/src/policy.ts:7 与源码证据 packages/mcp-runtime/src/policy.ts:27 与源码证据 packages/mcp-runtime/src/policy.ts:36 与源码证据 packages/mcp-runtime/src/policy.ts:44 与源码证据 packages/mcp-runtime/src/policy.ts:52 与源码证据 packages/mcp-runtime/src/policy.ts:60 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
从配置到决策
▸policy 把工具信号归一成决策
策略层把默认决策收束到 allow/ask/deny。packages/mcp-runtime/src/config.ts:377 与源码证据 packages/mcp-runtime/src/config.ts:380 与源码证据 packages/mcp-runtime/src/config.ts:398 与源码证据 packages/mcp-runtime/src/config.ts:409 与源码证据 packages/mcp-runtime/src/config.ts:414 与源码证据 packages/mcp-runtime/src/config.ts:434 描述 registry 和 policy schema,packages/mcp-runtime/src/policy.ts:27 与源码证据 packages/mcp-runtime/src/policy.ts:36 与源码证据 packages/mcp-runtime/src/policy.ts:38 与源码证据 packages/mcp-runtime/src/policy.ts:44 与源码证据 packages/mcp-runtime/src/policy.ts:52 与源码证据 packages/mcp-runtime/src/policy.ts:60 则负责基于 tool signals 评估实际决策。
边界与失败
▸失败先进入配置错误
失败模式包括 server 数量过多、配置项过多、OAuth header 冲突和明文 secret。packages/mcp-runtime/src/config.ts:109 与源码证据 packages/mcp-runtime/src/config.ts:112 与源码证据 packages/mcp-runtime/src/config.ts:414 与源码证据 packages/mcp-runtime/src/config.ts:418 与源码证据 packages/mcp-runtime/src/config.ts:481 与源码证据 packages/mcp-runtime/src/config.ts:504 把这些情况变成 schema error。