Skip to content

fix(core): warn when tools.eager entries match no discovered tool - #12451

Merged
yiliang114 merged 2 commits into
mainfrom
fix/issue-12435-eager-unmatched-tool-warning
Sep 22, 2026
Merged

yiliang114 merged 2 commits into
mainfrom
fix/issue-12435-eager-unmatched-tool-warning

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

Adds an existence check for settings.tools.eager entries that runs when an MCP discovery pass reaches COMPLETED (the bulk pass behind ToolRegistry.discoverAllTools(), the pooled pass used by daemon/ACP sessions, and the incremental pass used by the default background startup and by MCP hot-reload). Any active allowlist entry that matches no registered tool is reported on the console, once per entry per session. The check lives in a new file packages/core/src/permissions/eager-allowlist-coverage.ts; the only change to existing code is one import plus three one-line calls at those three discovery boundaries in mcp-client-manager.ts.

Matching deliberately mirrors PermissionManager.getToolRegistrationStatus: toolMatchesRuleToolName(entry, canonicalRegisteredName) over ToolRegistry.getAllToolNames(). So meta-categories (Read, Bash) and aliases (ReadFile, ListFiles) are never reported, and built-ins that are still unwarmed lazy factories count as registered. The same check covers built-in typos (read_flie) — there is no mcp__-specific validation branch.

Two classes of entry are intentionally not reported: (1) an entry whose MCP server is configured but not CONNECTED (refused by the client budget, failed handshake, disabled, still connecting) — that is an infrastructure outcome, not a spelling mistake, and the skip is not recorded in the warn-once ledger, so a later pass reports it if the name really is wrong; (2) computer_use__* entries — the generated cua-driver surface is not part of the tool registry at the MCP discovery boundary, so there is no reliable candidate set and any report would be a false positive. Entries initialize() already dropped as unusable are skipped too, so one mistake is never reported twice.

Why it's needed

tools.eager only shape-checks its entries today: an entry that parses cleanly is kept even if it names nothing. Because an active allowlist demotes every non-exempt tool it does not name, a single typo shrinks the whole eager tool surface — and nothing at startup says so. {"tools":{"eager":["mcp__githb__create_issue"]}} activates the allowlist, defers read_file and friends to tool_search, and produces zero diagnostics.

Semantics were settled by the issue author in #12435 (comment) — check only after discovery reaches COMPLETED (never in initialize(), where dynamic tools are not registered yet), match with toolMatchesRuleToolName rather than set membership, do not blame the operator for a server that was refused or never connected, exclude computer_use__* when its registration is not observable at this boundary, and warn once per entry across re-discovery.

Reviewer Test Plan

How to verify

New test file packages/core/src/permissions/eager-allowlist-warning.test.ts (16 tests). It wires a real Config (makeFakeConfig), a real PermissionManager and a real ToolRegistry the way Config.initialize() does, then drives a real discovery pass through McpClientManager.discoverAllMcpToolsIncremental() (default startup) and discoverAllMcpTools() (legacy blocking / re-discovery) and captures console.warn.

cd packages/core && ../../node_modules/.bin/vitest run src/permissions/eager-allowlist-warning.test.ts --coverage.enabled=false --retry=0

Before the fix the same file fails with the allowlist active, read_file deferred, no dropped-entry warning, and zero warnings about existence. Two red variants, because the fix is a new file plus three call sites: with only the three mcp-client-manager.ts call sites reverted (helper and tests kept) the five behavioural assertions fail — 5 failed | 11 passed (16); on pristine main (7837c6c200afb3853a126fc2dc01a155204aaaed, helper file absent) it is 11 failed | 5 passed (16), the extra six being the cases that call the helper directly. Both were re-run independently after the fact; green is 16 passed (16).

× keeps a shape-valid dynamic typo silent about existence: the allowlist activates and defers built-ins
  → expected [] to have a length of 1 but got +0
× warns once when a dynamic entry matches no discovered tool
  → expected [] to have a length of 1 but got +0
× warns on the legacy blocking discovery pass too
  → expected [] to have a length of 1 but got +0
× warns about a misspelt built-in entry as well
  → expected [] to have a length of 1 but got +0
× warns each unmatched entry only once across re-discovery
  → expected [] to have a length of 1 but got +0
 Test Files  1 failed (1)
      Tests  11 failed | 5 passed (16)

