Skip to content

feat(cli): add plugin quota and update notices - #2147

Merged
wbxl2000 merged 6 commits into
mainfrom
feat/plugin-quota-update-notices
Jul 27, 2026
Merged

feat(cli): add plugin quota and update notices#2147
wbxl2000 merged 6 commits into
mainfrom
feat/plugin-quota-update-notices

Conversation

@wbxl2000

@wbxl2000 wbxl2000 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

None — the problems are described below.

Problem

  1. Users who install an official plugin that bills against plan quota (currently Kimi Datasource) get no signal about that at install time.
  2. When the Official Marketplace has a newer version of an installed plugin, users who keep invoking the old version are never told — the update badge only shows inside the /plugins panel.

What changed

  • Quota note: after the install result of a quota-consuming official plugin, the TUI appends Note: This plugin consumes your quota. to the reload hint. The affected plugin ids live in one list (currently kimi-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.
  • Update notice: plugin usage is buffered per turn — plugin MCP tool calls (resolved to plugin ids via the runtime MCP server names, tolerant of core's 64-char tool-name truncation) and /<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 shows Update 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-url from the official CDN plugin path) — custom catalogs, local/GitHub installs, and same-id forks never trigger them.

Out of scope (review dispositions)

  • Subagent plugin usage does not feed the update notice (child-agent tool events are routed separately) — deliberate v1 boundary, not a defect.
  • Cross-process concurrent writes to the notice state file can produce a duplicate notice in rare multi-instance runs — accepted as a benign UX wart rather than adding a lock.

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

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

- 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-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f4ddcc1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 24, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@f4ddcc1
npx https://pkg.pr.new/@moonshot-ai/kimi-code@f4ddcc1

commit: f4ddcc1

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.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +142 to +144
const entry = marketplace.plugins.find((plugin) => plugin.id === pluginId);
if (entry === undefined) return;
const installed = (await session.listPlugins()).find((plugin) => plugin.id === pluginId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +54 to +57
const rest = toolName.slice(MCP_TOOL_NAME_PREFIX.length);
const separator = rest.indexOf('__');
if (separator <= 0) return undefined;
return rest.slice(0, separator);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +512 to +513
if (QUOTA_CONSUMING_PLUGIN_IDS.includes(summary.id)) {
host.showStatus(PLUGIN_QUOTA_NOTE, 'warning');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +176 to +178
await writePluginUpdateNoticeState(
{ ...state, notified: { ...state.notified, [pluginId]: status.latest } },
this.deps.stateFile,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +160 to +161
const marketplace = await this.loadCatalog();
const entry = marketplace.plugins.find((plugin) => plugin.id === pluginId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +621 to +624
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +230 to +233
// Let the post-check microtasks drain before asserting silence.
await new Promise((resolve) => {
setTimeout(resolve, 0);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines 620 to +623
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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +179 to +180
const state = await readPluginUpdateNoticeState(this.deps.stateFile);
if (state.notified[pluginId] === status.latest) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@wbxl2000
wbxl2000 merged commit 29783e4 into main Jul 27, 2026
15 checks passed
@wbxl2000
wbxl2000 deleted the feat/plugin-quota-update-notices branch July 27, 2026 09:00
@github-actions github-actions Bot mentioned this pull request Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant