feat(cli): add plugin quota and update notices - #2147
Conversation
- Show "Note: This plugin consumes your quota." after installing quota-consuming official plugins (currently Kimi Datasource). - Show a one-time update notice after invoking an outdated plugin (a plugin MCP tool call or a /<plugin>:<command> turn); the last notified version is persisted so each new marketplace version reminds once. - Skip the third-party trust prompt for loopback sources that mirror the official plugin CDN path, so the dev plugin marketplace no longer prompts when installing official plugins.
🦋 Changeset detectedLatest commit: f4ddcc1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
Buffer plugin MCP tool usage during the turn and report it together with plugin command usage when the turn's output has fully ended, instead of firing the check mid-turn at tool result time. Cancelled turns no longer trigger the notice.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eee4396a3e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| private async getMcpServerPluginIds(): Promise<Map<string, string>> { | ||
| if (this.mcpServerPluginIds !== undefined) return this.mcpServerPluginIds; |
There was a problem hiding this comment.
Invalidate plugin MCP cache when sessions change
When the first plugin MCP tool completes, this memoizes the server-to-plugin map permanently. The TUI reuses the same SessionEventHandler/PluginUpdateNotifier across /reload, /new, and session switches, so after a user installs or enables another plugin later in the same app run, that plugin's runtime server name is never added to this map; its MCP tool completions resolve to undefined and the new update notice is silently missed until restart.
Useful? React with 👍 / 👎.
| 'Run /plugins to install the latest version from the Official Marketplace.', | ||
| ); | ||
| await writePluginUpdateNoticeState( | ||
| { ...state, notified: { ...state.notified, [pluginId]: status.latest } }, |
There was a problem hiding this comment.
Serialize notice-state writes for concurrent plugin notices
When a turn uses two different outdated plugins, handleTurnEnd starts separate checks and inFlight only dedupes per plugin, so both checks can read the same state and then each write a new object derived from that stale snapshot. The last write drops the other plugin's notified entry, causing that version to be announced again on the next use even though the feature promises each new version is shown once.
Useful? React with 👍 / 👎.
Address review findings on the plugin update notifier: - The memoized MCP server-to-plugin map is reused across /reload, /new, and session switches, so plugins installed or enabled later in the same app run never resolved. Refresh the map once on a lookup miss, and never pin an empty map when there is no session. - Concurrent notices (a turn that used two outdated plugins) raced on the read-modify-write cycle of the notice state file and could drop each other's entries. Serialize checks through a promise queue so each notified version is persisted exactly once.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68c63ca1a1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const entry = marketplace.plugins.find((plugin) => plugin.id === pluginId); | ||
| if (entry === undefined) return; | ||
| const installed = (await session.listPlugins()).find((plugin) => plugin.id === pluginId); |
There was a problem hiding this comment.
Restrict update notices to official plugin sources
When KIMI_CODE_PLUGIN_MARKETPLACE_URL points to a custom catalog, or when a locally installed fork shares an ID with a catalog entry, this ID-only lookup treats that plugin as official and the notice explicitly directs the user to the “Official Marketplace.” loadPluginMarketplace() honors the custom-marketplace environment override, and the installed summary's source/originalSource is never checked, so a third-party update can be misrepresented as official; validate both catalog and installed provenance or explicitly load the official catalog.
Useful? React with 👍 / 👎.
| const rest = toolName.slice(MCP_TOOL_NAME_PREFIX.length); | ||
| const separator = rest.indexOf('__'); | ||
| if (separator <= 0) return undefined; | ||
| return rest.slice(0, separator); |
There was a problem hiding this comment.
Resolve plugin MCP tools after qualified-name truncation
For an allowed long plugin ID or MCP server name, core's qualifyMcpToolName truncates the entire qualified name to 64 characters and can remove the __ separator before appending its hash. In that case rest.indexOf('__') returns -1, so every completed tool from that plugin is silently ignored and its update notice never runs; resolution needs to account for the core truncation format rather than requiring the separator to survive.
Useful? React with 👍 / 👎.
| if (QUOTA_CONSUMING_PLUGIN_IDS.includes(summary.id)) { | ||
| host.showStatus(PLUGIN_QUOTA_NOTE, 'warning'); |
There was a problem hiding this comment.
Verify provenance before showing the quota warning
When a user installs a local, GitHub, or third-party plugin whose manifest ID is kimi-datasource, this ID-only condition displays the quota-consumption warning even though the installed code is not the official billed plugin. Because PluginSummary includes source and originalSource, gate the note on the trusted official source as well as the ID to avoid falsely telling users that an unrelated custom plugin consumes their plan quota.
Useful? React with 👍 / 👎.
…-name truncation Address review findings: - The quota note and the update notice keyed on the plugin id alone, so a local/GitHub fork reusing a billed plugin's manifest id was treated as the official build. Both now require official provenance (a zip install from the official CDN plugin path or its loopback dev mirror) via a shared isOfficialPluginInstall check. - Resolving plugin MCP tools by splitting on the '__' separator broke for qualified names core truncates to 64 chars, which can cut the separator. Match known server names by longest prefix with a name boundary instead, which survives truncation as long as the server part itself is intact.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b928b6b9b2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ); | ||
| if (!url.pathname.startsWith('/kimi-code/plugins/official/')) return false; | ||
| if (url.protocol === 'https:' && url.hostname === 'code.kimi.com') return true; | ||
| return url.protocol === 'http:' && isLoopbackHostname(url.hostname); |
There was a problem hiding this comment.
Restrict loopback trust to the dev marketplace
Any production user who installs http://localhost:<port>/kimi-code/plugins/official/<name>.zip now bypasses the third-party confirmation, even if that port is owned by an unrelated local service serving an unreviewed plugin. Because plugins can execute code and access the workspace, loopback plus a path prefix does not prove official provenance; limit this exception to the configured dev marketplace origin or an explicit development mode.
Useful? React with 👍 / 👎.
| await writePluginUpdateNoticeState( | ||
| { ...state, notified: { ...state.notified, [pluginId]: status.latest } }, | ||
| this.deps.stateFile, |
There was a problem hiding this comment.
Serialize notice state across CLI processes
When two CLI instances detect updates concurrently, both can read the same old state at line 170 and then write snapshots derived from it here. For the same plugin this produces duplicate supposedly one-time notices; for different plugins the last rename drops the other entry, causing a later duplicate. The read-modify-write needs a cross-process lock or another atomic merge mechanism, not only the instance-local promise queue.
Useful? React with 👍 / 👎.
The loopback carve-out let any local service bypass the third-party trust prompt by serving a zip under the official path shape, which does not prove official provenance (review P1). Revert to the single rule — only https://code.kimi.com/kimi-code/plugins/official/* is a trusted official source — and restore the stock dev marketplace server. Installing official plugins from the dev marketplace shows the trust prompt again.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0cc6657d25
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const marketplace = await this.loadCatalog(); | ||
| const entry = marketplace.plugins.find((plugin) => plugin.id === pluginId); |
There was a problem hiding this comment.
Reject custom catalogs before issuing official update notices
When KIMI_CODE_PLUGIN_MARKETPLACE_URL points to a custom catalog, this lookup accepts any matching entry without validating the catalog or entry provenance. If an official plugin is installed and that custom catalog advertises the same ID at a higher version, the CLI emits a notice claiming the version comes from the Official Marketplace and directs the user toward that custom entry. Restrict this notifier to the default official catalog or verify the matched entry's official source before announcing it.
Useful? React with 👍 / 👎.
| if (matchedCall !== undefined && isPluginMcpToolName(matchedCall.name)) { | ||
| // Buffer plugin MCP usage for the turn; the update notice fires once the | ||
| // whole turn's output has ended (see handleTurnEnd). | ||
| this.pluginMcpToolsUsedInTurn.add(matchedCall.name); |
There was a problem hiding this comment.
Include subagent MCP usage in update detection
When an outdated plugin MCP tool is invoked by a subagent, its events return through routeChildAgentEvent before reaching this collection logic, so the main turn finishes without ever notifying the user. Subagents share the session MCP manager and can invoke these tools, so record qualifying child tool completions in the subagent route as well and flush them when the encompassing main turn ends.
Useful? React with 👍 / 👎.
| // Let the post-check microtasks drain before asserting silence. | ||
| await new Promise((resolve) => { | ||
| setTimeout(resolve, 0); | ||
| }); |
There was a problem hiding this comment.
Await notifier completion before asserting silence
In the already-notified scenario, listPlugins being called does not mean the subsequent asynchronous state-file read has completed, and a zero-delay timer is not an ordering guarantee relative to filesystem I/O. Under load this assertion can pass before a late notification occurs, after which cleanup may delete the state directory while the swallowed background task is still running. Expose or inject an awaitable completion/drain seam instead of using real setTimeout; the same pattern recurs elsewhere in this file.
Useful? React with 👍 / 👎.
…tests - Skip the update check when the loaded marketplace is not the default official catalog, so a custom KIMI_CODE_PLUGIN_MARKETPLACE_URL can no longer produce a notice that claims to come from the Official Marketplace. - Return a never-rejecting promise from the notifier entry points so tests await the serialized queue directly instead of relying on zero-delay timers for ordering.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f4ddcc1a88
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const matchedCall = streamingUI.completeToolResult(event.toolCallId, resultData); | ||
| if (matchedCall !== undefined && isPluginMcpToolName(matchedCall.name)) { | ||
| // Buffer plugin MCP usage for the turn; the update notice fires once the | ||
| // whole turn's output has ended (see handleTurnEnd). |
There was a problem hiding this comment.
Track plugin use by child agents
When an outdated plugin MCP tool is used by a child agent, routeChildAgentEvent returns before execution reaches this buffering block, while SubAgentEventHandler only renders the child's tool.result. Consequently, a user turn whose plugin use occurs exclusively in an Agent/AgentSwarm child finishes without any update notice. Forward child plugin-tool completions into the parent turn's usage buffer as well.
Useful? React with 👍 / 👎.
| const state = await readPluginUpdateNoticeState(this.deps.stateFile); | ||
| if (state.notified[pluginId] === status.latest) return; |
There was a problem hiding this comment.
Ignore catalog versions older than the recorded notice
If a user was notified about 3.5.0 and a stale or rolled-back marketplace response later advertises 3.4.0 while the installed version is below both, this equality check treats 3.4.0 as a new notice and overwrites the saved 3.5.0; when 3.5.0 reappears, it is announced again. Compare the candidate with the recorded semver and skip versions that are not newer, rather than checking only equality.
Useful? React with 👍 / 👎.
Related Issue
None — the problems are described below.
Problem
/pluginspanel.What changed
Note: This plugin consumes your quota.to the reload hint. The affected plugin ids live in one list (currentlykimi-datasource) so future quota-billed official plugins only need a one-line entry. Gated on official provenance (a zip install from the official CDN plugin path), so a local/GitHub fork reusing the manifest id does not trigger it./<plugin>:<command>turns. When the turn's output has fully ended (turn.ended, skipped for cancelled turns), the TUI checks the official marketplace catalog (cached once per run) and showsUpdate detected: <name> <version> is available. Run /plugins ...when the installed version is older and the install has official provenance. The last notified version is persisted in~/.kimi-code/updates/plugin-notices.json, so each marketplace version reminds exactly once and a newer version reminds again; checks are serialized so concurrent notices cannot drop each other's state.Both notices only fire for the default official catalog and for installs with official provenance (
zip-urlfrom the official CDN plugin path) — custom catalogs, local/GitHub installs, and same-id forks never trigger them.Out of scope (review dispositions)
Tests cover the notifier (dedupe, re-notify on newer version, failure retry, provenance and catalog gates, truncated tool names, MCP map refresh on miss, two-plugin serialization) with awaitable entry points instead of timing hacks, the turn-end buffering (no notice at tool result, notice at turn end, cancelled turns skipped), the quota note (shown / not shown / fork), and the provenance/trust matrix. The plugins docs page (en + zh) documents both notices.
Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.