Mutation-checked (each mutant applied alone, then reverted): replacing toolMatchesRuleToolName with name-equality set membership kills stays quiet for meta-category and alias entries; dropping the warn-once ledger kills warns each unmatched entry only once across re-discovery and the suppressed-entry re-check; dropping the unconnected-server suppression kills all three refused/disconnected-server cases; dropping the computer_use__* skip kills that case.

Guardrails, all green with the change applied: permission-manager.test.ts + mcp-client-manager.test.ts + eager-surface-report = 638 passed (638); tool-registry.test.ts = 73 passed; config.test.ts = 825 passed; tsc --noEmit -p packages/core = exit 0 with zero output; prettier --write and eslint clean on all three touched files.

Not run: no live stdio stub MCP server and no real CLI session were used — the discovery boundary is exercised through the real McpClientManager incremental and bulk passes on a real Config/PermissionManager/ToolRegistry, with no server configured. A typo on a server that is actually up and connected is therefore covered by unit-level status injection (updateMCPServerStatus) rather than by a real handshake.

Evidence (Before & After)

N/A — non-UI change (console warning only). Test output above.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ✅

Environment (optional)

Unit tests only (vitest in packages/core).

Risk & Scope

  • Main risk or tradeoff: the unconnected-server suppression uses getMCPServerStatus(name) !== CONNECTED for a server present in config.getMcpServers(). A correctly spelled entry on a server that never comes up stays silent for the whole session — that is the intended tradeoff (an outage must not be reported as a user typo), and the entry is re-checked on every later pass rather than being remembered as warned. computer_use__* typos stay undetected for the same "no false positives" reason.
  • Not validated / out of scope: ToolRegistry.discoverToolsForServer() (/mcp reconnect for a single server) does not run the check; the eager allowlist is restart-scoped, so a startup pass has already reported every unmatched entry by then, and adding a fourth hook to a hot file did not seem worth it. No new settings key, no new public API, no telemetry.
  • Breaking changes / migration notes: none. Adds a console.warn for configurations that were already silently misbehaving.

Size: git diff --stat origin/main...HEAD → 3 files changed, 510 insertions(+) (145 source, 355 test, 10 in mcp-client-manager.ts).

Linked Issues

Fixes #12435

中文说明

这个 PR 做了什么

给 settings.tools.eager 加了一次「存在性校验」,只在 MCP 发现流程走到 COMPLETED 时执行(ToolRegistry.discoverAllTools() 背后的批量发现、daemon/ACP 使用的连接池发现、以及默认后台启动与 MCP 热重载使用的增量发现)。激活的 allowlist 里凡是匹配不到任何已注册工具的条目,都会在控制台告警,每个条目每个会话只告警一次。校验逻辑放在新文件 packages/core/src/permissions/eager-allowlist-coverage.ts;对既有代码的改动只有一个 import 加 mcp-client-manager.ts 里三个发现边界上各一行调用。

匹配方式刻意与 PermissionManager.getToolRegistrationStatus 保持一致:用 toolMatchesRuleToolName(entry, 已注册工具的 canonical name) 对 ToolRegistry.getAllToolNames() 求值。因此 meta-category(Read、Bash)与 alias(ReadFile、ListFiles)不会被误报,尚未 warm 的惰性 factory 形式的内置工具也算已注册。同一个校验也覆盖内置名拼错(read_flie)——没有 mcp__ 专属的校验分支。

有两类条目刻意不告警:(1) 条目所属的 MCP server 已配置但不是 CONNECTED(被连接预算拒掉、握手失败、被禁用、仍在连接中)——这是基础设施结果而不是拼写错误;并且这种跳过不记入 warn-once 集合,所以后续发现流程如果确认名字确实写错了仍会告警。(2) computer_use__* 条目——cua-driver 生成的那批工具在 MCP 发现边界上并不在 tool registry 里,没有可靠的候选集合,报出来必然是假阳性。initialize() 已经作为「不可用条目」丢弃并告警过的条目也会跳过,同一个错误不会被报两次。

为什么需要

今天 tools.eager 只做形状校验:只要能解析通过就保留,哪怕它什么都没指到。而激活的 allowlist 会把它没点名的所有非豁免工具降级,于是一个拼写错误就能把整个 eager 工具面缩小,启动期却没有任何提示。{"tools":{"eager":["mcp__githb__create_issue"]}} 会激活 allowlist、把 read_file 等工具推迟到 tool_search,并且零诊断输出。

语义由 issue 作者在 #12435 (comment) 拍板:只在发现流程 COMPLETED 之后校验(绝不在 initialize() 里,那时动态工具还没注册);用 toolMatchesRuleToolName 而不是集合成员判断;被拒或没连上的 server 不把责任推给使用者;computer_use__* 在该边界拿不到可靠注册结果时排除;每个条目跨 re-discovery 只告警一次。

评审验证方案

新测试文件 packages/core/src/permissions/eager-allowlist-warning.test.ts(16 个用例)。它按 Config.initialize() 的方式装配真 Config(makeFakeConfig)、真 PermissionManager、真 ToolRegistry,再通过 McpClientManager.discoverAllMcpToolsIncremental()(默认启动)与 discoverAllMcpTools()(旧的阻塞式/重新发现)跑真实发现流程,并捕获 console.warn。修复前同一文件失败(allowlist 已激活、read_file 已 deferred、没有 dropped-entry 告警、也没有任何存在性告警):11 failed | 5 passed。修复后 16 passed。

变异检验(每个 mutant 单独施加后立刻还原):把 toolMatchesRuleToolName 换成名字相等的集合判断 → meta-category/alias 用例变红;去掉 warn-once 集合 → 「只告警一次」与被跳过条目的复查用例变红;去掉未连接 server 的抑制 → 三个 refused/disconnected 用例变红;去掉 computer_use__* 跳过 → 对应用例变红。

修复前的红有两种形态(因为修复 = 一个新文件 + 三行调用):只把 mcp-client-manager.ts 的三处调用还原(保留 helper 与测试)⇒ 五条行为断言变红,5 failed | 11 passed (16);在原始 main(7837c6c200afb3853a126fc2dc01a155204aaaed,helper 文件不存在)⇒ 11 failed | 5 passed (16),多出的 6 条是直接调用 helper 的用例。两种都在事后被独立重跑复核过;修复后 16 passed (16)。

护栏(带上本改动全绿):permission-manager.test.ts + mcp-client-manager.test.ts + eager-surface-report = 638 passed (638);tool-registry.test.ts = 73 passed;config.test.ts = 825 passed;tsc --noEmit -p packages/core = exit 0、零输出;三个改动文件的 prettier --write 与 eslint 均干净。

未跑项:没有起真实的 stdio stub MCP server,也没有跑真实 CLI 会话——发现边界是通过真实 McpClientManager 的增量与批量两条流程、在真 Config/PermissionManager/ToolRegistry(未配置任何 server)上驱动的。因此「server 真的连上了但工具名拼错」这一情形是用单测里注入状态(updateMCPServerStatus)覆盖的,不是真握手。

风险与范围

  • 主要风险/取舍:未连接 server 的抑制判据是「config.getMcpServers() 里存在该 server 且 getMCPServerStatus(name) !== CONNECTED」。拼写正确但 server 始终起不来时,整个会话都不会告警——这正是想要的取舍(不能把服务故障报成用户配置错误),而且该条目在后续每一轮都会重新校验,不会被记成已告警。computer_use__* 的拼写错误同样检测不到,理由都是「宁可少报也不要假阳性」。
  • 未验证/超出范围:ToolRegistry.discoverToolsForServer()(/mcp reconnect 单服务器重连)不跑该校验;eager allowlist 是重启生效的,启动那一轮已经报过所有未匹配条目,为一个热点文件再加第四个钩子不划算。没有新增 settings 键、没有新增公共 API、没有新增 telemetry。
  • 破坏性变更/迁移说明:无。只是对本就静默出错的配置多了一条 console.warn。

规模:git diff --stat origin/main...HEAD → 3 files changed, 510 insertions(+)(源码 145 行、测试 355 行、mcp-client-manager.ts 10 行)。

关联 issue

Fixes #12435

`settings.tools.eager` only shape-checks its entries: an entry that parses
cleanly is kept even when it names no tool. Because the allowlist demotes
every non-exempt tool it does not name, one misspelt dynamic entry
(`mcp__githb__create_issue`) silently shrinks the whole eager tool surface
and nothing at startup says so.

