fix(desktop): isolate invalid optional MCP tools - #3366
Conversation
Keep the Desktop Runtime Host candidate usable when one optional MCP tool publishes an invalid schema, while retaining fail-closed behavior for required native capabilities. Cover AI SDK JSON Schema invocation and rate-limit terminal admission recovery. Generated-by: Codex
jackwener
left a comment
There was a problem hiding this comment.
深入 review 后需要修改再合入。
两个阻断问题:
- optional MCP tool 的过滤只验证单个 tool,无法保证最终 capability manifest 仍满足单 offer 64 tools、总 manifest 56 KiB 等协议约束,candidate 仍会因完整注册失败。
- AI SDK
jsonSchema()默认没有validate,当前 fallback 会直接放行参数;这使 Desktop 与 CodeMode 的 JSON Schema 执行契约不一致,非法参数会在 admission 后发往 MCP server。
建议以完整 manifest 作为唯一验证单位,确定性地加入 optional tool;并抽取 Runtime 共用的 JSON Schema validation adapter。另请将无 production change 对应的 rate-limit test 拆出本 PR。
| try { | ||
| decodeClientCapabilityReplaceInput({ | ||
| registrationId: "desktop_capability_validation", | ||
| offers: [capabilityOffer({ ...group, tools: [tool] }, hostPathAccess)], |
There was a problem hiding this comment.
这里每次只对一个 tool 构造 offer 并 decode,但最终会把整个 MCP group 一次性注册。协议的 CLIENT_CAPABILITY_MAX_TOOLS_PER_OFFER(64)和 56 KiB manifest 限制都是跨 tool 的:65 个各自合法的 MCP tool,或 3 个各约 20 KiB 的 schema,都会分别在最终注册时失败。请按最终完整 manifest 做增量校验并确定性地省略不能加入的 optional tool,否则 candidate 仍会被 optional MCP 工具拖垮。
| typeof parameters.jsonSchema === "object" && | ||
| !Array.isArray(parameters.jsonSchema) | ||
| ) { | ||
| return value; |
There was a problem hiding this comment.
buildMcpTools() 用的是 AI SDK jsonSchema(descriptor.inputSchema);AI SDK 将 validate 定义为 optional,默认正是 undefined。因此这个分支会把例如缺少 required 字段的参数直接放行,并在 options.accept() 后调用 MCP。CodeMode 已对同一类 schema 用 Ajv fallback 验证。请复用/抽取那条 Runtime 级 validation path,而不是在 Desktop 绕过 schema 的执行契约。
| }); | ||
| }); | ||
|
|
||
| test('a rate-limited root Turn releases admission before regenerate', async () => { |
There was a problem hiding this comment.
本 PR 没有对应的 Runtime Host production change;该 test 在 main 上也应成立,且与 optional MCP schema isolation 无关。请从此 PR 移除,若确有独立 rate-limit regression,再用 focused PR 提交,避免继续扩张较重的 execution-host suite。
|
Addressed the requested changes in 2bdd7f9. Optional MCP tools are now admitted deterministically only when the complete manifest decodes successfully, covering the 64-tool and 56 KiB limits. Desktop now uses the Runtime Ajv JSON Schema adapter before accept/MCP dispatch, with a regression test proving invalid arguments are neither accepted nor invoked. The unrelated rate-limit Runtime Host test has been removed. |
Validate optional MCP tools against the complete capability manifest, reuse Runtime JSON Schema validation before admission, and remove unrelated Runtime Host coverage. Generated-by: Codex
d593184 to
2bdd7f9
Compare
Use a temporary Desktop manifest version while building the Windows auto-update installer so the PE product version matches the update feed, then restore the source manifest. Generated-by: Codex
|
修复 Windows package check 的自动更新失败:electron-builder 的 �xtraMetadata.version 只更新应用包内 manifest,不会更新 PE ProductVersion。提交 209ffe0 在构建 version-bumped installer 时临时写入 Desktop manifest,构建结束后(含失败路径)恢复原文件,使 ProductVersion 与 latest.yml 一致。请重新检查 Windows package job。 |
jackwener
left a comment
There was a problem hiding this comment.
Independent review of exact head 60c26e80b9779d8d729467b9a0fed6f28f6f8b15.
三问
- 要解决什么:一条非法的可选 MCP 工具会让整次 Client Capability replace 失败,连 Browser / Computer Use 等必选 offer 一起带走。
- 怎么解决:只给
desktop_mcp开invalidToolPolicy: "omit";prepareCapabilityGroups按完整 selected manifest 调decodeClientCapabilityReplaceInput,能过才收下这把工具;必选 group 在 omit 循环前先 decode,失败直接抛。调度时用抽出来的validateJsonSchemaInput在accept()之前验参。 - 奥卡姆:隔离本身是小而准的。同一 PR 里的 Windows autoupdate 打包/校验改写是另一件事(分支还叫
fix/rate-limit-session-recovery)。
隔离判定边界
「无效」在这里等于「这把工具会让 Host 拒掉 replace」,不是 Ajv 能不能编译 schema。
- 会拦:单 offer 64 把、总 256、manifest 56KiB、单 schema 32KiB、keyword allowlist、未解析的本地
$ref、根 schema 不是type: "object"、重复 identity。 - 贪心前缀:排在后面、协议上合法的工具,可能只因为预算被丢掉。非法工具不会污染后面的兄弟(UDS 测试:
mcp_invalid再mcp_valid)。 patternProperties/not/if/then/else/ OpenAPInullable这类常见 JSON Schema 会被 omit。这是 Host 协议的门,不是这 PR 新发明的;相对 Host 不是误判,相对「MCP 服务器认为有效」会静默少工具。- Ajv 不在 admission。协议过了就会 offer;调用时 compile/校验失败 fail-closed。
compileJsonSchema返回undefined时仍会放行输入。
降级
- 必选(browser / CU / settings / rive):fail-closed。
- 可选 MCP:omit 后会话继续,空 group 丢掉,
onInvalidTool→reportError不抛。对那把坏工具是 fail-closed,对会话是 fail-open。可选能力这样是对的。 - 调度:非法参数在
accept()前拒绝。MCP + AI SDKjsonSchema()缺validate那条旧洞,这版 fallback 到 Ajv,测试用{}打 requiredquery能拦住。
CI(按 path filter)
期望:test(永远)、audit(动了 packages/runtime/package.json)、package(runtime-host-boot.ts + windows scripts)。不会跑 windows_sandbox_*。
test:completed / failure —format:check挂在scripts/package-windows-autoupdate-next.mjsaudit:completed / successpackage:仍 in_progress
旧的 jackwener REQUEST_CHANGES(bf667a1e)那两条——按单工具 decode、以及 jsonSchema() 无 validate 放行参数——在这个 head 上已经补上。本条是 COMMENT,不是 REQUEST_CHANGES。
NO-GO。 有未闭 [P2],且 required test 不是 completed+success。未 approve,未合。
| // `extraMetadata` only changes the packaged app manifest, so temporarily | ||
| // update every source manifest for the build and restore them even when | ||
| // electron-builder fails. | ||
| const [originalRootManifest, originalDesktopManifest, originalCliManifest] = |
There was a problem hiding this comment.
[P2] 这四份 Windows autoupdate 脚本(package-windows-autoupdate-next.mjs / verify-windows-autoupdate.mjs / verify-windows-harness.test.mjs / verify-windows-x64.mjs)跟「隔离无效的可选 MCP 工具」不是一件事。这里会在 next-installer 构建期间把 root / desktop / CLI 三份 package.json 的 version 写盘,再在 finally 里还原;硬杀构建时 restore 走不到,工作树会停在被改过的 version 上。
同一处也是 required test 红灯的唯一原因:format:check 挂在这文件第 94 行。隔离逻辑本身没挂 format。所以不是「格式坏了顺手修一下就能合」——夹带的改动既超出了本 PR 声明的范围,又是门禁红的原因。
请把这四份 Windows 脚本从本 PR 摘出去,单独开 PR。不要只在这里 biome format 后继续带着它们。
| selectedOptionalTools.set(group, tools); | ||
| onInvalidTool?.( | ||
| new Error( | ||
| `Invalid optional Desktop capability tool omitted: ${group.offerId}/${tool.name}`, |
There was a problem hiding this comment.
[P3] 这里的判定单位是「整份 selected manifest 还能不能 decode」,不是「这把工具自己非法」。贪心前缀对 64/56KiB 是对的,非法工具也不会污染后面的兄弟。
但任何 decode 失败——包括第 65 把完全合法的工具、或只是把 manifest 顶过 56KiB——都会打成 Invalid optional Desktop capability tool omitted。文案会把预算溢出说成无效。
Host keyword allowlist 比 JSON Schema 窄(patternProperties / not / if/then/else / OpenAPI nullable),这些 MCP 工具会被 omit。相对 Host 这不是误判;相对 MCP 服务器会静默少工具。Ajv 仍只在 dispatch,不在 admission。
| /** Validate an input against a provider JSON Schema when the schema is compilable. */ | ||
| export function validateJsonSchemaInput(schema: unknown, input: unknown): unknown { | ||
| const validator = compileJsonSchema(schema); | ||
| if (!validator || validator(input)) return input; |
There was a problem hiding this comment.
[P3] if (!validator || validator(input)) return input:compileJsonSchema 对非 object / 数组 / null 返回 undefined 时,输入直接过。这是从 ai-sdk-backend 原样抽出来的。
admission 已经要求根 schema 是 object,所以已 offer 的 MCP 工具通常走不到这条。compile 抛错会被 parseToolArguments 包成非法参数,那条是 fail-closed。真正的洞只剩「编不出 validator 就放行」。Desktop 现在和 CodeMode 共用这条路径,收紧的话两边一起收。
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for tightening optional MCP capability isolation. I reviewed exact head 60c26e80b9779d8d729467b9a0fed6f28f6f8b15 and found one additional final-manifest budget edge that is separate from the current threads. The branch is currently conflicting and its exact-head test check is failing, so I have not treated it as merge-ready. Please do push back if services are guaranteed to have a separately reserved budget—the author may have protocol context that is not visible from this lane.
中文摘要
感谢收紧可选 MCP capability 的隔离。这里还有一个与现有线程不同的最终 manifest 预算边界:预筛只算 offers,而真实 replace 还会携带 services。当前分支冲突且 exact-head test 失败。若 services 有单独预留预算,也欢迎直接反驳并补充协议上下文。
AI-assisted review disclosure: Codex ran an independent analysis lane; Astro-Han independently verified the exact head, production path, and severity, and owns this review.
| const validateSelectedGroups = () => { | ||
| const selected = selectedGroups(); | ||
| if (selected.length === 0) return; | ||
| decodeClientCapabilityReplaceInput({ |
There was a problem hiding this comment.
[P2] Suggestion (category ① — normal MCP capability registration at the manifest-size boundary): could the optional-tool selector validate the exact payload that ClientCapabilityChannel.replace() will send? This preflight only includes registrationId + offers, while the real provider always adds the scheduled-task native service (and may add OAuth services). The protocol applies the 56 KiB limit to the combined decoded offers + services document. Therefore a selected offer set can pass here but cross the limit once production services are added; the final replace then still rejects the whole candidate and required Desktop capabilities fail to register, which is the global failure this PR is trying to isolate. I suggest passing the actual service descriptors into this bounded selection (or sharing one final-manifest builder) and adding an offers-only-pass / offers-plus-service-fail boundary regression. Please push back if another layer reserves those service bytes before this function runs.
# Conflicts: # scripts/package-windows-autoupdate-next.mjs # scripts/verify-windows-autoupdate.mjs # scripts/verify-windows-harness.test.mjs # scripts/verify-windows-x64.mjs
|
@Astro-Han Thanks for pointing out the final-manifest budget boundary. I merged the latest apache/main and resolved the conflict in packages/runtime-host/src/protocol/index.ts. Since main had advanced the compatibility epoch to 105, this PR’s Client Capability tuple-schema change now uses epoch 106 while preserving the mainline history. |
Summary
Refs #3302
An invalid optional MCP tool schema previously caused the entire Desktop Runtime Host capability candidate to fail. This PR now:
This PR intentionally scopes itself to the MCP capability-isolation portion of #3302.
Verification
npm --workspace @maka/runtime run build- passednpm --workspace @maka/desktop run build:main- passednpm --workspace @maka/runtime run typecheck- passednpm --workspace @maka/runtime-host run typecheck- passednpm run lint- passednpm run format:check- passednpm run typecheck/ Desktop typecheck currently fail on unrelated UI API mismatches (settledText,conversationKey,unlockAutoFollow, andtrailingAction).npx knip --workspace apps/desktopcurrently reports three existing unused PiP exports and onetaskkillconfiguration hint.AI use
Tool(s) and scope: Codex contributed to issue analysis, implementation, tests, code review, and PR wording. I reviewed the changes and remain the human contributor of record.
Checklist
Does this PR entail a change in behavior?