关于 customizations-in-the-agent-host
面向 Agent Host 定制项启用机制的开发指南,涵盖插件、MCP 服务、Agent、技能和指令的状态决策、存储、发布与执行。它强调分作用域决策、客户端与主机叠加规则,以及按提供方实施启用控制。
主要功能
- 01按会话、工作区和全局范围决策
- 02区分客户端基础值与主机覆盖值
- 03按稳定身份保存 MCP 服务设置
- 04对待处理状态采用默认拒绝
- 05在发布和提供方交接时解析状态
- 06区分各提供方的启动与会话中控制
适用场景
- 01修改定制项启用状态逻辑
- 02排查 MCP 服务决策未持久化
- 03确保发布前执行启用检查
- 04验证不同提供方的启用限制
MCP 相关
说明 Agent Host 如何决定 MCP 服务是否启用、发布及交由各提供方执行,并涵盖启动和会话中的控制方式。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ vscode --skill customizations-in-the-agent-host安装后在 Claude Code 中输入/customizations-in-the-agent-host确认已加载。
详细介绍
Agent Host 中的自定义项
自定义项是 Agent Host 会话向模型公开的插件、MCP 服务、Agent、技能和指令。本技能介绍启用状态管理:如何确定、存储、发布和强制执行自定义项的开关状态。
在修改以下内容之前,请先阅读本文:
src/vs/platform/agentHost/node/agentHostCustomizationEnablementService.tssrc/vs/platform/agentHost/node/shared/customizationEnablementGate.tssrc/vs/platform/agentHost/common/customizationEnablement.tsnode/copilot/、node/claude/、node/codex/下各提供方的自定义项路径
思维模型
启用状态是一组作用域决策,而非布尔值
插件或 MCP 服务包含 enablement?: CustomizationEnablement[]——一个按特定程度从高到低排列的决策列表:
| 类型 | 所有者 | 含义 |
|---|---|---|
Session | Agent Host | 仅此会话 |
Workspace | Agent Host | 此工作目录(包含 uri) |
Global | 客户端;除非由主机发现 | 所有位置 |
isCustomizationEnabled() 会选取第一个条目;若条目缺失,则继承下一个更广的作用域;若列表不存在,则默认启用。切勿重新为插件或 MCP 服务添加并行的 enabled 布尔值——这个设计正是为了消除第二个事实来源。(DirectoryCustomization 合法地保留了普通的 enabled;使用者仍会通过 isCustomizationEnabled() 派生其状态。)
谁拥有 Global
对于主机发现的任何内容,主机都拥有其 Global,即使该内容位于客户端转发的插件中。客户端只拥有其实际打包的内容的 Global。判别依据是 childEnablement,而非 clientId。如果插件的 childEnablement 记录中包含某个子 MCP 服务的键,意味着“客户端打包了此子服务,并拥有其全局决策”;其他内容则是主机解析插件目录时发现的。
叠加模型
这是最重要的不变量。此前的错误处理导致了适得其反的写入,悄无声息地抹除了用户决策:
客户端的值是基值。主机决策叠加在其上。“仅存储有差异的值”应与该基值比较——绝不能与硬编码的
true比较。
具体而言,在 agentHostCustomizationEnablementService.ts 中:
_clientGlobalEnablement是按会话划分的内存中基值。_setClientGlobal在此处写入,绝不能触碰_persistent。_persistent仅保存与基值不同、由主机拥有的决策。- 继承顺序为主机 → 基值 →
DEFAULT_CUSTOMIZATION_ENABLED(_globalEnablement、_workspaceInheritedEnablement)。 _setPersistentDecision会删除与其继承值相同的决策,以便界面无需第三个“继承”操作即可恢复继承状态。
将两者分开,才能避免以下循环:replaceEnablement → 同步重新发布 → 客户端过时的 [{global,true}] → _setClientGlobal → “仅存储有差异的值”删除刚写入的条目。
身份不能依赖树中的位置
MCP 服务的存储键由 owningPluginUri 派生,而非由它在已发布树中的位置决定。同一服务无论是在顶层被发现(mcpServers#azure),还是嵌套在插件下(<pluginURI>#mcp=azure),都必须生成同一个键,否则某个会话中写入的决策在下一个会话中便不可见。targetForMcpServer() 和门控中的 withOwningPluginUri() 是构建该目标的唯一受支持方式。
Pending 表示关闭失败——但 pending 必须是临时状态
当会话尚未初始化(reason: 'session')或尚未确定工作目录(reason: 'workingDirectory')时,resolve() 会返回 {kind:'pending'}。resolveCustomizationEnablement() 会将这些 ID 记录在 pendingCustomizationIds 中,而 isCustomizationSdkEligible() 会让 SDK 边界的使用方默认拒绝。
由此得出两条规则:
- 绝不要通过默认启用来“修复” pending 状态的自定义项。
- 绝不要缓存 pending 判定。Pending 是一种临时状态;应重新计算,并在启用服务完成处理后重新发布。缓存
pendingEnablement: true会悄无声息地永久丢弃插件。
子 MCP 服务处于 pending 状态时,其所属插件也会标记为 pending,因为 SDK 可能会自行从插件目录中发现该子服务。
避免大多数错误的规则
每条发布、发送事件和移交给 SDK 的路径都必须经过解析。
此功能中有三个独立的生产环境错误源于跳过了解析(copilotAgent 中的四处、mcpCustomizationController 中的一处,以及启动后的协调逻辑)。新增任何发布自定义项或将其交给提供方的路径时,都应通过 resolveCustomizationEnablement() 处理——不要读取已存储的快照。
由此还可得出:输入发生变化时,必须重建只计算一次的状态。 对每个客户端合并的启用映射若只追加条目,那么当具有相同 URI 的新插件被解析时,旧判定就会泄漏到新插件上。
强制执行按提供方区分,且各不相同
| 提供方 | 启动时 | 会话中途 |
|---|---|---|
| Copilot | 打开/恢复时,在 SessionConfig 上设置 disabledMcpServers | rpc.mcp.enable / rpc.mcp.disable |
| Claude | 实例化时设置会话选项 | 实时协调 |
| Codex | 仅在 thread/start / 恢复时提供 MCP 服务 | 不可能——文档所述的限制 |
强制执行中的陷阱:
- 启动后的协调会在启动约 800 ms 后运行。仅在该阶段进行门控的内容仍可能在启动窗口期间启动并请求身份验证。启动时也要进行门控。
- SDK 的
not_configured状态不得被转换为enabled: true。这个错误会让正确的“跳过此禁用服务”判定看起来像是“启用它”。 - Copilot 启动门控必须包含用户
mcp.config.list中的名称,而不能只使用客户端快照。协调和身份验证也必须在主机保留新发现的名称之前先解析它们;后续发布不得将继承的启用默认值变成启用由 SDK 配置为禁用的服务的显式意图。 - 根 MCP 自定义项 ID 使用所属的 AHP 会话 ID,而不是其 SDK 后端 ID。恢复时,应从已应用的根/插件定义中推导已声明的 MCP 名称,绝不要从主机保留的已发布运行时清单中推导。
- 连接生命周期(
pending、connected、stopped)不代表启用意图。应通过被动的mcp.listConfigured启用状态确认外部切换,并忽略过时的异步快照和主机请求的更改。
客户端提供的 MCP 服务(由客户端打包,而非由主机发现)必须将全局禁用操作路由到客户端,而不是主机——主机会正确拒绝存储客户端所有的全局设置,因此将其路由到主机只会悄无声息地无效。区分依据是 isClientBundled;请参阅 mcpListWidget.ts 中的 getBuiltinMcpServerEnablementActions。
调试手册
为信号排序。它们彼此矛盾,而且模型会撒谎:
- 主要依据 — 已发布的自定义对象和主机存储的决策。
- 工具公开情况的确定依据 —
Agent Host.log中的tool_search override … clientMatched=[…]。 - 次要依据 — 自定义项 UI。
- 仅作佐证 — 模型的说法。服务器处于
stopped状态时,模型回答“是的,我有 azure 工具”。切勿仅据此下结论。
复现流程,用于触发真实故障面:启动会话,发送一条消息以启动所有内容,禁用该项,然后在一个新会话中询问是否有这些工具。身份和覆盖层错误会在跨会话持久化时显现。
使用 agent-host-logs 技能处理导出的日志包,并使用 launch 技能驱动真实实例。
已知误报:
azure插件除了 MCP 服务,还会提供 24 个azure-*技能。禁用服务后,技能仍保持启用,这是正确行为。sleepy是一个故意设计为损坏状态的测试服务,无法初始化,不能用作正向对照。
可能耗费大量时间的陷阱
CustomizationType和CustomizationEnablementKind是const enum。**仅类型导入能够通过编译和 lint 检查,但会在运行时崩溃。**请将它们作为值导入。- 单元测试运行器使用预编译输出,不会因类型错误而失败。务必单独运行
npm run typecheck-client。 - 集成测试服务以
--quiet启动。在agentHostServerMain.ts中将if (!options.quiet)内的代码提升到外部,会悄无声息地改变它们的行为——提升setWorktreeIsolation后,安静模式的服务会将isolation: 'folder'解析为结果,并导致另外七项无关测试失败。 common/state/protocol/下的协议文件带有“DO NOT EDIT — auto-generated”标记。对此处的修改需要同步回补到agent-host-protocol仓库。- Electron 运行器设置中止(
EEXIST/EPERM/ENOENT/ReactiveObjC)属于环境问题。运行rm -rf .build/electron && npm run electron进行恢复。
验证
npm run typecheck-client
./scripts/test.sh --grep "customizationEnablement"
./scripts/test-integration.sh --runGlob "**/agentHost/**/{sessionConfig,toolApproval,codexCustomizations}.integrationTest.js"
./scripts/test-integration.sh --run src/vs/platform/agentHost/test/node/claudeAgent.integrationTest.ts
直接构造 Agent 会话的测试需要在其服务集合中加入 IAgentHostCustomizationEnablementService;请使用 src/vs/platform/agentHost/test/node/testCustomizationEnablementService.ts 中的 createNoopCustomizationEnablementService()。