Add a registry-aware existence check that runs when an MCP discovery pass
reaches COMPLETED (bulk, pooled and incremental), so it can never fire
before dynamic tools are registered. Matching mirrors
`getToolRegistrationStatus`: `toolMatchesRuleToolName` over canonical
registered names, so meta-categories (`Read`, `Bash`) and aliases
(`ReadFile`) are never reported, and built-ins that are still unwarmed lazy
factories still count as registered.

Entries whose MCP server is configured but not connected (refused by the
client budget, failed handshake, disabled) are skipped: that is an
infrastructure outcome, not a spelling mistake. The skip is not recorded in
the warn-once ledger, so a later pass still reports the entry once the
server is up and the name turns out to be wrong. `computer_use__*` entries
are skipped because the generated cua-driver surface is not part of the
registry at this boundary, so any report would be a false positive.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmuc7mwe8dh
Picks up #12446 (scripts/tests/corepack-warmup.js) so the Test lane stops
failing on the runner's corepack pnpm cache wreckage (#12436).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmucahs3hdl
@yiliang114

Copy link
Copy Markdown
Collaborator Author

The Test lane failure was runner infra, not this PR — base merged to pick up the fix

Test (ubuntu-latest, Node 22.x) was red at e6b484c (run 35693678458, job 106635920987, step 16):

FAIL scripts/tests/package-scripts.test.js > package scripts > selects the CLI dependency closure without selecting the same-named root   (x3)
Error: Cannot find module '/home/github-runner/actions-runner-21/_work/_temp/qwen-ci-home/.cache/node/corepack/v1/pnpm/11.24.0/bin/pnpm.mjs'
  code: 'MODULE_NOT_FOUND'
also: scripts/tests/release-versioning.test.js:71:60
Test Files  2 failed | 90 passed (92)      Tests  3 failed | 2595 passed | 12 skipped (2610)

That is #12436. CI routes HOME to a per-run temp dir; concurrent vitest workers race the corepack download and can leave wreckage behind (version dir present, bin/ missing, .corepack marker still valid). After that every corepack pnpm on that home fails with exactly this MODULE_NOT_FOUND, so re-running the job cannot help by design — the poisoned cache outlives the retry. #12446 addressed it by adding scripts/tests/corepack-warmup.js, merged to main at 2026-09-22T05:28:45Z (5f713a24). This branch predated that merge and therefore never had the warmup. Sibling #12412 was red with the identical signature at the same time.

This PR did not cause it: its diff touches 0 files under scripts/ — only packages/core/src/permissions/eager-allowlist-coverage.ts, packages/core/src/permissions/eager-allowlist-warning.test.ts, packages/core/src/tools/mcp-client-manager.ts.

So this is a base merge only; no product code was changed and no test was touched. Merge commit: ea5d42629496b8c24f4ef1c6deb5f0793a48c6c1.

Proof the merge is purely mechanical (no conflict resolution smuggled in):

check result
parent count of HEAD 2 — e6b484c8 + a64d8dec
HEAD^{tree} vs pre-merge git merge-tree --write-tree OID both 0b9e1b2e8be409714c2e50218537136fd05de2b6 → identical
git diff --shortstat origin/main...HEAD 3 files changed, 510 insertions(+) both before and after → PR delta did not grow
scripts/tests/corepack-warmup.js ABSENT at e6b484c8 → PRESENT at ea5d4262

Focused local verification at the merge commit: npx vitest run src/permissions/eager-allowlist-warning.test.ts → Test Files 1 passed (1) / Tests 16 passed (16); npm run typecheck in packages/core → clean (rc=0). I deliberately did not run package-scripts.test.js / release-versioning.test.js locally — they spawn real corepack pnpm, and this checkout has no packageManager pin, so a local result would not be representative of the runner.

Delivered as a single non-force push: e6b484c858..ea5d426294.

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Critical-only review at ea5d4262, base a64d8dec.

Verdict: APPROVE — no review or thread has ever been filed against this PR, so there is no historical blocker to re-check, and my own scan found no Critical. The triage pass reported no blockers either; I did not rely on that and verified the load-bearing claims in code.

The diagnostic cannot break discovery

This was the first thing to establish, because all three call sites sit on completion paths where an exception would be attributed to discovery rather than to a warning. The whole body of warnOnUnmatchedEagerToolEntries is wrapped in try { … } catch {} and documented as never throwing. The three insertions are one import and three one-line calls, each after the registry has settled: the per-session bulk pass after its finally has decremented bulkPassDepth and flushed any refused batch; the pooled pass after setMCPDiscoveryState(COMPLETED) and its mcp-client-update emit; and the incremental pass after the emit that the AppContainer batch-flush subscriber depends on for observing the terminal state. Nothing existing is reordered, and the diff is purely additive at +510/-0.

The matcher is the same oracle as the gate it warns about

A warning that fires on correctly spelled entries would be worse than no warning, so I checked the matching rather than trusting the description. toolMatchesRuleToolName(ruleToolName, contextToolName) is the signature in rule-parser.ts:415, and the new code calls it as toolMatchesRuleToolName(entry, name) with entry the parsed rule name and name a registered tool name — the same order permission-manager.ts:949 uses for (eagerName, canonicalName). keptEagerEntries also mirrors the manager: it skips rule.invalid and pushes rule.toolName, which is what permission-manager.ts:604 and 1333 do. Reversed arguments here would have produced one spurious warning per entry per session, and they are not reversed.

Candidates come from registry.getAllToolNames() rather than from instantiated-tool membership, which is the right choice and the one the module documents: set membership would false-positive on meta-categories and aliases and would miss built-ins that are still unwarmed lazy factories. The test at line 200 pins exactly that case.

The two suppression classes are sound, and asymmetric in the right direction

isOnUnconnectedServer suppresses only when the server name is a configured key and its status is not CONNECTED. It tests that with Object.prototype.hasOwnProperty.call(servers, serverName) rather than in, so a server literally named constructor or toString cannot false-match off the prototype and silently suppress a real typo. An entry whose server is not configured at all stays reportable, which is correct: that is a spelling mistake, not an outage. Empty server names and non-mcp__ entries both return false early.

Suppression is deliberately not written to the warn-once ledger, so a later pass — once the server is up — still reports the entry if the name really is wrong. computer_use__* entries are skipped by the same continue that precedes the ledger write, so they are re-evaluated each pass rather than permanently silenced. Both asymmetries are the safe direction: a suppressed entry can still be reported later, and nothing is recorded as warned unless a warning actually went out.

Warn-once is per session and cannot leak or repeat

The ledger is a WeakMap<Config, Set<string>>, so it is keyed per session, dies with it, and never touches global scope. A /mcp reconnect, a hot-reload reconcile or a second startup pass therefore does not repeat a warning the operator has already seen, and the retained set is bounded by the number of eager entries. Cost is only incurred when it can matter: three early returns (no permission manager or the allowlist is inactive, no registry, no entries) precede any work, and the loop is small in both dimensions.

Two smaller things I checked rather than assumed

console.warn is the established operator-facing channel in this package, not a new convention — there are 26 non-test uses under packages/core/src, including the message display dispatcher and the stream guards. And Config.getEagerTools() returns readonly string[] | undefined, which is what the ?? [] and the typeof raw !== 'string' guard in keptEagerEntries are written against.

Test coverage

The suite pins every branch in the implementation rather than a happy path: warns on an unmatched dynamic entry, on the legacy blocking pass, and on a misspelt built-in; stays quiet when every entry resolves, for meta-category and alias entries, for a tool that is only an unwarmed lazy factory, and when no allowlist is active; warns each entry once across re-discovery; does not re-report entries initialize() already dropped. The unconnected-server group covers a correctly spelled tool on a down server, a client-budget refusal, the case where the server is connected and simply has no such tool, an unknown server name as a typo, and the re-check-after-suppression behaviour. computer_use__* has its own case. I found no assertion that could pass against a broken implementation.

CI

Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox) and both Desktop Shell lanes pass at this head. web-shell E2E Smoke is still pending and I did not wait on it; nothing about this change touches the web shell. The red Test lane the author reported on the previous commit was in scripts/tests/package-scripts.test.js, which none of these three files can influence, and the lane is green after the base merge — that is the only part of that account I needed to confirm, and it holds.

