diff --git a/backend/app/extensions/runtime.py b/backend/app/extensions/runtime.py index a6f0730..6a5e918 100644 --- a/backend/app/extensions/runtime.py +++ b/backend/app/extensions/runtime.py @@ -633,6 +633,7 @@ class PluginRuntime: "command_id": _spec.command_id, "arguments": arguments, "context": context, + "settings": settings, "secrets": secret_values, } }, diff --git a/backend/extensions/fixtures/mcp-echo/server.py b/backend/extensions/fixtures/mcp-echo/server.py index ba61194..f9d480f 100644 --- a/backend/extensions/fixtures/mcp-echo/server.py +++ b/backend/extensions/fixtures/mcp-echo/server.py @@ -71,8 +71,10 @@ def call_tool(request_id: int, params: dict[str, Any]) -> None: envelope = arguments.get("_notesagent") or {} command_arguments = envelope.get("arguments") or {} context = envelope.get("context") or {} + settings = envelope.get("settings") or {} secrets = envelope.get("secrets") or {} message = command_arguments.get("message") or context.get("selection") or "" + message = f"{settings.get('message_prefix', '')}{message}" respond( request_id, { diff --git a/backend/extensions/fixtures/mcp-echo/settings.yaml b/backend/extensions/fixtures/mcp-echo/settings.yaml index dfc3aa9..3ddb40e 100644 --- a/backend/extensions/fixtures/mcp-echo/settings.yaml +++ b/backend/extensions/fixtures/mcp-echo/settings.yaml @@ -1,6 +1,10 @@ section_id: mcp-fixture.general schema_version: 1 fields: + - key: message_prefix + label: Message Prefix + type: string + default: "" - key: api_key label: Fixture API Key type: secret diff --git a/backend/tests/test_extension_core.py b/backend/tests/test_extension_core.py index 25539b8..b96f648 100644 --- a/backend/tests/test_extension_core.py +++ b/backend/tests/test_extension_core.py @@ -415,6 +415,9 @@ def test_mcp_command_target_receives_scoped_context_and_declared_secret( mcp_container.plugins.put_setting_secret( "mcp-fixture", "api_key", "mcp-command-secret" ) + mcp_container.plugins.update_settings( + "mcp-fixture", 1, {"message_prefix": "Fixture: "} + ) result = await mcp_container.plugins.execute_command( "mcp-fixture.notify", {}, @@ -427,7 +430,7 @@ def test_mcp_command_target_receives_scoped_context_and_declared_secret( assert result.effect.type == "notification" assert result.effect.payload == { "level": "success", - "message": "来自选区", + "message": "Fixture: 来自选区", "secret_configured": True, } assert "mcp-command-secret" not in repr( diff --git a/docs/contracts/第二阶段接口契约-开发版.md b/docs/contracts/第二阶段接口契约-开发版.md index c60cae3..8a64ace 100644 --- a/docs/contracts/第二阶段接口契约-开发版.md +++ b/docs/contracts/第二阶段接口契约-开发版.md @@ -546,7 +546,7 @@ error 需要 Secret 的 Command 必须在 `commands.yaml` 的内部 `secrets` 数组中声明对应 Setting Key,并在 Plugin Manifest 声明 `secrets.use` 权限。安装时宿主校验该字段确实属于当前 Plugin Settings Schema 的 `secret` 类型;只有权限已授予并启用后,运行时才向受控 handler 或 MCP Command Target 提供声明过的 Secret。未声明字段返回 `PLUGIN_SECRET_ACCESS_DENIED`,必填 Secret 未配置返回 `PLUGIN_SECRET_REQUIRED`。`secrets` 不属于前端 `PluginCommand` DTO,Secret 明文也不会并入普通 Settings 字典。 -`commands.yaml` 中的执行目标必须在宿主白名单 `handler` 与当前 Plugin 命名空间的 `mcp_tool` 之间二选一。MCP Command Target 不注册为 Agent Tool;宿主用 `_notesagent` 保留包装传入 Command ID、参数、裁剪后的 Context 和声明过的 Secret,并将 MCP structured result 再校验为白名单 effect。Command 与 Tool Schema 仅允许 `#...` 文档内引用,任何通过 `$ref` 或 `$dynamicRef` 指向文件、HTTP 或其他外部资源的 Schema 都会在注册前被拒绝。文档内引用遵循 Draft 2020-12 的嵌套 `$id` 与 Anchor 资源作用域,不能解析的引用不得进入运行时。 +`commands.yaml` 中的执行目标必须在宿主白名单 `handler` 与当前 Plugin 命名空间的 `mcp_tool` 之间二选一。MCP Command Target 不注册为 Agent Tool;宿主用 `_notesagent` 保留包装传入 Command ID、参数、裁剪后的 Context、已校验的非敏感 Settings 和声明过的 Secret,并将 MCP structured result 再校验为白名单 effect。Command 与 Tool Schema 仅允许 `#...` 文档内引用,任何通过 `$ref` 或 `$dynamicRef` 指向文件、HTTP 或其他外部资源的 Schema 都会在注册前被拒绝。文档内引用遵循 Draft 2020-12 的嵌套 `$id` 与 Anchor 资源作用域,不能解析的引用不得进入运行时。 ### 7.5 Settings Schema diff --git a/docs/development/Plugin-Command与Settings开发说明.md b/docs/development/Plugin-Command与Settings开发说明.md index 0daa95d..e2f755c 100644 --- a/docs/development/Plugin-Command与Settings开发说明.md +++ b/docs/development/Plugin-Command与Settings开发说明.md @@ -47,7 +47,7 @@ Plugin 在 `plugin.yaml` 的 `contributes.commands` 与 `contributes.settings_se 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 和声明过的 Secret;MCP Server 必须返回结构化的白名单 effect。远程原始错误不直接透传给 HTTP 调用方。插件仍不能把模块路径或 Shell 字符串作为执行器。 +MCP Command Target 是专用执行目标,不注册进 Agent `ToolRegistry`,因此模型无法绕过 Command 权限与 Context 裁剪直接调用。宿主通过 `_notesagent` 保留包装传入 `command_id`、已校验 arguments、已裁剪 Context、已校验的非敏感 Settings 和声明过的 Secret;MCP Server 必须返回结构化的白名单 effect。远程原始错误不直接透传给 HTTP 调用方。插件仍不能把模块路径或 Shell 字符串作为执行器。 Command 与 Tool 的 JSON Schema 只允许当前文档内的 Fragment 引用(`#...`);宿主在注册前递归拒绝 `$ref` / `$dynamicRef` 指向的文件、HTTP 或其他外部资源,避免 Schema 校验触发未授权 I/O。文档内引用使用 Draft 2020-12 Resource Resolver 预检,嵌套 `$id` 创建的新资源及其 Anchor 按各自作用域解析,无法解析的引用在注册阶段返回稳定错误。