Claude Code Agent System Prompt Skills TypeScript Runtime Subagent 权限控制 沙箱 逆向工程 工程实践 AI Agents Evaluation LLM Anthropic OpenIM Go 后端 死锁 Socket K8s 一致性Hash

Claude Code 源码解析(二):Skills 如何进入 System Prompt

本文是 Claude Code 源码逆向系列 的第二篇,聚焦 Skills 发现与 System Prompt 注入机制。 我最先关心的问题是:AGENTS.md 里的规则到底怎么进入模型上下文? 恢复后,这条链路大致是: src/core/skills/agentsFile.ts:从工作目录向上查找并读取 AGENTS.md src/core/skills/prompt.ts:解析可用 skill,并构造可注入的 prompt 片段 src/core/model/request.ts:把 skills prompt 追加到 system 消息块 src/core/tools/skill.ts:提供内置 Skill 工具,支持运行时查询/加载 一个典型的 TS 片段(示意,保留结构)是这样的: 1 2 3 4 5 6 7 // src/core/model/request.ts if (params.skills && params.skills.trim()) { systemBlocks.push({ type: "text", text: params.skills, }); } 对应伪代码: 1 2 3 4 skillsPrompt = discoverSkillsFromAgentsFile(cwd) if skillsPrompt exists: append skillsPrompt into system messages send request to model 这块我有个明确取舍:先把 Skills 恢复成独立模块,不急着耦合进 runCli 主流程。原因很简单,Skills 的输入输出边界很清晰,独立后更容易做逐步校验,也更适合后续替换解析策略。 ...

2026年2月19日 · 3 分钟 · 522 字 · Simon Sun

Claude Code 源码解析(三):Subagent / Agent Runtime 的执行闭环

本文是 Claude Code 源码逆向系列 的第三篇,聚焦 Agent Runtime 的核心执行循环与子代理协作机制。 第二块是我认为最有"框架味"的部分:Agent 不是单次调用,而是一个带状态的循环执行体。 恢复后的模块拆分如下: src/core/agent/runtime.ts:核心循环,负责模型调用、tool_use 执行、结果回填 src/core/agent/types.ts:运行时消息、事件、配置类型 src/core/agent/mailbox.ts:队友/子代理消息邮箱(内存实现) src/core/agent/manager.ts:管理多个 in-process teammate src/core/agent/protocol.ts:控制消息协议(如 shutdown) src/core/agent/inProcessRunner.ts:轮询邮箱并驱动 runtime src/core/agent/run.ts:对外暴露的便捷入口,创建 runtime 并执行 src/core/agent/options.ts:解析 teammate 选项 一、核心循环:AgentRuntime.submitMessage 整个 Agent Runtime 的灵魂是 AgentRuntime 类的 submitMessage 方法。它是一个 AsyncGenerator——不是简单的 async 函数,而是调用者可以按需消费每一步事件的异步迭代器。 核心循环可概括为: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // src/core/agent/runtime.ts (简化示意) async *submitMessage(input: string): AsyncGenerator<AgentRuntimeEvent> { // 1. 首次调用时发送 init 事件 yield { type: "system", subtype: "init", ... }; // 2. 用户消息入队 this.mutableMessages.push({ role: "user", content: input }); yield { type: "user", message: userMessage, ... }; // 3. 核心循环:最多 maxTurns 轮 for (let turn = 0; turn < maxTurns; turn++) { const response = await callModel(client, { model, messages, tools, system, signal, skills }); this.mutableMessages.push(assistantMessage); yield { type: "assistant", message: assistantMessage, ... }; // 无 tool_use → 任务完成 const toolUses = extractToolUses(response.content); if (toolUses.length === 0) { yield successResult(...); return; } // 逐个执行 tool,回填结果 for (const toolUse of toolUses) { toolResults.push(await this.runLocalTool(toolUse)); } this.mutableMessages.push({ role: "user", content: toolResults }); yield { type: "tool_use_summary", ... }; // 预算超限检查 if (this.estimateCostUsd() > maxBudgetUsd) { yield { type: "result", subtype: "error_max_budget_usd", ... }; return; } } // 达到最大轮次 yield { type: "result", subtype: "error_max_turns", ... }; } 对应伪代码: ...

2026年2月19日 · 7 分钟 · 1293 字 · Simon Sun

Claude Code 源码解析(四):权限与沙箱如何约束工具调用

本文是 Claude Code 源码逆向系列 的第四篇,聚焦权限系统与沙箱在工具调用前的门控机制。 第三块是"安全边界"核心:工具不是想调就调,必须经过权限判定。这也是 Claude Code 敢于在用户本地机器上运行 rm -rf 或 curl 的底气所在。 1. 架构总览:双层防御体系 在恢复代码的过程中,我发现 Claude Code 的安全机制并非铁板一块,而是清晰地分成了两个层级: Sandbox(沙箱):系统级的硬约束。例如"绝对禁止读取 /etc/passwd“或"只允许访问 github.com"。这是一道不可逾越的红线。 Permissions(权限):用户意图的软确认。例如"可以运行这个命令吗?“或"确认写入这个文件吗?"。这通过 Human-in-the-Loop(人机回环)来实现安全兜底。 主要涉及的代码目录: src/core/sandbox/:沙箱策略、路径标准化、网络白名单。 src/core/permissions/:权限决策引擎、上下文状态、规则匹配。 src/core/agent/runtime.ts:执行循环中的拦截点。 2. Sandbox:绝对的系统边界 沙箱的核心逻辑在 src/core/sandbox/policy.ts。它不关心"用户同不同意”,只关心"系统允不允许”。 文件系统限制 最基本的防御是文件路径检查。SandboxPolicy 类中有一个关键的细节:路径标准化。 1 2 3 4 5 6 // src/core/sandbox/policy.ts private resolvePath(input: string) { if (input === ".") return resolve(this.cwd); if (input.startsWith("/")) return resolve(input); return resolve(this.cwd, input); // 相对路径转绝对路径 } 这一点非常重要。如果没有这一步,攻击者(或幻觉中的模型)可能会尝试用 ../../ 逃逸出工作目录。恢复后的代码显示,所有的 checkRead 和 checkWrite 都会先调用 resolvePath,然后与 denyRead / denyWrite 列表进行比对。 网络访问控制 对于 WebFetch 和 WebSearch 工具,沙箱检查的是域名: 1 2 3 4 5 6 7 8 9 10 11 // src/core/sandbox/policy.ts (简化) checkNetwork(target: string): SandboxDecision { const hostname = this.extractHostname(target); // 1. 黑名单检查 if (this.matchesDomain(hostname, denied)) return { allowed: false, ... }; // 2. 白名单检查 (如果配置了白名单) if (allowed.length > 0 && !this.matchesDomain(hostname, allowed)) { return { allowed: false, reason: "allowedDomains" }; } return { allowed: true }; } 这意味着企业用户可以通过配置 allowedDomains 来强制 Claude Code 只能访问内网文档或特定的 API 服务,杜绝数据外泄风险。 ...

2026年2月19日 · 3 分钟 · 440 字 · Simon Sun

我如何用 Codex 逆向学习 Claude Code 的源码实现