@qwen-code-dev-bot qwen-code-dev-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE at ea5d426.

  • Head re-verified immediately before submitting: ea5d426, OPEN and mergeable. No formal review and no inline thread existed on this PR before mine, so there was no historical blocker to clear.
  • Required CI green on this head, with review-pr excluded as reviewer-bot infrastructure per my standing disclosure; Test (macos/windows) and Integration Tests (CLI, No Sandbox) are skipped repo-wide.
  • No new Critical. The load-bearing property of this change is that the existence check cannot disagree with the mechanism it describes, and it holds. The deferral decision at permission-manager.ts:945-952 is eagerToolAllowList.some(eagerName => toolMatchesRuleToolName(eagerName, canonicalName)), and the allowlist is built by the same shape filter that keptEagerEntries re-derives verbatim: drop non-strings and empties, drop what parseRule marks invalid, otherwise keep rule.toolName. Same matcher, same argument order, same candidate names. An entry that really does keep a tool eager therefore cannot be reported. The corollary settles the wildcard question, which is the first thing I went looking for: toolMatchesRuleToolName has no glob branch, so an mcp__* style entry keeps nothing eager today either, and reporting it is correct rather than a false positive.
  • The other two false-positive risks are handled and each is pinned by its own test: meta-category and alias entries match through the same READ_TOOLS, EDIT_TOOLS and shell-covers-monitor branches the mechanism uses, and a built-in that is still an unwarmed lazy factory still matches because the candidate set is getAllToolNames rather than the set of instantiated tools.
  • The suppression logic points the right way. A correctly spelled tool on a configured server that is not connected is an outage rather than a typo and stays quiet, and that suppression is deliberately not written into the warn-once ledger, so a later pass still reports the entry if it proves wrong once the server is up; an entry whose server is not configured at all stays reportable. The computer_use__ exclusion states its reason instead of hiding it.
  • It cannot damage what it runs inside. The whole body is wrapped so a diagnostic can never fail a discovery pass, it returns early unless the allowlist is active, and the warn-once ledger is a WeakMap keyed by Config, so a reconnect or a hot-reload reconcile neither repeats a warning nor leaks past the session. Emitting through console.warn with that eslint-disable reasoning matches the sibling allowlist warning already at permission-manager.ts:344, so it follows the established pattern for this feature rather than introducing one.
  • All three call sites are settled-registry points (the per-session bulk pass, the legacy blocking pass, and the startup plus hot-reload reconcile path), which is where this has to run because MCP tools are discovered after initialize().

@yiliang114
yiliang114 added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit a858dea Sep 22, 2026
51 checks passed
@chiga0

chiga0 commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Post-merge review — head ea5d42629 (merged)

No blocking findings. Would have been an APPROVE on the open PR.


What was checked

New file: packages/core/src/permissions/eager-allowlist-coverage.ts

  • keptEagerEntries(config) mirrors PermissionManager.initialize() exactly: same empty/trim/parseRule()/rule.invalid filter chain, same rule.toolName push. Cross-checked against permission-manager.ts — both build canonical names identically.
  • isOnUnconnectedServer extracts the server name segment from mcp__X__Y entries and guards against false positives when a server hasn't finished discovery yet.
  • Warn-once WeakMap: warnedEntriesByConfig: WeakMap<Config, Set<string>> — once an entry is logged for a config lifetime, duplicates across all three call sites are suppressed. After a server connects, its tools appear in getAllToolNames(), so the matched entry never reaches the warnedSet.add() path — no duplicate on reconnect.
  • try/catch wrapping the entire function body: a failed getPermissionManager() or getAllToolNames() call is silently swallowed rather than breaking the discovery pass. Correct for a diagnostic-only helper.
  • computer_use__ exclusion confirmed via test coverage (test: "says nothing for computer_use__ entries").

Three call sites in packages/core/src/tools/mcp-client-manager.ts

  • All three are at COMPLETED boundaries (bulk pass, session pass, incremental pass) — correct placement; tool registry is fully populated before each call.
  • Warn-once semantics mean the three call sites produce at most one warning per entry, regardless of how many passes run before a server connects.

Test file: packages/core/src/permissions/eager-allowlist-warning.test.ts (16 tests)

  • Typo detection, warn-once, meta-category/alias silence, lazy factory names, unconnected server suppression, re-check after server connects — all material paths covered.
  • Cumulative callCount spy pattern (not toHaveBeenCalledTimes) correctly measures across multiple runDiscoveryPass invocations in the same test.

Cross-file: permission-manager.ts — verified initialize() builds canonicalNames with the same filter chain; getToolRegistrationStatus calls toolMatchesRuleToolName(eagerName, canonicalName) in the same order as warnOnUnmatchedEagerToolEntries. No contract asymmetry.


Non-blocking observations

None.

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

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(core): warn when dynamic tools.eager entries match no discovered tool

5 participants