Skip to content

fix(desktop): isolate invalid optional MCP tools - #3366

Open
CxHsin wants to merge 17 commits into
apache:mainfrom
CxHsin:fix/rate-limit-session-recovery
Open

fix(desktop): isolate invalid optional MCP tools#3366
CxHsin wants to merge 17 commits into
apache:mainfrom
CxHsin:fix/rate-limit-session-recovery

Conversation

@CxHsin

@CxHsin CxHsin commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Refs #3302

An invalid optional MCP tool schema previously caused the entire Desktop Runtime Host capability candidate to fail. This PR now:

  • adds optional MCP tools in source order only while the complete capability manifest remains protocol-valid, including the per-offer 64-tool and 56 KiB manifest limits;
  • reuses Runtime's Ajv JSON Schema validation adapter before Desktop admission and MCP dispatch;
  • keeps required native capabilities fail-closed;
  • reports each omitted optional offer/tool through Desktop diagnostics; and
  • removes the unrelated rate-limit Runtime Host test.

This PR intentionally scopes itself to the MCP capability-isolation portion of #3302.

Verification

  • npm --workspace @maka/runtime run build - passed
  • npm --workspace @maka/desktop run build:main - passed
  • npm --workspace @maka/runtime run typecheck - passed
  • npm --workspace @maka/runtime-host run typecheck - passed
  • Desktop native capability tests - 16 passed
  • Desktop Runtime Host real UDS tests - 5 passed
  • Runtime tool-argument violation tests - 18 passed
  • Runtime CodeMode tests - 29 passed
  • npm run lint - passed
  • npm run format:check - passed
  • Full npm run typecheck / Desktop typecheck currently fail on unrelated UI API mismatches (settledText, conversationKey, unlockAutoFollow, and trailingAction).
  • npx knip --workspace apps/desktop currently reports three existing unused PiP exports and one taskkill configuration hint.
  • Packaged macOS arm64 verification not run: no Apple Silicon host available. Maintainer verification requested.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

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

  • Tests cover the change and fail without it
  • Lint, format, and affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

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 jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

深入 review 后需要修改再合入。

两个阻断问题:

  1. optional MCP tool 的过滤只验证单个 tool,无法保证最终 capability manifest 仍满足单 offer 64 tools、总 manifest 56 KiB 等协议约束,candidate 仍会因完整注册失败。
  2. 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)],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里每次只对一个 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本 PR 没有对应的 Runtime Host production change;该 test 在 main 上也应成立,且与 optional MCP schema isolation 无关。请从此 PR 移除,若确有独立 rate-limit regression,再用 focused PR 提交,避免继续扩张较重的 execution-host suite。

@CxHsin

CxHsin commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

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
@CxHsin
CxHsin force-pushed the fix/rate-limit-session-recovery branch from d593184 to 2bdd7f9 Compare August 21, 2026 05:51
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
@CxHsin

CxHsin commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

修复 Windows package check 的自动更新失败:electron-builder 的 �xtraMetadata.version 只更新应用包内 manifest,不会更新 PE ProductVersion。提交 209ffe0 在构建 version-bumped installer 时临时写入 Desktop manifest,构建结束后(含失败路径)恢复原文件,使 ProductVersion 与 latest.yml 一致。请重新检查 Windows package job。

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review of exact head 60c26e80b9779d8d729467b9a0fed6f28f6f8b15.

三问

  1. 要解决什么:一条非法的可选 MCP 工具会让整次 Client Capability replace 失败,连 Browser / Computer Use 等必选 offer 一起带走。
  2. 怎么解决:只给 desktop_mcpinvalidToolPolicy: "omit"prepareCapabilityGroups 按完整 selected manifest 调 decodeClientCapabilityReplaceInput,能过才收下这把工具;必选 group 在 omit 循环前先 decode,失败直接抛。调度时用抽出来的 validateJsonSchemaInputaccept() 之前验参。
  3. 奥卡姆:隔离本身是小而准的。同一 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_invalidmcp_valid)。
  • patternProperties / not / if/then/else / OpenAPI nullable 这类常见 JSON Schema 会被 omit。这是 Host 协议的门,不是这 PR 新发明的;相对 Host 不是误判,相对「MCP 服务器认为有效」会静默少工具。
  • Ajv 不在 admission。协议过了就会 offer;调用时 compile/校验失败 fail-closed。compileJsonSchema 返回 undefined 时仍会放行输入。

降级

  • 必选(browser / CU / settings / rive):fail-closed。
  • 可选 MCP:omit 后会话继续,空 group 丢掉,onInvalidToolreportError 不抛。对那把坏工具是 fail-closed,对会话是 fail-open。可选能力这样是对的。
  • 调度:非法参数在 accept() 前拒绝。MCP + AI SDK jsonSchema()validate 那条旧洞,这版 fallback 到 Ajv,测试用 {} 打 required query 能拦住。

CI(按 path filter)

期望:test(永远)、audit(动了 packages/runtime/package.json)、packageruntime-host-boot.ts + windows scripts)。不会跑 windows_sandbox_*

  • test:completed / failureformat:check 挂在 scripts/package-windows-autoupdate-next.mjs
  • audit:completed / success
  • package:仍 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] =

@jackwener jackwener Aug 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] if (!validator || validator(input)) return inputcompileJsonSchema 对非 object / 数组 / null 返回 undefined 时,输入直接过。这是从 ai-sdk-backend 原样抽出来的。

admission 已经要求根 schema 是 object,所以已 offer 的 MCP 工具通常走不到这条。compile 抛错会被 parseToolArguments 包成非法参数,那条是 fail-closed。真正的洞只剩「编不出 validator 就放行」。Desktop 现在和 CodeMode 共用这条路径,收紧的话两边一起收。

@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Aug 27, 2026

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@CxHsin

CxHsin commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@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.
I also updated the affected Desktop test fixtures for the required resolveBrowserUrl dependency. Runtime and Runtime Host typechecks, protocol tests, affected Desktop tests, Desktop build, format check, and lint all pass locally.
The fix is committed as 0bec64e. Please take another look when convenient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants