fix(extension): 收紧插件命令运行时契约

This commit is contained in:
2026-09-02 20:03:39 +08:00
parent eb3464b522
commit d39ae727c1
10 changed files with 466 additions and 43 deletions
@@ -40,14 +40,14 @@ Plugin 在 `plugin.yaml` 的 `contributes.commands` 与 `contributes.settings_se
3. 根据 `when` 检查必要上下文;
4. 仅向执行器传递声明过的 Context 字段;
5. 在超时范围内调用宿主受控 handler,或调用独立的 MCP Command Target
6. 校验 effect 类型、可序列化性和 64 KiB 大小上限;
6. effect 类型校验专属 payload、可序列化性和 64 KiB 大小上限;
7. 返回统一 `PluginCommandResult`
首批 effect 为 `none``notification``navigate``refresh``job`。前端不得把 effect 当作任意代码执行。
首批 effect 为 `none``notification``navigate``refresh``job`后端分别限制通知级别与消息、宿主路由名、刷新范围和 Job IDPydantic 与 TypeScript 均使用同一判别语义,前端不得把 effect 当作任意代码执行。
Command 执行器通过受控 Resolver 按需读取 `commands.yaml` 已声明且确实属于当前 Plugin Schema 的 Secret;使用 Secret 的 Plugin 还必须声明并获授 `secrets.use` 权限。读取未声明字段返回 `PLUGIN_SECRET_ACCESS_DENIED`,必填 Secret 未配置则返回 `PLUGIN_SECRET_REQUIRED`。Secret 不会并入普通 Settings 字典。Command 审计使用 500 条有界内存队列,仅保留 `command_id``plugin_id`、成功/失败状态、耗时、错误码和时间。arguments、正文选区、文件路径、effect 与 Secret 均不进入审计事件。
MCP Command Target 是专用执行目标,不注册进 Agent `ToolRegistry`,因此模型无法绕过 Command 权限与 Context 裁剪直接调用。宿主通过 `_notesagent` 保留包装传入 `command_id`、已校验 arguments、已裁剪 Context、已校验的非敏感 Settings 和声明过的 SecretMCP Server 必须返回结构化的白名单 effect。远程原始错误不直接透传给 HTTP 调用方。插件仍不能把模块路径或 Shell 字符串作为执行器。
MCP Command Target 是专用执行目标,不注册进 Agent `ToolRegistry`,因此模型无法绕过 Command 权限与 Context 裁剪直接调用。宿主通过 `_notesagent` 保留包装传入 `command_id`、已校验 arguments、已裁剪 Context、已校验的非敏感 Settings 和声明过的 Secret。启用阶段只检查目标 `inputSchema` 在顶层 `properties` 中直接声明 `_notesagent: { type: object }`,不自行求解 JSON Schema,也不用空对象伪造业务数据;引用和组合约束可以放在 `_notesagent` 对象内部。执行阶段再用保留的完整 Schema 和官方 Draft 2020-12 Validator 校验真实信封。MCP Server 必须返回结构化的白名单 effect。远程原始错误不直接透传给 HTTP 调用方。插件仍不能把模块路径或 Shell 字符串作为执行器。
Command 与 Tool 的 JSON Schema 只允许当前文档内的 Fragment 引用(`#...`);宿主在注册前递归拒绝 `$ref` / `$dynamicRef` 指向的文件、HTTP 或其他外部资源,避免 Schema 校验触发未授权 I/O。文档内引用使用 Draft 2020-12 Resource Resolver 预检,嵌套 `$id` 创建的新资源及其 Anchor 按各自作用域解析,无法解析的引用在注册阶段返回稳定错误。
@@ -67,6 +67,8 @@ APP_DATA_DIR/plugins/settings.json
Secret 写入必须调用专用端点。后端通过 `SecretStr` 接收明文,再交给现有 `EncryptedCredentialStore`;普通 Settings API 只返回 `{ configured: true|false }`,不会返回 Secret 值。`plugin.*` 是保留命名空间,通用凭据 API、Provider 配置、Provider 临时测试凭据和 Provider Resolver 均不得访问,防止覆盖、删除或外发 Plugin Secret。卸载 Plugin 时同时删除普通设置命名空间和对应加密凭据。
没有默认值的 `required` 普通字段必须在启用 Plugin 前配置。Enable 和每次 Command Execute 都会重新检查有效设置;缺失时返回 `PLUGIN_SETTINGS_REQUIRED`,不启动 MCP Host,也不调用 Command handler。
读取持久化引用时,宿主会重新计算并核对 `plugin.<sha256(...)>`,引用不匹配即按损坏存储拒绝处理,不能借由篡改 `settings.json` 读取或删除 Provider 等其他命名空间的凭据。删除单个 Secret 或卸载 Plugin 时先原子更新 Settings 引用,再删除加密凭据;底层删除失败会恢复原引用。多 Secret 卸载使用一次凭据表原子替换,避免分批删除部分删除。
开发阶段凭据文件由本机 Fernet Key 加密。桌面端落地后,应由 Tauri Host 将同一引用语义迁移到 Stronghold 或系统 KeychainHTTP Contract 无需因此改变。
@@ -90,7 +92,7 @@ DELETE /api/plugins/{plugin_id}/settings/{key}/secret
- Command 未注册、冲突、参数或 Context 无效;
- 执行超时、执行器异常、effect 无效或过大;
- Settings Schema 无效、版本冲突、字段类型/边界错误;
- Settings Schema 无效、版本冲突、字段类型/边界错误或运行时必填值缺失
- Secret 字段不存在、空 Secret、凭据存储异常;
- Settings JSON 根结构或 Plugin 命名空间损坏。