From e37ac7b0a474b5027a488286289df20b3220557b Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Tue, 1 Sep 2026 21:25:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(extension):=20=E5=BC=BA=E5=8C=96=20MCP=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E4=B8=8E=E7=94=9F=E4=BA=A7=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- backend/README.md | 2 +- backend/app/container.py | 10 ++++-- backend/app/extensions/runtime.py | 36 +++++++++---------- backend/tests/test_extension_core.py | 29 +++++++++++++-- .../AI笔记软件技术栈说明-团队版-v2.3.md | 4 ++- docs/contracts/后端接口契约-开发版.md | 2 +- docs/contracts/第二阶段接口契约-开发版.md | 7 ++-- .../AI-Core与Agent-Core开发说明.md | 2 +- .../Knowledge与Retrieval-Core开发说明.md | 2 +- .../MCP-Bridge与Plugin-Host开发说明.md | 6 ++-- docs/development/前端壳子与接口层开发说明.md | 4 +-- .../模型提供商与模型发现开发说明.md | 2 +- .../后端全面审阅问题与修复复盘.md | 2 +- 14 files changed, 72 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index ac7a7db..10d735b 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ cd frontend pnpm test ``` -当前回归基线为后端 91 项测试、前端 27 项测试,且生产构建通过。测试数量会随功能增长,以本地实际输出和 CI 为准。 +当前回归基线为后端 92 项测试、前端 27 项测试,且生产构建通过。测试数量会随功能增长,以本地实际输出和 CI 为准。 构建产物位于 `frontend/dist`,该目录不提交到 Git。 diff --git a/backend/README.md b/backend/README.md index 6a0930e..3d4fde4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -23,7 +23,7 @@ uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 uv run pytest ``` -当前基线为 91 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY`、`DEEPSEEK_API_KEY` 或 `AINOTE_CREDENTIAL_` 注入;不要把真实密钥写入仓库。 +当前基线为 92 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY`、`DEEPSEEK_API_KEY` 或 `AINOTE_CREDENTIAL_` 注入;不要把真实密钥写入仓库。 团队接口清单见 `../docs/contracts/后端接口契约-开发版.md`,机器可读契约以运行时的 `/openapi.json` 为准。 diff --git a/backend/app/container.py b/backend/app/container.py index 0cff70a..9263ca9 100644 --- a/backend/app/container.py +++ b/backend/app/container.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from app.agent import AgentRuntime, PermissionManager, PermissionPolicy, ToolRegistry from app.agent.builtin_tools import register_builtin_tools from app.contracts import ModelCapability, ProviderConfig, ProviderType -from app.config import BACKEND_DIR +from app.config import BACKEND_DIR, get_settings from app.extensions import PluginRuntime, SkillRuntime from app.providers import MockProvider, ProviderFactory, ProviderRegistry from app.providers.credentials import ( @@ -26,6 +26,7 @@ class ApplicationContainer: def build_container() -> ApplicationContainer: + settings = get_settings() credentials = EncryptedCredentialStore() provider_factory = ProviderFactory( ChainedCredentialResolver(credentials, EnvironmentCredentialResolver()) @@ -50,7 +51,12 @@ def build_container() -> ApplicationContainer: tools = ToolRegistry() register_builtin_tools(tools) - plugins = PluginRuntime(tools) + plugins = PluginRuntime( + tools, + # 当前 Python Host 尚无 OS 沙箱。生产构建必须保持关闭,直到 + # Tauri/Rust Host 能签发绑定命令摘要的可信启动许可。 + allow_unsandboxed_mcp=settings.environment == "development", + ) plugins.install(BACKEND_DIR / "extensions" / "plugins" / "text-tools") plugins.enable("text-tools") diff --git a/backend/app/extensions/runtime.py b/backend/app/extensions/runtime.py index cacbb12..832b726 100644 --- a/backend/app/extensions/runtime.py +++ b/backend/app/extensions/runtime.py @@ -256,10 +256,13 @@ class PluginRuntime: tools: ToolRegistry, host: DeclarativePluginHost | None = None, mcp_bridge: McpBridge | None = None, + *, + allow_unsandboxed_mcp: bool = False, ) -> None: self.registry = tools self.host = host or DeclarativePluginHost() self.mcp = mcp_bridge or McpBridge() + self.allow_unsandboxed_mcp = allow_unsandboxed_mcp self._records: dict[str, _PluginRecord] = {} self._lock = threading.RLock() @@ -346,6 +349,16 @@ class PluginRuntime: status_code=409, details={"plugin_id": plugin_id, "permissions": missing_grants}, ) + if ( + record.plugin.manifest.backend.type == "mcp" + and not self.allow_unsandboxed_mcp + ): + raise ExtensionError( + "MCP_TRUST_APPROVAL_REQUIRED", + "Unsandboxed MCP Hosts are disabled outside development mode.", + status_code=403, + details={"plugin_id": plugin_id}, + ) declared_tools = list(record.plugin.manifest.contributes.tools) conflicts = [name for name in declared_tools if self.registry.contains(name)] if conflicts: @@ -665,27 +678,10 @@ def _arguments_model_from_schema( ) -> type[BaseModel]: if schema.get("type", "object") != "object": raise ExtensionError("PLUGIN_TOOL_SCHEMA_INVALID", "Tool parameters must be an object schema.") - properties = schema.get("properties", {}) - required = set(schema.get("required", [])) - fields: dict[str, tuple[Any, Any]] = {} - types = { - "string": str, - "number": float, - "integer": int, - "boolean": bool, - "array": list[Any], - "object": dict[str, Any], - } - for name, field_schema in properties.items(): - schema_type = field_schema.get("type") - # JSON Schema 允许联合类型数组;复杂类型继续由 Draft Validator - # 精确校验,Pydantic 在这里只承担参数载体职责。 - annotation = types.get(schema_type, Any) if isinstance(schema_type, str) else Any - fields[name] = (annotation, ... if name in required else None) model_name = "PluginArgs_" + re.sub(r"\W+", "_", tool_name) - # 完整 JSON Schema 已在 ToolRegistry 中先行校验。这里允许额外字段,避免 - # Pydantic 再次拒绝 additionalProperties/patternProperties 接受的合法参数。 - return create_model(model_name, __config__=ConfigDict(extra="allow"), **fields) + # 完整 JSON Schema 已在 ToolRegistry 中先行校验。参数载体不重复声明字段, + # 从而完整保留 model_dump、连字符键、联合类型和动态属性等合法 JSON 键值。 + return create_model(model_name, __config__=ConfigDict(extra="allow")) def _validate_tool_schema(spec: DeclarativeToolSpec) -> None: diff --git a/backend/tests/test_extension_core.py b/backend/tests/test_extension_core.py index 5b368aa..29a2d92 100644 --- a/backend/tests/test_extension_core.py +++ b/backend/tests/test_extension_core.py @@ -509,13 +509,38 @@ def test_mcp_argument_model_preserves_json_schema_additional_properties() -> Non "mcp-fixture.dynamic", { "type": "object", + "properties": {"model_dump": {"type": "string"}}, + "required": ["model_dump"], "additionalProperties": {"type": "string"}, }, ) - arguments = arguments_model.model_validate({"dynamic_key": "value"}) + arguments = arguments_model.model_validate( + {"model_dump": "method name remains data", "dynamic-key": "value"} + ) - assert arguments.model_dump() == {"dynamic_key": "value"} + assert arguments.model_dump() == { + "model_dump": "method name remains data", + "dynamic-key": "value", + } + + +def test_production_rejects_unsandboxed_mcp_host(monkeypatch) -> None: + monkeypatch.setenv("APP_ENVIRONMENT", "production") + get_settings.cache_clear() + container = build_container() + installed = container.plugins.install(MCP_FIXTURE) + assert installed.status == "permission_required" + container.plugins.set_permissions("mcp-fixture", ["notes.read"]) + try: + with pytest.raises(ExtensionError) as exc: + container.plugins.enable("mcp-fixture") + assert exc.value.code == "MCP_TRUST_APPROVAL_REQUIRED" + assert container.plugins.get_host_status("mcp-fixture").status == "stopped" + assert not container.tools.contains("mcp-fixture.echo") + finally: + container.plugins.shutdown() + get_settings.cache_clear() def test_mcp_abnormal_exit_unregisters_tools_and_restart_recovers(mcp_container) -> None: diff --git a/docs/architecture/AI笔记软件技术栈说明-团队版-v2.3.md b/docs/architecture/AI笔记软件技术栈说明-团队版-v2.3.md index dc4ad1a..47b6fe7 100644 --- a/docs/architecture/AI笔记软件技术栈说明-团队版-v2.3.md +++ b/docs/architecture/AI笔记软件技术栈说明-团队版-v2.3.md @@ -1044,6 +1044,8 @@ Python 包形式的 MCP Server 推荐使用固定版本的 `uvx --isolated --fro 面向社区或不可信 Plugin 开放前,Tauri/Rust Host 必须增加平台级沙箱、完整进程树回收、包来源/签名校验,并在首次安装或命令变化时向用户完整展示 executable 和参数、要求明确同意。当前 Python Host 的独立进程、环境裁剪和 Permission 只用于可信开发联调,不能替代这些生产安全门槛。 +在该门槛完成前,后端仅允许 `APP_ENVIRONMENT=development` 启动未沙箱化 MCP Host;生产环境统一返回 `MCP_TRUST_APPROVAL_REQUIRED`。Python `uvx` Server 在开发模式首次运行可能联网解析依赖,生产版本必须在安装/更新阶段预取并验证固定版本,正常运行阶段只使用已经准备好的环境。 + ### 12.5 MCP Bridge MCP Bridge 用于接入具有 MCP Server 接口的插件或外部工具服务。 @@ -2327,7 +2329,7 @@ Markdown Workspace 第一阶段 Plugin Runtime 已完成安装、启用、停用、权限和声明式 Tool 注册,建立 Skill 调用 Plugin Tool 的基础链路。Command、Settings 和 MCP 执行不计入第一阶段完成项。 -截至 2026-09-01,上述第一阶段后端链路和 Web 联调前端均已完成;第二阶段前置的 Workspace 去 Mock 联调、Agent Trace 持久化/恢复接口以及 stdio MCP Bridge / Plugin Host 也已完成。当前验证基线为后端 91 项测试、前端 27 项测试及生产构建通过。向量链路当前使用 `HashEmbeddingProvider` 验证工程正确性,真实 Embedding 召回质量不属于该测试结论。 +截至 2026-09-01,上述第一阶段后端链路和 Web 联调前端均已完成;第二阶段前置的 Workspace 去 Mock 联调、Agent Trace 持久化/恢复接口以及 stdio MCP Bridge / Plugin Host 也已完成。当前验证基线为后端 92 项测试、前端 27 项测试及生产构建通过。向量链路当前使用 `HashEmbeddingProvider` 验证工程正确性,真实 Embedding 召回质量不属于该测试结论。 第二阶段在既有 Contract 上接入: diff --git a/docs/contracts/后端接口契约-开发版.md b/docs/contracts/后端接口契约-开发版.md index ea080e8..099a666 100644 --- a/docs/contracts/后端接口契约-开发版.md +++ b/docs/contracts/后端接口契约-开发版.md @@ -176,7 +176,7 @@ RunCancelled ## 当前实现状态 -更新至 2026-09-01:后端 91 项回归测试通过。 +更新至 2026-09-01:后端 92 项回归测试通过。 - Chat、Agent Run、Agent Events、Tool 列表、Provider 配置生命周期、模型列表和连接测试已经接入 AI Core。 - Agent Run/Event 已持久化到 SQLite;SSE 帧携带 sequence `id`,断线后可以回放缺失事件。Trace API 与 Benchmark 共用同一事件事实,并在入库前执行 Secret 脱敏和结果限长。 diff --git a/docs/contracts/第二阶段接口契约-开发版.md b/docs/contracts/第二阶段接口契约-开发版.md index 6c70504..68e0d9b 100644 --- a/docs/contracts/第二阶段接口契约-开发版.md +++ b/docs/contracts/第二阶段接口契约-开发版.md @@ -437,11 +437,13 @@ backend: transport: stdio command: uvx args: [--isolated, --from, example-mcp==1.2.3, example-mcp] - startup_timeout_seconds: 5 + startup_timeout_seconds: 60 tool_timeout_seconds: 30 ``` -命令通过参数数组直接启动,不经过 Shell。带路径的 executable 必须位于 Plugin 包内;PATH 中的命令可以按名称引用。Python 包形式的 MCP 推荐使用固定版本的 `uvx --isolated --from`,但 `uvx` 只隔离依赖而不是文件/网络/系统调用安全沙箱,非 Python Server 不强制使用。子进程只继承运行所需的系统环境变量,不继承 `OPENAI_API_KEY`、`APP_DB_PATH`、Vault 路径等宿主状态。Secret 注入留给阶段 D 的专用引用接口。 +命令通过参数数组直接启动,不经过 Shell。带路径的 executable 必须位于 Plugin 包内;PATH 中的命令可以按名称引用。Python 包形式的 MCP 推荐使用固定版本的 `uvx --isolated --from`,但 `uvx` 只隔离依赖而不是文件/网络/系统调用安全沙箱,非 Python Server 不强制使用。开发模式首次运行未缓存的 uvx 包可能联网解析,因此 startup 示例使用 60 秒;生产安装阶段必须预取并验证,运行阶段不得临时解析依赖。子进程只继承运行所需的系统环境变量,不继承 `OPENAI_API_KEY`、`APP_DB_PATH`、Vault 路径等宿主状态。Secret 注入留给阶段 D 的专用引用接口。 + +在平台沙箱和可信命令许可完成前,`APP_ENVIRONMENT != development` 时启用 MCP Plugin 必须返回 `403 MCP_TRUST_APPROVAL_REQUIRED`,不得启动进程或注册 Tool。该门禁由后端执行,不能只依赖前端提示或文档约定。 远端 Tool 的可选项目权限放在 MCP `_meta`: @@ -632,6 +634,7 @@ MCP_CAPABILITY_UNSUPPORTED MCP_TOOL_SCHEMA_INVALID MCP_TOOL_CALL_FAILED MCP_TOOL_RESULT_TOO_LARGE +MCP_TRUST_APPROVAL_REQUIRED PLUGIN_COMMAND_NOT_FOUND PLUGIN_COMMAND_CONTEXT_INVALID PLUGIN_SETTINGS_SCHEMA_INVALID diff --git a/docs/development/AI-Core与Agent-Core开发说明.md b/docs/development/AI-Core与Agent-Core开发说明.md index 4bc716f..fc79e05 100644 --- a/docs/development/AI-Core与Agent-Core开发说明.md +++ b/docs/development/AI-Core与Agent-Core开发说明.md @@ -2,7 +2,7 @@ > 本文档用于团队开发和模块联调,记录当前已经落地的核心边界与使用方式。 -> 更新日期:2026-09-01。第一阶段 AI Core、Agent Core、Extension Core 和 Model Core 主链路已经完成;第二阶段 Agent Trace 持久化、可恢复 SSE、stdio MCP Bridge 与隔离 Plugin Host 已落地,后端当前回归基线为 91 项测试通过。 +> 更新日期:2026-09-01。第一阶段 AI Core、Agent Core、Extension Core 和 Model Core 主链路已经完成;第二阶段 Agent Trace 持久化、可恢复 SSE、stdio MCP Bridge 与隔离 Plugin Host 已落地,后端当前回归基线为 92 项测试通过。 ## 当前实现 diff --git a/docs/development/Knowledge与Retrieval-Core开发说明.md b/docs/development/Knowledge与Retrieval-Core开发说明.md index 24f2e63..8aee584 100644 --- a/docs/development/Knowledge与Retrieval-Core开发说明.md +++ b/docs/development/Knowledge与Retrieval-Core开发说明.md @@ -3,7 +3,7 @@ > 本文档用于团队开发和模块联调,记录 Knowledge Core / Retrieval Core 已经落地的 > 模块边界、数据模型、接口与使用方式,对应分工表中的杨星萱。 -> 更新日期:2026-09-01。第一阶段 Knowledge/Retrieval 主链路已经完成,并已接入 Agent Tool Registry;完整后端回归基线为 91 项测试通过。 +> 更新日期:2026-09-01。第一阶段 Knowledge/Retrieval 主链路已经完成,并已接入 Agent Tool Registry;完整后端回归基线为 92 项测试通过。 ## 当前实现 diff --git a/docs/development/MCP-Bridge与Plugin-Host开发说明.md b/docs/development/MCP-Bridge与Plugin-Host开发说明.md index 7514142..bd43a94 100644 --- a/docs/development/MCP-Bridge与Plugin-Host开发说明.md +++ b/docs/development/MCP-Bridge与Plugin-Host开发说明.md @@ -69,7 +69,7 @@ backend: transport: stdio command: uvx args: [--isolated, --from, example-mcp==1.2.3, example-mcp] - startup_timeout_seconds: 5 + startup_timeout_seconds: 60 tool_timeout_seconds: 30 ``` @@ -160,7 +160,7 @@ MCP Tool: 当前隔离是“独立进程 + 协议边界”,不是完整的操作系统沙箱。 -`uvx` 解决的是 Python 工具依赖隔离:它等价于 `uv tool run`,在 uv 缓存中使用可丢弃的独立虚拟环境。它不会限制 Server 读取用户文件、访问网络、创建子进程或调用系统 API,因此不能代替安全沙箱。首次解析尚未缓存的包还可能访问包索引;生产清单必须固定来源和版本,安装/更新阶段与运行阶段分离。 +`uvx` 解决的是 Python 工具依赖隔离:它等价于 `uv tool run`,在 uv 缓存中使用可丢弃的独立虚拟环境。它不会限制 Server 读取用户文件、访问网络、创建子进程或调用系统 API,因此不能代替安全沙箱。当前开发模式下,首次 `enable` 尚未缓存的包可能访问包索引,因此示例使用 60 秒启动上限;生产实现不得依赖该行为,必须在用户确认后的安装/更新阶段预取和验证固定版本,运行阶段只启动已准备好的环境。 已经执行的保护: @@ -185,6 +185,8 @@ MCP Tool: 在这些门槛完成前,当前 MCP Host 只适用于内置 Fixture、团队可信插件和开发联调;不得把它描述为可以安全执行任意社区代码。上述安装确认要求遵循 MCP [SEP-1024](https://modelcontextprotocol.io/seps/1024-mcp-client-security-requirements-for-local-server-);`uvx` 行为依据 uv 官方 [Using tools](https://docs.astral.sh/uv/guides/tools/) 文档。 +后端通过 `APP_ENVIRONMENT` 强制该边界:只有 `development` 可以启动当前未沙箱化的 MCP Host;其他环境返回 `403 MCP_TRUST_APPROVAL_REQUIRED`,且不会创建进程或注册 Tool。后续 Tauri/Rust Host 提供沙箱与绑定完整命令摘要的可信许可后,再替换此临时门禁。 + ## 7. Host API ```http diff --git a/docs/development/前端壳子与接口层开发说明.md b/docs/development/前端壳子与接口层开发说明.md index b943dee..40a5dd3 100644 --- a/docs/development/前端壳子与接口层开发说明.md +++ b/docs/development/前端壳子与接口层开发说明.md @@ -187,12 +187,12 @@ pnpm build ```text pnpm build passed pnpm test 27 passed -uv run pytest 91 passed +uv run pytest 92 passed preview smoke HTTP 200 git diff --check passed ``` -当前前端使用 Vitest 执行 Store、Workspace API Adapter、SSE 恢复游标、文件树、编辑器组件、智能体标签、轻量动效约束、Markdown 对比度 Token、scoped CSS 选择器约束和 Shiki GitHub 双主题测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 91 项测试结果,也不涉及产品代码。 +当前前端使用 Vitest 执行 Store、Workspace API Adapter、SSE 恢复游标、文件树、编辑器组件、智能体标签、轻量动效约束、Markdown 对比度 Token、scoped CSS 选择器约束和 Shiki GitHub 双主题测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 92 项测试结果,也不涉及产品代码。 Vite 当前会提示 Chat 与 Workspace 的部分异步 Chunk 超过 500 kB,这是 Milkdown、CodeMirror、KaTeX 和 Shiki 等编辑/渲染依赖带来的性能优化项,不影响构建成功或功能正确性;进入桌面打包前应通过手动分包或更细粒度动态加载继续优化。 diff --git a/docs/development/模型提供商与模型发现开发说明.md b/docs/development/模型提供商与模型发现开发说明.md index 54cfae0..598ede8 100644 --- a/docs/development/模型提供商与模型发现开发说明.md +++ b/docs/development/模型提供商与模型发现开发说明.md @@ -104,4 +104,4 @@ pnpm build 自动化验证覆盖 Provider 预设、OpenAI-Compatible `/models` 请求与鉴权头、模型映射、前端自动刷新、排序去重及按 Provider 隔离错误。生产构建同时执行 Vue 和 TypeScript 类型检查。 -当前完整回归基线:后端 91 项测试、前端 27 项测试通过,前端类型检查和生产构建通过。Provider 配置目前仍保存在内存 Registry,AI Core 重启后需要重新创建;凭据密文会保留。OpenAI Responses 与 Anthropic Messages Adapter 尚未实现,设置页正式预设不会使用这两种协议。 +当前完整回归基线:后端 92 项测试、前端 27 项测试通过,前端类型检查和生产构建通过。Provider 配置目前仍保存在内存 Registry,AI Core 重启后需要重新创建;凭据密文会保留。OpenAI Responses 与 Anthropic Messages Adapter 尚未实现,设置页正式预设不会使用这两种协议。 diff --git a/docs/retrospectives/后端全面审阅问题与修复复盘.md b/docs/retrospectives/后端全面审阅问题与修复复盘.md index fe03208..59fa47e 100644 --- a/docs/retrospectives/后端全面审阅问题与修复复盘.md +++ b/docs/retrospectives/后端全面审阅问题与修复复盘.md @@ -4,7 +4,7 @@ > 审阅范围:FastAPI、Knowledge / Retrieval Core、Agent Core、Extension Core、Provider Adapter、公共接口和后端开发文档。 > 文档用途:记录问题形成原因、实际影响、修复判断和落地方案,供后续开发文档、比赛材料与技术博客使用。 -> 2026-09-01 状态补充:本文记录的缺陷均保持修复。此后又加入 Provider 预设、模型发现、DeepSeek/OpenAI 凭据解析、Fernet 加密存储、Agent Trace 持久化和 stdio MCP Plugin Host,当前完整后端回归基线为 91 项测试通过。 +> 2026-09-01 状态补充:本文记录的缺陷均保持修复。此后又加入 Provider 预设、模型发现、DeepSeek/OpenAI 凭据解析、Fernet 加密存储、Agent Trace 持久化和 stdio MCP Plugin Host,当前完整后端回归基线为 92 项测试通过。 ## 1. 审阅结论