465 lines
24 KiB
Markdown
465 lines
24 KiB
Markdown
# Plugin Command 与 Settings 问题与修复复盘
|
||
|
||
> 审阅与修复日期:2026-09-02
|
||
> 涉及分支:`feat/plugin-command-settings`
|
||
> 初始功能提交:`6a08ad8 feat(extension): 实现插件命令与设置贡献`
|
||
> 最终修复提交:`d39ae72 fix(extension): 收紧插件命令运行时契约`
|
||
> 合并提交:`ff3da5d Merge pull request 'feat(extension): 实现 Plugin Command 与 Settings Contribution' (#10)`
|
||
> 文档用途:记录阶段 D 从首次提交、连续审阅到合并期间发现的问题,说明形成原因、实际后果、解决思路和最终方案,供后续开发、问题定位、比赛材料和技术博客写作使用。
|
||
|
||
## 1. 背景与结论
|
||
|
||
阶段 D 在既有 Extension Core、MCP Bridge 和 Plugin Host 上增加 Plugin Command 与 Settings Contribution。初始实现已经具备命令注册、参数校验、设置持久化、Secret 加密存储和前端 Service,但首次提交仍把若干“接口能够调用”误当成了“安全边界已经闭合”。
|
||
|
||
本次分支没有在初始功能提交后直接合并,而是围绕清单输入、凭据隔离、JSON Schema、MCP 协议、事务一致性和前后端 Contract 连续审阅。最终共形成 1 个功能提交和 6 个修复提交,处理 12 类问题:
|
||
|
||
| 编号 | 问题 | 级别 | 处理结果 |
|
||
| --- | --- | --- | --- |
|
||
| D-01 | Plugin Secret 可被通用凭据接口或 Provider 引用 | P0 | 建立 `plugin.*` 保留命名空间和双层访问门禁 |
|
||
| D-02 | Command 与 Settings 清单边界不完整 | P1 | 收紧空值、重复项、权限、类型和数值边界校验 |
|
||
| D-03 | MCP Command Target 只有声明,没有真实执行链路 | P0 | 增加专用 MCP 命令目标并与 Agent Tool 隔离 |
|
||
| D-04 | JSON Schema 外部引用可能触发越权 I/O | P0 | 只允许当前文档内 Fragment 引用 |
|
||
| D-05 | Secret 引用可被篡改并跨凭据命名空间 | P0 | 改用定长哈希引用并在读取、删除前重新核对 |
|
||
| D-06 | Secret 删除和插件卸载缺少事务一致性 | P0 | 原子替换凭据表,失败时恢复 Settings 引用 |
|
||
| D-07 | Schema 引用校验忽略嵌套 `$id` 资源作用域 | P1 | 使用 Draft 2020-12 Resource Resolver 按资源解析 |
|
||
| D-08 | MCP Command 未收到普通 Settings | P1 | 在固定信封中同时传递 Settings 与声明过的 Secret |
|
||
| D-09 | Command Effect 是任意字典,前后端边界过宽 | P0 | 改为五类判别联合并限制各自 Payload |
|
||
| D-10 | 必填普通设置没有阻止启用和执行 | P1 | 在 Enable 与 Execute 两个入口增加运行时门禁 |
|
||
| D-11 | MCP 工具 Schema 被发现后丢弃,真实信封未校验 | P0 | 保存目标 Schema,并在调用前校验完整信封 |
|
||
| D-12 | 启用探测和回归测试存在假阳性或假阴性 | P1 | 使用最小协议标记,移除自制求解器并补强测试 |
|
||
|
||
最终验证基线为后端 136 项测试、前端 29 项测试、TypeScript 类型检查、前端生产构建、`uv lock --check` 和 `git diff --check` 通过。PR #10 已合并到 `main`。
|
||
|
||
## 2. 提交与审阅过程
|
||
|
||
| 顺序 | 提交 | 主要内容 |
|
||
| --- | --- | --- |
|
||
| 1 | `6a08ad8` | 首次实现 Plugin Command、Settings、Secret API 和前端 Service |
|
||
| 2 | `c3ef9df` | 收紧 Plugin Secret、Provider 凭据和 Command 清单边界 |
|
||
| 3 | `9e680a0` | 补齐真实 MCP Command Target,限制 JSON Schema 外部引用 |
|
||
| 4 | `022c322` | 改造 Secret 引用,修复删除和卸载事务 |
|
||
| 5 | `c06b962` | 按 Draft 2020-12 资源作用域修复 Schema 引用解析 |
|
||
| 6 | `eb3464b` | 补回 MCP Command 信封中的普通 Settings |
|
||
| 7 | `d39ae72` | 收紧 Effect、必填设置和 MCP 运行时 Contract,修复测试盲点 |
|
||
| 合并 | `ff3da5d` | PR #10 合并进入 `main` |
|
||
|
||
这段过程说明,Extension Core 的风险主要不在正常路径能否运行,而在不同入口是否共享同一套边界。HTTP、Provider Factory、Plugin Runtime、MCP Host、Settings Store 和测试 Fixture 只要有一个入口绕过限制,就可能形成跨命名空间访问、错误执行或假测试通过。
|
||
|
||
## 3. D-01:Plugin Secret 可被通用凭据接口或 Provider 引用
|
||
|
||
### 原因
|
||
|
||
初始实现把 Plugin Secret 存入已有 `EncryptedCredentialStore`,但只在 Plugin Settings API 中隐藏明文,没有为凭据 ID 建立用途隔离。通用凭据 API 可以读写或删除同名 ID,Provider 配置也可以把 Plugin Secret 的凭据引用当作自己的 API Key 使用。
|
||
|
||
此外,Command 能否读取 Secret 只依赖 Plugin 拥有 `secrets.use` 权限,没有继续限制到当前 Command 在 `commands.yaml` 中明确声明的字段。
|
||
|
||
### 后果
|
||
|
||
- 前端或其他模块可以覆盖、删除 Plugin 私有 Secret;
|
||
- Provider 可能把插件密钥发送给外部模型服务;
|
||
- 同一 Plugin 内权限较低的 Command 可以读取与自己无关的 Secret;
|
||
- API 没有回显明文并不代表 Secret 没有发生横向流动。
|
||
|
||
### 解决思路
|
||
|
||
Secret 隔离必须同时覆盖存储命名空间、公共 HTTP 入口、Provider 解析入口和 Command 字段级授权,不能只在返回 DTO 上隐藏值。
|
||
|
||
### 解决方案
|
||
|
||
- 将 `plugin.*` 设为 Plugin Settings 专用保留命名空间;
|
||
- 通用凭据查询、写入和删除接口拒绝访问该前缀;
|
||
- 在 `ProviderFactory` 外包一层 `ProviderCredentialResolver`,即使绕过 HTTP 配置校验也不能读取 Plugin Secret;
|
||
- Command 通过 `secrets` 字段声明允许读取的 Setting Key;
|
||
- Resolver 同时检查插件权限、字段是否存在、字段类型和 Command 声明;
|
||
- 未声明字段返回 `PLUGIN_SECRET_ACCESS_DENIED`,必填 Secret 未配置返回 `PLUGIN_SECRET_REQUIRED`;
|
||
- 审计事件不记录 arguments、effect 或 Secret。
|
||
|
||
## 4. D-02:Command 与 Settings 清单边界不完整
|
||
|
||
### 原因
|
||
|
||
YAML 解析成功只说明语法可读,并不代表清单结构满足宿主协议。初始校验对 `commands:` 空值、重复 Secret、未声明权限、无效执行目标、Settings 默认值及非有限数值边界等情况覆盖不足。
|
||
|
||
例如 YAML 中只有 `commands:` 时,解析结果是 `null` 而不是空数组;`NaN` 和正负无穷虽然属于 Python 浮点值,却不能成为可移植的表单边界。
|
||
|
||
### 后果
|
||
|
||
- 安装阶段可能放过无法执行的 Command;
|
||
- 空清单在后续遍历时变成内部异常,而不是稳定业务错误;
|
||
- 前后端对数值范围和默认值产生不一致理解;
|
||
- 重复声明或越过 Plugin 命名空间的 ID 会污染全局注册表;
|
||
- 错误只能到运行期暴露,定位成本更高。
|
||
|
||
### 解决思路
|
||
|
||
把 Plugin 包视为不可信输入,在安装阶段完成结构、语义和权限的完整验证,并将解析异常统一转换成稳定的 `ExtensionError`。
|
||
|
||
### 解决方案
|
||
|
||
- 要求 `contributes` 声明与 `commands.yaml`、`settings.yaml` 内容完全一致;
|
||
- 拒绝 `null` Command 列表、重复 ID、重复 Secret 和越过 Plugin 命名空间的标识;
|
||
- 执行目标只能在受控 `handler` 与当前 Plugin 的 `mcp_tool` 中二选一;
|
||
- 校验 `when`、Context、Location 和权限白名单;
|
||
- Settings 类型限定为 `string`、`number`、`boolean`、`select`、`secret`;
|
||
- 校验默认值、Select 选项、必填规则和最小/最大边界;
|
||
- 拒绝 `NaN`、正无穷和负无穷;
|
||
- 清单错误统一返回可定位的稳定错误码。
|
||
|
||
## 5. D-03:MCP Command Target 只有声明,没有真实执行链路
|
||
|
||
### 原因
|
||
|
||
初始 Command 执行器只支持宿主内置 Handler。接口和规划中虽然存在 MCP Command 的概念,但 Runtime 没有把 Command 绑定到 MCP 工具,也没有定义 Command Context、Settings 和 Secret 如何进入 MCP 请求。
|
||
|
||
直接复用 Agent `ToolRegistry` 看似省事,却会把“用户主动执行的插件命令”和“模型可自主调用的 Agent Tool”混成同一种能力。
|
||
|
||
### 后果
|
||
|
||
- MCP 插件声明的 Command 实际无法执行;
|
||
- 如果简单注册为 Agent Tool,模型可能绕过命令位置、Context 裁剪和 Secret 声明直接调用;
|
||
- Command 超时、Effect 和错误边界无法统一;
|
||
- 前端看到命令已注册,点击后却只能得到运行时错误。
|
||
|
||
### 解决思路
|
||
|
||
为 MCP Command 建立独立执行通道。它可以复用 MCP 连接,但不能自动进入 Agent Tool Registry;宿主负责构造固定协议信封并验证返回的白名单 Effect。
|
||
|
||
### 解决方案
|
||
|
||
- `commands.yaml` 支持当前 Plugin 命名空间内的 `mcp_tool` 目标;
|
||
- Runtime 在 Plugin 启用后绑定目标,在禁用、异常退出和重启时同步注销;
|
||
- MCP Command 不注册到 Agent `ToolRegistry`;
|
||
- 宿主只传递已校验 arguments、已裁剪 Context、有效 Settings 和当前 Command 声明的 Secret;
|
||
- MCP 返回结果必须转换成受控 Effect;
|
||
- 远程原始异常、过大结果和超时统一映射为稳定宿主错误。
|
||
|
||
## 6. D-04:JSON Schema 外部引用可能触发越权 I/O
|
||
|
||
### 原因
|
||
|
||
Command 和 MCP Tool 都接受插件提供的 JSON Schema。初始实现直接交给校验器处理 `$ref` 或 `$dynamicRef`,没有限制引用 URI。恶意或错误 Schema 可以引用本地文件、HTTP 地址或其他外部资源。
|
||
|
||
### 后果
|
||
|
||
- Schema 校验可能读取宿主文件或发起未授权网络请求;
|
||
- 安装一个插件就可能产生隐式 I/O;
|
||
- 离线环境中校验结果不稳定;
|
||
- 外部资源变化会让相同插件包得到不同验证结果;
|
||
- Command 和 Agent Tool 如果采用不同规则,会出现新的绕过路径。
|
||
|
||
### 解决思路
|
||
|
||
当前阶段不需要跨文件 Schema。宿主应只允许当前文档内部的 Fragment 引用,并在注册阶段递归检查所有 Schema 节点。
|
||
|
||
### 解决方案
|
||
|
||
- Command 与 Tool 共用 `schema_security` 校验边界;
|
||
- 递归扫描 `$ref` 和 `$dynamicRef`;
|
||
- 只允许以 `#` 开头的当前文档 Fragment;
|
||
- 拒绝文件、HTTP 和其他外部资源 URI;
|
||
- 无法解析的本地引用在安装或注册阶段直接失败;
|
||
- 运行期继续使用官方 Draft 2020-12 Validator 校验数据。
|
||
|
||
## 7. D-05:Secret 引用可被篡改并跨凭据命名空间
|
||
|
||
### 原因
|
||
|
||
初始 Settings 文件保存的 Secret 引用由可读的 Plugin ID 和 Setting Key 拼接而成。长度随名称增长,而且 Runtime 读取引用时默认信任磁盘内容,没有重新确认该引用确实属于当前字段。
|
||
|
||
本地文件损坏或被篡改后,一个 Plugin Setting 可以被改为指向 Provider 凭据或另一个 Plugin 的 Secret。
|
||
|
||
### 后果
|
||
|
||
- 长 Plugin ID 和 Setting Key 可能超过凭据 ID 长度限制;
|
||
- Settings 文件泄露内部字段名称;
|
||
- 篡改引用可能造成跨命名空间读取或删除;
|
||
- 卸载一个插件时可能误删其他模块的凭据。
|
||
|
||
### 解决思路
|
||
|
||
Secret 引用应由宿主确定性生成,长度固定,并在每次敏感操作前由当前 `plugin_id + setting_key` 重新计算,而不是信任持久化文件。
|
||
|
||
### 解决方案
|
||
|
||
使用以下语义生成引用:
|
||
|
||
```text
|
||
plugin.<sha256(plugin_id + "\\0" + setting_key)>
|
||
```
|
||
|
||
- 引用长度固定且符合凭据 ID 规则;
|
||
- Settings Store 只保存引用和 `configured` 状态,不保存明文;
|
||
- 读取、覆盖、删除和卸载前重新计算期望引用;
|
||
- 引用不匹配时按损坏存储拒绝处理;
|
||
- 增加跨命名空间篡改回归测试。
|
||
|
||
## 8. D-06:Secret 删除和插件卸载缺少事务一致性
|
||
|
||
### 原因
|
||
|
||
Secret 同时涉及 Settings 引用文件和加密凭据文件。初始删除流程按顺序修改两个存储,但任一步失败都没有完整回滚。插件卸载多个 Secret 时逐条删除,执行到一半失败会留下部分清理状态。
|
||
|
||
### 后果
|
||
|
||
- Settings 显示未配置,但密文仍残留;
|
||
- 凭据已删除,Settings 却仍显示已配置;
|
||
- 多 Secret 卸载可能只删除前几项;
|
||
- 重试操作无法判断上一次执行到哪里;
|
||
- 用户以为插件卸载已清除密钥,实际磁盘仍可能保留数据。
|
||
|
||
### 解决思路
|
||
|
||
把引用更新和凭据删除看作一个逻辑事务。底层单文件凭据表应一次构造新状态并原子替换;跨 Settings 与 Credential Store 的操作需要显式补偿回滚。
|
||
|
||
### 解决方案
|
||
|
||
- `EncryptedCredentialStore` 增加多凭据原子删除;
|
||
- 先验证全部目标引用,再生成新的凭据表;
|
||
- 通过临时文件和原子替换一次提交;
|
||
- 删除单个 Secret 时,凭据删除失败则恢复原 Settings 引用;
|
||
- 卸载 Plugin 时,批量删除失败则恢复完整 Settings 命名空间;
|
||
- 错误统一转换为稳定存储错误,避免部分成功被当作完整成功。
|
||
|
||
## 9. D-07:Schema 引用校验忽略嵌套 `$id` 资源作用域
|
||
|
||
### 原因
|
||
|
||
第一版本地引用检查把整个 Schema 当成单一 Fragment 树,用根文档指针或 Anchor 查找所有引用。Draft 2020-12 允许嵌套 `$id` 创建新的 Schema Resource;资源内部的 `#anchor` 应相对于新的 Base URI 解析,根资源也不能反向使用嵌套资源的 Anchor。
|
||
|
||
### 后果
|
||
|
||
- 合法的嵌套资源引用被误拒绝;
|
||
- 根 Schema 可能错误引用只属于子资源的 Anchor;
|
||
- 宿主预检结果与官方运行时 Validator 不一致;
|
||
- 同一 Schema 在安装阶段通过,却可能在执行阶段失败,反之亦然。
|
||
|
||
### 解决思路
|
||
|
||
安全限制仍然是“禁止外部资源”,但本地资源内部的解析语义必须遵守 JSON Schema 标准,不能自己用字符串和全局 Anchor 集合近似实现。
|
||
|
||
### 解决方案
|
||
|
||
- 引入与 Draft 2020-12 Validator 配套的 Resource Registry;
|
||
- 为根资源和嵌套 `$id` 建立正确作用域;
|
||
- 每个引用按其所在资源的 Base URI 解析;
|
||
- 保留外部资源拒绝策略;
|
||
- 增加“根资源不能使用子资源 Anchor”和“子资源可使用自身 Anchor”的成对测试。
|
||
|
||
## 10. D-08:MCP Command 未收到普通 Settings
|
||
|
||
### 原因
|
||
|
||
真实 MCP Command 链路补齐后,固定信封传递了 Command ID、Arguments、Context 和 Secret,但遗漏了已经通过 Schema 校验的普通 Settings。声明式内置 Handler 能读取 Settings,MCP Handler 却不能,两个执行目标语义不一致。
|
||
|
||
### 后果
|
||
|
||
- 用户在插件设置页修改普通配置,对 MCP Command 不生效;
|
||
- 插件只能把非敏感设置错误地编码进 arguments 或 Secret;
|
||
- 内置 Handler 测试通过会掩盖 MCP 路径的缺口;
|
||
- 插件从内置实现迁移到 MCP 后行为发生变化。
|
||
|
||
### 解决思路
|
||
|
||
内置 Handler 与 MCP Command 应消费同一份运行时配置。差别只在执行介质,不在 Command Contract。
|
||
|
||
### 解决方案
|
||
|
||
- MCP 固定信封增加 `settings`;
|
||
- Settings 由 `PluginSettingsStore.runtime_values()` 产生;
|
||
- 只传递当前 Plugin Schema 中有效的非敏感字段;
|
||
- Secret 继续放在独立 `secrets` 命名空间;
|
||
- Fixture 回显非敏感配置用于断言,但不回显 Secret;
|
||
- 增加 Settings 实际到达 MCP Server 的集成测试。
|
||
|
||
## 11. D-09:Command Effect 是任意字典,前后端边界过宽
|
||
|
||
### 原因
|
||
|
||
初始 `PluginCommandEffect` 只有 `type` 和任意 `payload`。宿主虽然限制 Effect 名称和总体大小,却没有限制 Payload 字段、路由名称、刷新范围或通知级别。前端 TypeScript 也只能把 Payload 当成普通对象处理。
|
||
|
||
### 后果
|
||
|
||
- 插件可以返回前端从未支持的字段和路由;
|
||
- 前端需要在运行时猜测 Payload 结构;
|
||
- `navigate` 或 `refresh` 可能越过宿主允许的目标;
|
||
- OpenAPI 无法表达不同 Effect 的必填字段;
|
||
- 无效 Effect 往往要到页面执行时才暴露。
|
||
|
||
### 解决思路
|
||
|
||
Effect 是宿主能力协议,不是插件任意消息。每种 Effect 都应是独立、封闭、可判别的 Contract,并在进入 HTTP 响应前完成验证。
|
||
|
||
### 解决方案
|
||
|
||
- 后端建立 `notification`、`navigate`、`refresh`、`job`、`none` 五类模型;
|
||
- 使用 `type` 作为 Pydantic 判别字段;
|
||
- 限制通知级别、消息长度、路由白名单、刷新 Scope 和 Job ID;
|
||
- TypeScript 同步为精确的判别联合;
|
||
- OpenAPI `PluginCommandResult.effect` 生成 `oneOf` 和 discriminator;
|
||
- 保留可序列化性和 64 KiB 总大小限制作为第二层保护。
|
||
|
||
## 12. D-10:必填普通设置没有阻止启用和执行
|
||
|
||
### 原因
|
||
|
||
初始 Runtime 会合并已保存值和默认值,却没有检查 `required` 且没有默认值的普通字段是否仍为空。Secret 已有独立缺失检查,因此测试容易只覆盖 Secret,忽略普通 Settings。
|
||
|
||
### 后果
|
||
|
||
- 配置不完整的 Plugin 仍可启动 MCP Host;
|
||
- Command 到插件内部才因缺少字段失败;
|
||
- 用户只能看到模糊执行错误,不知道应先补配置;
|
||
- 插件启用后删除必填值,后续执行没有再次校验。
|
||
|
||
### 解决思路
|
||
|
||
必填设置既是启用前置条件,也是每次执行的运行时不变量。不能只在保存表单时校验,因为磁盘内容可能变化,启用后的配置也可能被更新。
|
||
|
||
### 解决方案
|
||
|
||
- `runtime_values()` 返回完整有效普通 Settings;
|
||
- 缺少必填且无默认值的字段时抛出 `PLUGIN_SETTINGS_REQUIRED`;
|
||
- Plugin Enable 前执行一次检查,不启动无效 Host;
|
||
- 每次 Command Execute 前重新检查;
|
||
- 返回 409 和缺失字段上下文,便于前端引导用户进入设置页;
|
||
- 增加“补齐设置后可启用”的完整回归测试。
|
||
|
||
## 13. D-11:MCP 工具 Schema 被发现后丢弃,真实信封未校验
|
||
|
||
### 原因
|
||
|
||
MCP 初始化阶段能够取得工具名称和 `inputSchema`,但 Runtime 记录只保留了工具名。Command 执行时直接发送宿主信封,没有用目标工具的完整 Schema 校验实际数据。
|
||
|
||
这意味着插件只要暴露同名工具就可能通过启用检查,即使它根本不接受 NotesAgent Command 协议。
|
||
|
||
### 后果
|
||
|
||
- 不兼容目标在启用阶段被注册为可执行 Command;
|
||
- 错误推迟到远程 MCP Server,返回信息不稳定;
|
||
- Context、Settings 或 Secret 结构变化时无法在宿主边界发现漂移;
|
||
- 前端看到可用命令,执行后才得到 502;
|
||
- 禁用或重启后若 Schema 缓存不清理,还可能使用过期契约。
|
||
|
||
### 解决思路
|
||
|
||
发现阶段保留完整目标 Schema;启用阶段只检查最低协议标记;执行阶段再用真实数据验证全部约束。Schema 生命周期必须和 MCP 工具生命周期一致。
|
||
|
||
### 解决方案
|
||
|
||
- Plugin 运行记录增加 MCP Command Schema 映射;
|
||
- 禁用、回滚、重启和 Host 不可用时同步清理 Schema;
|
||
- 启用时要求目标 Schema 顶层直接声明 `properties._notesagent`,且类型为 `object`;
|
||
- 执行前构造完整固定信封;
|
||
- 使用官方 Draft 2020-12 Validator 校验真实信封;
|
||
- 不匹配时返回 `PLUGIN_COMMAND_TARGET_SCHEMA_MISMATCH`,不调用 MCP Server。
|
||
|
||
## 14. D-12:启用探测和回归测试存在假阳性或假阴性
|
||
|
||
### 原因
|
||
|
||
审阅期间先后暴露了三类测试方法问题:
|
||
|
||
1. 启用阶段曾用空 arguments、Context、Settings 和 Secret 伪造信封,以判断目标 Schema 是否兼容。合法 Schema 如果要求真实业务字段,会被错误拒绝;
|
||
2. 为避免空数据误判,曾尝试加入自定义 JSON Schema 可满足性求解,但该近似实现无法正确覆盖 `not`、`oneOf` 等完整 Draft 2020-12 语义;
|
||
3. MCP Secret Fixture 即使没有收到 `api_key` 也会返回成功,测试只验证了命令成功,没有证明 Secret 真正到达服务端。
|
||
|
||
最后还发现空 Echo 消息会构造 `notification`,但新的通知 Contract 要求消息非空,导致合法空输入被包装为 502。
|
||
|
||
### 后果
|
||
|
||
- 合法插件可能在启用阶段被拒绝,形成假阴性;
|
||
- 不兼容 Schema 可能被自定义求解器放过,形成假阳性;
|
||
- Secret 传递链路回归后测试仍显示通过;
|
||
- Contract 收紧后,旧 Fixture 的边界值会产生新的运行时错误;
|
||
- 测试数量增加,却没有真正覆盖需要证明的安全事实。
|
||
|
||
### 解决思路
|
||
|
||
启用阶段只做稳定且明确的协议结构检查,完整 Schema 语义交给官方 Validator 和真实执行数据。测试必须让目标事实缺失时明确失败,而不是通过返回值间接猜测。
|
||
|
||
### 解决方案
|
||
|
||
- 取消空业务数据 Probe;
|
||
- 将启用门槛缩小为直接 `_notesagent: { type: object }` 协议标记;
|
||
- 允许 `$ref`、`$dynamicRef`、`allOf`、`anyOf`、`oneOf` 和 `not` 等约束出现在 `_notesagent` 内部;
|
||
- 完全移除自定义 Schema 可满足性求解器;
|
||
- 实际执行统一交给官方 Draft 2020-12 Validator;
|
||
- MCP Fixture 未收到声明的 Secret 时主动返回 MCP 错误,但永不返回明文;
|
||
- 空 Echo 返回 `none` Effect,非空 Echo 返回 `notification`;
|
||
- 为协议标记、真实信封、Secret 到达、空 Echo 和恶意 Effect 分别增加回归测试。
|
||
|
||
## 15. 验证方法与结果
|
||
|
||
本分支最终执行以下验证:
|
||
|
||
```powershell
|
||
cd backend
|
||
uv lock --check
|
||
uv run pytest
|
||
|
||
cd ../frontend
|
||
pnpm test -- --run
|
||
pnpm type-check
|
||
pnpm build
|
||
|
||
cd ..
|
||
git diff --check
|
||
```
|
||
|
||
结果:
|
||
|
||
```text
|
||
backend: 136 passed
|
||
frontend: 29 passed
|
||
TypeScript type-check passed
|
||
frontend production build passed
|
||
uv lock --check passed
|
||
git diff --check passed
|
||
```
|
||
|
||
后端测试在 Windows 下仍有既有 `.pytest_cache` 权限警告,不影响测试结果。前端生产构建仍有既有大 Chunk 提示,不影响本次功能正确性和合并结论。
|
||
|
||
新增或强化的关键测试包括:
|
||
|
||
- Plugin Secret 公共 API、Provider Resolver 和 Command Resolver 三层隔离;
|
||
- 清单空值、重复项、无效权限、非有限边界和非法 Schema;
|
||
- MCP Command 与 Agent Tool 隔离;
|
||
- 固定信封中的 Context、Settings 和声明式 Secret;
|
||
- 外部 Schema 引用拒绝和嵌套 `$id` 资源作用域;
|
||
- Secret 定长引用、篡改阻断、删除回滚和卸载原子性;
|
||
- 五类 Effect 的合法 Payload 和恶意 Payload 拒绝;
|
||
- 必填普通设置对 Enable 和 Execute 的双重门禁;
|
||
- MCP 实际信封校验、Secret 到达证明和空 Echo 行为。
|
||
|
||
## 16. 预防措施
|
||
|
||
- Plugin 包、MCP Schema 和 MCP 返回值一律按不可信输入处理;
|
||
- Secret 安全检查必须覆盖写入、读取、删除、解析、传输和审计全链路;
|
||
- 保留命名空间需要在公共 API 和内部 Resolver 两侧同时阻断;
|
||
- 多存储更新必须设计原子提交或补偿回滚,并测试中途失败;
|
||
- 不自行实现通用 JSON Schema 求解器,标准语义交给官方 Validator;
|
||
- 启用阶段只检查静态协议能力,不使用伪业务数据推导可执行性;
|
||
- Agent Tool 和用户触发的 Plugin Command 必须保持独立注册和权限边界;
|
||
- Pydantic Contract、OpenAPI、TypeScript DTO 和测试 Fixture 在同一提交中同步;
|
||
- 回归测试应直接证明目标事实,例如“Secret 确实到达且未落盘”,不能只证明接口返回成功;
|
||
- 每次收紧 Contract 后,重新检查空值、默认值、最大值和旧 Fixture 等边界输入。
|
||
|
||
## 17. 当前边界与后续事项
|
||
|
||
本次合并完成的是阶段 D 的宿主协议和开发期运行边界,不等于第三方插件已经具备生产级操作系统隔离。
|
||
|
||
当前仍保留以下后续事项:
|
||
|
||
- 阶段 C.5 按规划放在第二阶段功能与测试完成后、第三阶段桌面集成正式构建 Tauri/Rust 沙箱之前;
|
||
- 生产环境继续通过配置门禁拒绝未沙箱化的 MCP Host;
|
||
- 桌面端将 Plugin Secret 从开发期 Fernet 文件迁移到 Stronghold 或系统 Keychain,保持现有引用和 HTTP Contract;
|
||
- 前端已实现命令面板、Plugin 详情命令和动态 Settings / Secret 表单;上下文菜单与 Toolbar 挂载点仍复用现有 Service 继续扩展;
|
||
- Command 审计当前是 500 条有界内存队列,长期审计持久化需在后续阶段单独设计;
|
||
- 前端大 Chunk 应通过路由和 Markdown 依赖拆包处理,不与本次 Extension Contract 修改混合。
|
||
|
||
## 18. 复盘结论
|
||
|
||
本次最重要的经验是:插件系统的正确性不能只按“命令是否执行成功”判断。真正需要审阅的是数据从清单进入注册表、从 Settings 进入运行时、从 Secret Store 进入执行器、从宿主信封进入 MCP,以及从 MCP Effect 返回前端的每一道边界。
|
||
|
||
连续审阅避免了跨命名空间 Secret 访问、非事务删除、外部 Schema I/O、MCP 伪兼容和任意 Effect 等问题进入 `main`。最终实现把每个边界落到明确 Contract、稳定错误码和可失败的回归测试上,为下一阶段前端集成和桌面安全沙箱提供了可复用基础。
|