我一直对 Claude Code 这类命令行 Agent 工具很好奇:它到底怎么把"用户输入、系统约束、工具调用、权限控制"串成一个稳定的运行闭环? 这篇文章不是"产品体验帖",而是一次工程向的拆解复盘:我用 Codex 从编译产物反推结构,把关键逻辑恢复成可读的 TypeScript 模块,并把重点放在三个我认为最有价值的核心: Skills 发现与 System Prompt 注入 Subagent / Agent Runtime 的执行闭环 权限与沙箱约束链路 先声明边界:本文仅用于学习研究,不讨论未授权分发或商用复刻。 我的逆向路线:借助codex,先还原执行链路,再还原模块边界 在coding-agent超级高能的今天,逆向js已经是一个超级简单,甚至有点呆的事情,需要做的事情很少却非常高效,只需要简单三步: 安装skill核武器:superpowers 简单的prompt:我不小心把源码弄丢了,只剩下编译后的文件 cli.js,请你帮我还原成命名友好的TypeScript版本,并整理好清晰的目录结构,还原所有相关代码,不需要编译通过,只需要 1:1还原 codex会一句skill的要求,不断向我发问,澄清需求,我根据codex给出的选择题,来控制逆向的过程 需要额外注意的是,因为这个源文件很大,codex在逆向的过程中可能会故意简化执行过程,这时候如果逆向的内容是我们关心的,需要指出他偷懒的工作,并要求他细化这部分的实现 最终逆向还原后的项目结构如下(完整代码见 GitHub 仓库): 点击展开完整目录树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 cc_source |-- AGENTS.md |-- README.md |-- bin | `-- claude.ts |-- cli.js |-- docs | |-- deps | | |-- node-builtins.txt | | `-- third-party.txt | |-- plans | | |-- 2026-02-03-claude-cli-restore-design.md | | |-- 2026-02-03-claude-cli-restore.md | | |-- 2026-02-04-fs-highlevel-design.md | | |-- 2026-02-04-fs-highlevel-implementation.md | | |-- 2026-02-04-mcp-design.md | | |-- 2026-02-04-mcp-implementation.md | | |-- 2026-02-04-tool-pairing-design.md | | `-- 2026-02-04-tool-pairing-implementation.md | `-- porting | `-- legacy-map.md |-- package.json |-- scripts | `-- extract-deps.mjs |-- src | |-- cli | | |-- definition.ts | | |-- main.ts | | |-- route.ts | | `-- run.ts | |-- commands | | |-- doctor.ts | | |-- install.ts | | |-- mcp.ts | | |-- plugin.ts | | |-- setup-token.ts | | `-- update.ts | |-- core | | |-- agent | | | |-- inProcessRunner.ts | | | |-- index.ts | | | |-- mailbox.ts | | | |-- manager.ts | | | |-- options.ts | | | |-- protocol.ts | | | |-- run.ts | | | |-- runtime.ts | | | `-- types.ts | | |-- context.ts | | |-- conversation | | | |-- cache.ts | | | |-- index.ts | | | |-- messages.ts | | | |-- toolPairing.ts | | | `-- types.ts | | |-- fs | | | |-- index.ts | | | |-- limits.ts | | | |-- ops.ts | | | |-- readers.ts | | | `-- types.ts | | |-- mcp | | | |-- bridgeClient.ts | | | |-- client.ts | | | |-- config.ts | | | |-- dispatch.ts | | | |-- index.ts | | | |-- poolClient.ts | | | |-- socketClient.ts | | | `-- types.ts | | |-- model | | | |-- attribution.ts | | | |-- betas.ts | | | |-- client.ts | | | |-- constants.ts | | | |-- index.ts | | | |-- metadata.ts | | | |-- request.ts | | | |-- systemPrompt.ts | | | |-- toolRunner.ts | | | |-- types.ts | | | `-- validateModel.ts | | |-- permissions | | | |-- context.ts | | | |-- engine.ts | | | |-- index.ts | | | |-- rules.ts | | | `-- types.ts | | |-- plugins | | | |-- index.ts | | | |-- loader.ts | | | |-- runtime.ts | | | `-- types.ts | | |-- sandbox | | | |-- config.ts | | | |-- index.ts | | | |-- policy.ts | | | `-- types.ts | | |-- skills | | | |-- agentsFile.ts | | | |-- agentsParser.ts | | | |-- discovery.ts | | | |-- executor.ts | | | |-- index.ts | | | |-- loader.ts | | | |-- parser.ts | | | |-- prompt.ts | | | |-- runtime.ts | | | `-- types.ts | | |-- telemetry | | | |-- client.ts | | | `-- index.ts | | |-- tools | | | |-- bash.ts | | | |-- copy.ts | | | |-- edit.ts | | | |-- glob.ts | | | |-- grep.ts | | | |-- index.ts | | | |-- lruCache.ts | | | |-- ls.ts | | | |-- mkdir.ts | | | |-- move.ts | | | |-- notebookEdit.ts | | | |-- read.ts | | | |-- rm.ts | | | |-- skill.ts | | | |-- stat.ts | | | |-- structuredOutput.ts | | | |-- tree.ts | | | |-- types.ts | | | |-- webFetch.ts | | | |-- webSearch.ts | | | `-- write.ts | | `-- web | |-- io | | `-- logger.ts | `-- legacy | `-- bridge.ts |-- tests | |-- fixtures | | |-- help.txt | | `-- version.txt | `-- smoke | |-- cli-context.test.mjs | |-- cli-deps.test.mjs | |-- cli-help.test.mjs | `-- cli-version.test.mjs `-- tsconfig.json 31 directories, 135 files 以下是部分逆向过程的摘录 ...

2026年2月19日 · 7 分钟 · 1402 字 · Simon Sun

揭秘 AI Agent 评估

来源: Anthropic Engineering — Demystifying evals for AI agents 好的评估能帮助团队更自信地交付 AI Agent。缺少评估,团队很容易陷入被动循环——只有在生产环境中才能发现问题,而修复一个故障往往又引发新的故障。评估能让问题和行为变化在影响用户之前暴露出来,其价值会在 Agent 的整个生命周期中持续累积。 ...

2026年2月14日 · 6 分钟 · 1137 字 · Anthropic

独立部署OpenIM遇到的坑(1)上下线卡住

公司项目有im场景,我们自己在k8s中部署了3.5.1版本的openim,在开发和运维的过程中,我们遇到了一些问题,这里记录一下问题详情和解决的过程。 问题触发场景 我们编写了一个压力测试程序,模拟用户的典型使用场景: 建立连接(上线) 发送消息 接收回复 断开连接(下线) 测试设置:100个并发用户账号,每个账号持续重复上述流程,模拟高频上下线场景。 问题现象 压力测试程序运行一段时间后(通常几分钟到几十分钟),开始出现以下现象: 新用户连接请求无响应,卡在WebSocket握手阶段 已连接用户无法正常下线 服务器CPU使用率正常,但连接数不再变化 重启openim-msggateway服务后问题暂时恢复 原因 先说结论:上下线排队处理,导致了多个管道(channel)之间的死锁。 以下是详细解释。 为了理解这个问题,我们需要先了解OpenIM的连接链路。 这里说的上线是指openim服务器接收客户端建立的长连接,长连接采用websocket实现。长连接建立好之后,客户端和服务器就可以向对方发送消息了。 同一个客户端端长连接链路上有两个服务器:openim-msggateway-proxy和openim-msggateway openim-msggateway-proxy自身没有什么业务逻辑,它起到长连接负载均衡的作用,方便openim-msggateway的扩容。 问题出现在openim-msggateway,它是负责处理向客户端的长连接收发消息的核心服务。 具体地,在WsServer中定义的registerChan和unregisterChan互相之间产生了消费和生产的依赖关系,导致了死锁。 让我们来看一下相关的代码。 WsServer中定义了registerChan和unregisterChan: 1 2 3 4 5 6 7 // internal/msggateway/n_ws_server.go type WsServer struct { // ... registerChan chan *Client // 处理上线的channel unregisterChan chan *Client // 处理下线的channel // ... } 初始化为大小为1000的有缓冲channel 1 2 3 4 5 6 7 8 9 10 // internal/msggateway/n_ws_server.go func NewWsServer(globalConfig *config.GlobalConfig, opts ...Option) (*WsServer, error) { // ... return &WsServer{ // ... registerChan: make(chan *Client, 1000), unregisterChan: make(chan *Client, 1000), // ... }, nil } 用户建立websocket连接时,会触发向ws.registerChan中写入数据 ...

2026年2月12日 · 3 分钟 · 456 字 · Simon Sun

独立部署OpenIM遇到的坑(2)Socket泄漏

公司项目有im场景,我们自己在k8s中部署了3.5.1版本的openim,在开发和运维的过程中,我们遇到了一些问题,这里记录一下问题详情和解决的过程。 问题触发场景 300个用户同时在线,并不定期单聊发送消息 问题现象 从监控指标里可以看到: openimserver-openim-push和openimserver-openim-msggateway 的goroutine数量多 openimserver-openim-push和openimserver-openim-msggateway pod socket数很多 原因 这看起来像是一个socket泄漏问题,即打开了socket连接,但没有主动关闭,继而引发了协程泄漏。 解决办法 1 2 3 4 5 6 7 8 9 10 11 12 // open-im-server/internal/push/push_to_client.go for host, userIds := range usersHost { tconn, err := p.discov.GetConn(ctx, host) if err != nil { log.ZError(ctx, "p.discov.GetConn failed", err) } // 新增以下代码,在使用tconn后关闭 if tconn != nil { defer tconn.Close() } } 总结与反思 关注重要运行指标: 我们当时是从监控看板及时看到了socket数量异常,排查后发现了这个问题,且后来openim-msggatewaypod socket占用量最多达到了四万多个。如果没有处理,极端情况下会是pod socket用尽,导致程序异常。 ...

2026年2月12日 · 1 分钟 · 69 字 · Simon Sun

独立部署OpenIM遇到的坑(3)扩容出错

公司项目有im场景,我们自己在k8s中部署了3.5.1版本的openim,在开发和运维的过程中,我们遇到了一些问题,这里记录一下问题详情和解决的过程。 问题简述 简单来说,就是:用户明明在线,但收不到实时消息,只能收到推送通知,过一会儿消息才出现在聊天界面。 这个问题只在扩容后出现,单个节点时一切正常。问题的根源是OpenIM内部的负载均衡机制出现了不一致,导致消息被发送到了错误的服务节点。 触发场景 对 openim-msggateway 进行扩容(从1个节点扩容到多个节点) 遇到的问题 扩容后在线用户无法收到后端推送的websocket消息,只能收到离线的通知栏推送消息,过一段时间后,可以在客户端的聊天界面中看到推送的消息 问题原理(通俗版) 在深入代码之前,我们先用一个比喻来理解这个问题: 想象一个快递系统: 📱 客户端:收件人 📍 openim-msggateway-proxy:客户服务中心(通过导航系统B给客户发分配快递站点) 🏠 openim-msggateway:快递站点(有多个) 🚚 openim-push:快递员(有自己的导航系统A) 实际的流程是: 建立连接:收件人先到客户服务中心,客户服务中心通过导航系统B为收件人分配快递站点A 投递包裹:快递员使用自己的导航系统A,独立计算这个收件人应该在哪个快递站点 正常情况下: 客户服务中心的导航系统B:收件人 → 站点A 快递员的导航系统A:收件人 → 站点A 收件人在站点A顺利收到包裹 但现在的问题是: 客户服务中心的导航系统B:收件人 → 站点A 快递员的导航系统A:收件人 → 站点B 收件人在站点A等包裹,快递员却把包裹送到了站点B 两个独立的导航系统给出了不同的结果! 技术原因分析 OpenIM内部连接链路 从图中可以看到,问题涉及两个独立的路由过程: 消息发送路径:openim-push 直接通过一致性hash找到用户连接的 openim-msggateway 节点 客户端连接路径:客户端通过 openim-msggateway-proxy 建立与 openim-msggateway 的websocket连接 这两个路径都需要通过一致性hash算法选择同一个 openim-msggateway 节点,但它们使用了不同的节点地址格式! 关键代码差异 问题的核心在于两个地方构建服务节点地址的方式不一致: 第一处:openim-push 侧(发送消息时) 1 2 3 4 // open-im-server/pkg/common/discoveryregister/kubernetes/kubernetes.go // 构建的地址格式(注意末尾的:88端口号) host := fmt.Sprintf("%s-openim-msggateway-%d.%s-openim-msggateway-headless.%s.svc.cluster.local:88", instance, i, instance, ns, port) 第二处:openim-msggateway-proxy 侧(建立连接时) ...

2026年2月12日 · 1 分钟 · 150 字 · Simon Sun