clodex-ide governed agentic IDE
Clodex 把持久任务、Agent Host、MCP、Shell、记忆和策略执行拆成明确边界。它最值得拆的点不是 UI,而是“模型输出不可信,权限来自策略和审计”的工程化实现。
它解决什么,凭什么值得拆
Clodex 的核心问题定义很清楚:AI 编程任务不是一次回答,而是一段可暂停、可恢复、可审计的工程过程。packages/agent-core/src/services/agent-manager/agent-manager.ts:151 把 AgentManager 做成服务边界,packages/agent-core/src/services/agent-manager/agent-manager.ts:163 维护 active agents,packages/agent-core/src/services/agent-manager/agent-manager.ts:227 明确所有状态写入都流过 state-mutations。
这个项目符合源码拆解条件:它与 coding agent 和 AI developer tooling 直接相关,有隔离运行时、MCP runtime、shell execution、runner security、evidence memory 和 cloud-task 恢复链路。apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:16 与源码证据 packages/mcp-runtime/src/config.ts:130 与源码证据 packages/agent-shell/src/engine/shell-service.ts:207 分别代表三条核心执行路径。
▸核心数据
进程边界先于模型能力
AgentManager 的构造依赖说明了它是 core 层枢纽而不是 UI 组件:packages/agent-core/src/services/agent-manager/agent-manager.ts:174 引入命令注册,packages/agent-core/src/services/agent-manager/agent-manager.ts:184 通过 AgentHost 获取 logger/telemetry/models,packages/agent-core/src/services/agent-manager/agent-manager.ts:190 用 host-defined registry 生成具体 Agent。这个边界让 core 不直接 import browser services。
▸系统拓扑
▸AgentManager 的边界声明
151export class AgentManager extends DisposableService { 152 /** Server-side bounds for user-editable agent titles. Slightly more permissive 153 * than the UI's 2–80 range so minor client/server drift never silently drops 154 * a valid title. */ 155 private static readonly TITLE_MIN_LENGTH = 1; 156 private static readonly TITLE_MAX_LENGTH = 200; 157 158 private static readonly NETWORK_RETRY_SCAN_INTERVAL_MS = 5_000; 159 private static readonly NETWORK_RETRY_BACKOFF_MS: [number, ...number[]] = [ 160 5_000, 15_000, 30_000, 60_000, 120_000, 161 ]; 162 163 private activeAgents = new Map< 164 string, 165 BaseAgent<any, any> | BaseAgent<never, any> 166 >(); 167 168 private readonly pendingNetworkRetries = new Map< 169 string, 170 { retryCount: number; nextRetryAt: number; lastErrorMessage: string } 171 >(); 172 private readonly networkRetryInterval: ReturnType<typeof setInterval>; 173 174 private readonly commandRegistry: CommandRegistry; 175 private readonly managerToolbox: AgentManagerToolboxPort; 176 private readonly agentToolbox: BaseAgentToolboxView; 177 private readonly logger: Logger; 178 /** 179 * The agent-core capability seam. Threaded into every `BaseAgent` 180 * instance via `BaseAgentDependencies.host` so the core never imports 181 * browser services directly. Also the sole source of `logger`, 182 * `telemetry`, and `models` consumed by this manager. 183 */ 184 private readonly host: AgentHost;
▸状态写入集中到 mutation 层
状态管理不是随手 setState:packages/agent-core/src/services/agent-manager/agent-manager.ts:227 到 packages/agent-core/src/services/agent-manager/agent-manager.ts:233 把 one-store-update-per-intent 写成注释约束,配合 packages/agent-core/src/services/agent-manager/agent-manager.ts:242 的 startupPolicy 与 packages/agent-core/src/services/agent-manager/agent-manager.ts:253 的 persistenceDb,说明任务恢复和运行中状态被当作一等设计。
Turn Runtime 是模型与工具之间的断路器
isolated turn runtime 的关键不是调用模型,而是控制“模型可以请求什么”。apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:26 从 request.tools 建立 allowedToolNames,apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:75 遍历 toolCalls,apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:76 拒绝未声明工具。这个检查把工具清单从 prompt 建议变成运行时硬边界。
▸模型调用和 step 事件
16export async function executeIsolatedAgentTurn( 17 request: IsolatedAgentTurnRequest, 18 options: IsolatedAgentTurnRuntimeOptions, 19): Promise<IsolatedAgentTurnResult> { 20 throwIfAborted(options.signal); 21 22 const messages: IsolatedAgentConversationMessage[] = structuredClone( 23 request.messages, 24 ); 25 const steps: IsolatedAgentTurnStepResult[] = []; 26 const allowedToolNames = new Set(request.tools.map((tool) => tool.name)); 27 let text = ''; 28 29 for (let step = 1; step <= request.maxSteps; step++) { 30 throwIfAborted(options.signal); 31 options.onEvent?.({ type: 'step-started', step }); 32 33 const modelResult = await options.handlers.callModel( 34 { 35 agentInstanceId: request.agentInstanceId, 36 modelId: request.modelId, 37 traceId: request.traceId, 38 metadata: request.metadata, 39 systemPrompt: request.systemPrompt, 40 messages, 41 tools: request.tools, 42 settings: request.settings, 43 }, 44 { 45 signal: requireSignal(options.signal), 46 onEvent: (event) => { 47 options.onEvent?.({ ...event, step }); 48 }, 49 }, 50 ); 51 throwIfAborted(options.signal); 52 53 text += modelResult.text; 54 messages.push({ 55 role: 'assistant', 56 text: modelResult.text, 57 toolCalls: modelResult.toolCalls, 58 }); 59 60 const stepResult: IsolatedAgentTurnStepResult = { 61 index: step, 62 text: modelResult.text, 63 reasoning: modelResult.reasoning, 64 finishReason: modelResult.finishReason, 65 rawFinishReason: modelResult.rawFinishReason, 66 usage: modelResult.usage, 67 providerMetadata: modelResult.providerMetadata, 68 toolCalls: modelResult.toolCalls, 69 toolResults: [], 70 toolErrors: [], 71 approvalRequests: [], 72 }; 73 steps.push(stepResult);
▸工具审批、错误和结果回填
75 for (const call of modelResult.toolCalls) { 76 if (!allowedToolNames.has(call.toolName)) { 77 throw new Error( 78 `Model requested undeclared isolated tool "${call.toolName}"`, 79 ); 80 } 81 82 options.onEvent?.({ type: 'tool-call', step, call }); 83 const result = await options.handlers.callTool( 84 { 85 agentInstanceId: request.agentInstanceId, 86 call, 87 messages, 88 }, 89 { 90 signal: requireSignal(options.signal), 91 }, 92 ); 93 throwIfAborted(options.signal); 94 95 if (result.status === 'approval-required') { 96 const approvalRequest = { 97 approvalId: result.approvalId, 98 toolCallId: call.toolCallId, 99 toolName: call.toolName, 100 }; 101 stepResult.approvalRequests.push(approvalRequest); 102 options.onEvent?.({ 103 type: 'tool-approval-request', 104 step, 105 ...approvalRequest, 106 }); 107 continue; 108 } 109 if (result.status === 'error') { 110 const toolError = { 111 toolCallId: call.toolCallId, 112 toolName: call.toolName, 113 message: result.message, 114 }; 115 stepResult.toolErrors.push(toolError); 116 messages.push({ 117 role: 'tool', 118 toolCallId: call.toolCallId, 119 toolName: call.toolName, 120 output: { 121 error: result.message, 122 }, 123 }); 124 options.onEvent?.({ 125 type: 'tool-error', 126 step, 127 ...toolError, 128 }); 129 continue; 130 } 131 132 const toolResult = { 133 toolCallId: call.toolCallId, 134 toolName: call.toolName, 135 output: result.output, 136 }; 137 stepResult.toolResults.push(toolResult); 138 messages.push({ 139 role: 'tool', 140 ...toolResult, 141 }); 142 options.onEvent?.({ 143 type: 'tool-result', 144 step, 145 ...toolResult, 146 }); 147 }
▸停止条件
停止条件也体现出 governed execution:当 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:156 检测没有工具调用,或 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:158 出现 approvalRequests,runtime 会返回 completed,而不是继续替用户自动推进敏感动作。
MCP 配置把 secret 和 egress 写进 schema
MCP runtime 最有价值的代码在 config schema,而不只是 protocol 类型。packages/mcp-runtime/src/config.ts:12 到 packages/mcp-runtime/src/config.ts:22 列出敏感 key 片段,packages/mcp-runtime/src/config.ts:68 对 stdio args 做 superRefine,packages/mcp-runtime/src/config.ts:95 拦截 Bearer token。这些检查发生在配置层,早于 server 启动。
▸stdio 与 secret 拦截
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 });
▸remote URL 必须可治理
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 });
▸MCP 策略不是装饰项
远程 MCP 的约束延伸到 URL、headers、OAuth scopes 和 executable runtime policy。packages/mcp-runtime/src/config.ts:210 开始定义 OAuth 配置,packages/mcp-runtime/src/config.ts:266 附近出现 executableRuntimePolicySchema,packages/mcp-runtime/src/config.ts:377 把 tool policy decision 收束为 allow/ask/deny。
Shell 执行是持久会话,不是一次性 subprocess
ShellService 的实现说明 Clodex 把终端当成 Agent 的长期环境:packages/agent-shell/src/engine/shell-service.ts:22 保存 detected shell,packages/agent-shell/src/engine/shell-service.ts:23 保存 SessionManager,packages/agent-shell/src/engine/shell-service.ts:47 和 packages/agent-shell/src/engine/shell-service.ts:57 限制流式 UI 更新负载。
▸初始化与会话管理
17export class ShellService extends DisposableService { 18 private readonly logger: Logger; 19 private readonly sink?: ShellStreamSink; 20 private readonly getShellLogsDir: (agentInstanceId: string) => string; 21 22 private shell: DetectedShell | null = null; 23 private sessionManager: SessionManager | null = null; 24 private resolvedEnv: Record<string, string> | null = null; 25 26 private readonly preDetectedShell: DetectedShell | null; 27 private readonly preResolvedEnv: Record<string, string> | null; 28 private readonly protectedFiles: ProtectedFileStorage | undefined; 29 30 /** 31 * Per-tool-call streaming bookkeeping. Each entry records which session 32 * the tool call is streaming from and whether new output has arrived 33 * since the last flush. The UI snapshot is computed on-demand by 34 * reading the session's current grid state — we do not accumulate raw 35 * chunks here. 36 */ 37 private readonly outputBuffers = new Map< 38 string, 39 { sessionId: string; dirty: boolean } 40 >(); 41 private readonly outputFlushTimers = new Map<string, NodeJS.Timeout>(); 42 private readonly outputMaxIntervalTimers = new Map<string, NodeJS.Timeout>(); 43 44 /** Per-agent throttle timers for pushing the shells manifest to the sink. */ 45 private readonly shellManifestTimers = new Map<string, NodeJS.Timeout>(); 46 47 private static readonly FLUSH_DEBOUNCE_MS = 100; 48 private static readonly FLUSH_MAX_INTERVAL_MS = 300; 49 /** 50 * Per-flush cap for the live UI snapshot. Grid serialization is already 51 * bounded by `STREAMING_MAX_ROWS` (~24KB theoretical max at 120 cols), 52 * but 16KB is a snug safety rail that keeps live-stream IPC payloads 53 * small without truncating realistic live previews. The final command 54 * output is unaffected — it uses the head+tail capped capture on a 55 * separate code path. 56 */ 57 private static readonly MAX_STREAMING_BYTES = 16_384; 58 private static readonly MANIFEST_THROTTLE_MS = 500;
▸创建 PTY session 并推送 session id
153 /** 154 * Create a new PTY session without executing any command. 155 * Returns immediately with the session ID — does not wait for shell init. 156 */ 157 createSession( 158 agentInstanceId: string, 159 toolCallId: string, 160 cwd: string, 161 ): string { 162 this.assertNotDisposed(); 163 164 if (!this.shell || !this.sessionManager) { 165 throw new Error('Shell service is not available — no shell detected.'); 166 } 167 168 const env = sanitizeEnv(this.resolvedEnv, this.shell.type); 169 const sessionId = this.sessionManager.createSession( 170 agentInstanceId, 171 cwd, 172 env, 173 () => {}, 174 ); 175 176 // Wire streaming callback 177 this.sessionManager.setOnData(sessionId, (_sid: string, _chunk: string) => { 178 if (!this.sink) return; 179 const entry = this.outputBuffers.get(toolCallId); 180 if (entry) { 181 entry.dirty = true; 182 } else { 183 this.outputBuffers.set(toolCallId, { 184 sessionId, 185 dirty: true, 186 }); 187 } 188 this.scheduleFlush(agentInstanceId, toolCallId); 189 this.scheduleShellManifestPush(agentInstanceId); 190 }); 191 192 this.pushShellsToSink(agentInstanceId); 193 194 // Publish session ID to the stream sink so the UI can cancel in-flight 195 // commands. 196 this.sink?.publishSessionId(agentInstanceId, toolCallId, sessionId); 197 198 return sessionId; 199 }
▸执行命令复用 session
207 async executeInSession( 208 agentInstanceId: string, 209 toolCallId: string, 210 request: SessionCommandRequest, 211 ): Promise<SessionCommandResult> { 212 this.assertNotDisposed(); 213 214 if (!this.shell || !this.sessionManager) { 215 return { 216 sessionId: '', 217 output: 'Shell service is not available — no shell detected.', 218 exitCode: null, 219 sessionExited: true, 220 timedOut: false, 221 resolvedBy: 'session_exited', 222 }; 223 } 224 225 const env = sanitizeEnv(this.resolvedEnv, this.shell.type); 226 227 // Build a streaming callback targeting the current toolCallId. 228 // We no longer accumulate chunks — we just mark the tool call "dirty" 229 // and let the flush read the current grid snapshot from the session. 230 const makeOnData = (targetToolCallId: string, targetSessionId: string) => { 231 return (_sid: string, _chunk: string) => { 232 if (!this.sink) return; 233 const entry = this.outputBuffers.get(targetToolCallId); 234 if (entry) { 235 entry.dirty = true; 236 } else { 237 this.outputBuffers.set(targetToolCallId, { 238 sessionId: targetSessionId, 239 dirty: true, 240 }); 241 } 242 this.scheduleFlush(agentInstanceId, targetToolCallId); 243 this.scheduleShellManifestPush(agentInstanceId); 244 }; 245 }; 246 247 // Reuse existing session or create a new one 248 let sessionId = request.sessionId; 249 if (!sessionId) { 250 try { 251 // Create with a no-op onData; wire the real callback immediately 252 // after so we can close over the sessionId for snapshot reads. 253 sessionId = this.sessionManager.createSession( 254 agentInstanceId, 255 request.cwd ?? '', 256 env, 257 () => {}, 258 ); 259 this.sessionManager.setOnData( 260 sessionId, 261 makeOnData(toolCallId, sessionId), 262 ); 263 } catch (err) { 264 return { 265 sessionId: '', 266 output: 267 err instanceof Error 268 ? err.message 269 : 'Failed to create shell session.', 270 exitCode: null, 271 sessionExited: true, 272 timedOut: false, 273 resolvedBy: 'session_exited', 274 }; 275 } 276 } else { 277 // Re-target streaming output to the current tool call 278 this.sessionManager.setOnData( 279 sessionId, 280 makeOnData(toolCallId, sessionId), 281 ); 282 } 283 284 // Push updated session manifest on new session creation 285 if (!request.sessionId) { 286 this.pushShellsToSink(agentInstanceId); 287 } 288 289 // Publish session ID to the stream sink so the UI can cancel in-flight 290 // commands. 291 this.sink?.publishSessionId(agentInstanceId, toolCallId, sessionId); 292 293 return this.sessionManager.executeCommand(sessionId, request);
状态、记忆与恢复是同一条链路
源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:151 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:163 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:168 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:174 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:184 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:190 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:212 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:218 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:227 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:242 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:253 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:257 与源码证据 packages/agent-core/src/services/agent-manager/agent-manager.ts:260 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:16 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:22 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:26 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:29 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:33 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:45 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:51 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:60 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:75 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:76 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:83 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:95 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:109 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:132 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:156 与源码证据 apps/browser/src/backend/agent-host/isolated-agent-turn-runtime.ts:169 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 packages/mcp-runtime/src/config.ts:4 与源码证据 packages/mcp-runtime/src/config.ts:5 与源码证据 packages/mcp-runtime/src/config.ts:9 与源码证据 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 显示,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/agent-shell/src/engine/shell-service.ts:17 与源码证据 packages/agent-shell/src/engine/shell-service.ts:22 与源码证据 packages/agent-shell/src/engine/shell-service.ts:30 与源码证据 packages/agent-shell/src/engine/shell-service.ts:47 与源码证据 packages/agent-shell/src/engine/shell-service.ts:57 与源码证据 packages/agent-shell/src/engine/shell-service.ts:77 与源码证据 packages/agent-shell/src/engine/shell-service.ts:97 与源码证据 packages/agent-shell/src/engine/shell-service.ts:128 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 packages/agent-shell/src/engine/shell-service.ts:153 与源码证据 packages/agent-shell/src/engine/shell-service.ts:168 与源码证据 packages/agent-shell/src/engine/shell-service.ts:176 与源码证据 packages/agent-shell/src/engine/shell-service.ts:194 与源码证据 packages/agent-shell/src/engine/shell-service.ts:207 与源码证据 packages/agent-shell/src/engine/shell-service.ts:225 与源码证据 packages/agent-shell/src/engine/shell-service.ts:247 与源码证据 packages/agent-shell/src/engine/shell-service.ts:293 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
源码证据 packages/agent-shell/src/engine/disposable-local-worktree-runner-adapter.ts:136 与源码证据 packages/agent-shell/src/engine/disposable-local-worktree-runner-adapter.ts:154 与源码证据 packages/agent-shell/src/engine/disposable-local-worktree-runner-adapter.ts:265 与源码证据 packages/agent-shell/src/engine/disposable-local-worktree-runner-adapter.ts:276 显示,Clodex 不是把 AI 聊天窗口塞进 IDE,而是把任务状态、工具权限、运行时隔离和可审计执行拆成独立层。这里每个引用都对应真实实现:管理器负责状态与生命周期,isolated runtime 负责模型/工具循环,MCP 配置负责边界校验,shell 层负责可取消、可回放的命令执行。
▸为什么这比普通 IDE 插件复杂
普通 IDE 插件只要响应当前命令即可;Clodex 需要解释“这一步为什么发生”。所以 packages/agent-core/src/services/agent-manager/agent-manager.ts:140 定义 session checkpoint 状态,packages/agent-core/src/services/agent-manager/agent-manager.ts:212 接入网络在线判断,packages/agent-shell/src/engine/shell-service.ts:303 提供 recent output 给分类器判断下一条命令风险。
技术亮点之外,别忽略这些
第一,项目很新,release status 仍是 technical preview;源码里有大量 promotion-gated cloud-task 与 runner 代码,读者应把它当作架构样本而不是稳定产品。第二,monorepo 规模很大,packages/mcp-runtime/src/config.ts:414 限制 registry servers 数量,但真实部署仍要验证 UI、存储和 runner 的组合风险。