Skip to content

Plugin identity: explicit group + id, with an install-time conflict gate - #153

Merged
DianaSensei merged 8 commits into
mainfrom
plugin-market-baseid-fix
Sep 18, 2026
Merged

DianaSensei merged 8 commits into
mainfrom
plugin-market-baseid-fix

Conversation

@DianaSensei

@DianaSensei DianaSensei commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Summary

A plugin installed via a market (e.g. Containers from the official market) opened to This tool hit an error — Không có plugin "container-manager" trong registry. usePluginSdkFor() chỉ dùng cho code thuộc một plugin có thật, không phải cho shell. — even though it was installed and actively rendering.

Root cause: installer.ts prefixed a market-installed plugin's registry id with <marketId>- (e.g. official-container-manager) so two markets couldn't collide over the same plugin id. But the plugin's own bundled code has no idea about that prefix — it always calls usePluginSdkFor('container-manager') / getPluginSdk('container-manager') with its bare, self-authored id.

Design

Every plugin now has two identity fields:

  • id — self-authored, bare, what the plugin's own bundle calls itself via usePluginSdkFor/getPluginSdk. Also the storage/secrets/audit namespace key. This is a change from the old <marketId>-<id> scheme for market installs — but it needs no data migration: under the old scheme, a market-installed plugin crashed in usePluginSdkFor (the bug this PR fixes) before it could ever reach a single sdk.storage/sdk.secrets call, so there is no data under the old namespace for anyone to lose. See the comment above installedPluginManifests() in installer.ts for the same reasoning inline.
  • group — where it was installed from: "core" for the 26 compile-time plugins (auto-assigned, no need to touch those files), "url" for a bare-URL install, or the marketId for a market install. Pure metadata: used for route generation (/installed/<group>/<id>, unified for both URL and market installs) and for deciding install-time conflicts.

id still ends up unique system-wide in practice: usePluginSdkFor/getPluginSdk are called by a plugin's own bundle with just the bare id, no group — a module-scope call (Kafka's/RabbitMQ's background consumer stores, no React context) can't resolve which group it belongs to. Letting the same id genuinely coexist under two groups would make that lookup ambiguous and silently misattribute storage/audit/permissions to the wrong plugin.

So installer.ts enforces it at install time, not registry time: assertNoConflictingInstall(id, marketId, registeredGroup?) checks already-installed plugins for the same bare id under a different group and throws a clear "uninstall the old one first" error before Rust ever writes to disk — called from both install call sites (ExtensionInstallDialog, SettingsExtensionInstaller) using the manifest they've already fetched for the confirmation screen (no extra network round-trip). The optional registeredGroup catches the same-id-as-a-built-in-tool case immediately; every other group value falls through to an on-disk scan, since the in-memory registry only loads once at bootstrap and can go stale mid-session (e.g. right after an in-session uninstall).

registry.ts is back to the simple pre-bug form: registerManifest just defaults group to 'core' for compile-time plugins and keeps one flat seenIds uniqueness check; getPlugin(id) is a plain PLUGIN_MAP.get(id) — no fallback/ambiguity machinery left.

Test plan

  • tsc --noEmit
  • vitest run — 1376/1376 passing
  • Verified assertNoConflictingInstall's guards by temporarily reverting each fix — the corresponding tests fail for the right reason, confirming real coverage
  • Manual: install a plugin from a market, then try installing the same plugin id via raw URL — confirm the install is blocked with a clear message

🤖 Generated with Claude Code

https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2

… themselves

A plugin installed via a market gets a registry id prefixed with
<marketId>- (e.g. "official-container-manager") to avoid colliding with
another market publishing the same plugin id (installer.ts's
installedPluginManifests()). But the plugin's own bundled code has no idea
about that prefix and always calls usePluginSdkFor/getPluginSdk with its
bare, self-authored id (e.g. usePluginSdkFor('container-manager')) — it
can't know at build time which market, if any, it'll be installed under.

usePluginSdkFor's context match compared against the namespaced `id`, so
it never matched for any market install, falling through to
getPlugin(bareId) — which also only ever indexed PLUGIN_MAP by the
namespaced id — and threw "Không có plugin ... trong registry" even
though the plugin was installed and actively rendering. getPluginSdk
(used by plugins for module-scope stores, e.g. Kafka's/RabbitMQ's
consumer stores that must survive route changes) had the exact same
bug, unconditionally.

Fix: track the original, un-prefixed id separately as `baseId` on
PluginManifest/PluginSdk (installer.ts sets it whenever it builds a
market-prefixed registry id), match usePluginSdkFor's context check
against `baseId` instead of `id`, and have getPlugin() fall back to a
baseId scan when the exact (possibly-prefixed) key misses. The `id` field
itself is untouched everywhere it actually needs to stay namespaced
(storage keys, audit log, PLUGIN_MAP routing) — only the plugin's
self-lookup path changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2
@DianaSensei
DianaSensei enabled auto-merge (squash) September 18, 2026 04:12
@codecov

codecov Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 42.07%. Comparing base (3b45449) to head (4629018).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #153      +/-   ##
==========================================
+ Coverage   42.03%   42.07%   +0.04%     
==========================================
  Files         299      299              
  Lines       19848    19864      +16     
  Branches     4904     4909       +5     
==========================================
+ Hits         8343     8358      +15     
  Misses      10508    10508              
- Partials      997      998       +1     
Flag Coverage Δ
frontend 37.20% <100.00%> (+0.05%) ⬆️
rust 65.43% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/components/ExtensionInstallDialog.tsx 81.81% <100.00%> (+0.56%) ⬆️
src/components/SettingsExtensionInstaller.tsx 66.66% <100.00%> (+0.42%) ⬆️
src/lib/market.ts 91.11% <ø> (ø)
src/platform/installer.ts 89.58% <100.00%> (+1.21%) ⬆️
src/platform/registry.ts 91.83% <100.00%> (+0.17%) ⬆️
src/platform/sdk.ts 68.00% <100.00%> (+0.32%) ⬆️
src/platform/types.ts 100.00% <ø> (ø)
src/platform/usePluginSdkFor.ts 75.00% <ø> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

ℹ️ No critical issues — one coverage gap inline, plus a limitation worth tracking.

Reviewed changes

  • baseId added to manifests/SDK — PluginManifest.baseId carries the un-prefixed plugin id; createPluginSdk defaults it to id, so compile-time and non-market installs are unchanged.
  • installer.ts sets baseId — market-prefixed records keep the original bare id in baseId while id/route/storage/audit stay namespaced.
  • getPlugin falls back to a baseId scan — a bare-id lookup now resolves a market install whose registry id is prefixed.
  • usePluginSdkFor matches context by baseId — in-tree plugin code gets its own SDK instead of falling through to the throw.
  • Tests — registry lookup by prefixed id and by bare baseId; baseId defaulting and override in the SDK.

Verified tsc --noEmit and the targeted platform suite (registry.installer, sdk, installer) — all green.

ℹ️ Module-scope getPluginSdk(bareId) cannot disambiguate two markets shipping the same id

The in-tree path (usePluginSdkFor) is now correct because the context carries the exact install's baseId. The module-scope path (getPluginSdk) has no such context: with two markets shipping the same bare id, getPlugin(bareId) returns whichever install registered first, so the second install's module-scope stores silently get the first install's SDK — wrong storage namespace, audit id, and permission set. This is the exact two-market collision the <marketId>- prefix was built to support, and the fallback is what newly allows a wrong-but-successful resolution instead of a throw. No clean fix without bundle-scoped identity injection, so it may well be an accepted limitation — but worth an explicit decision rather than a comment.

Technical details
# Module-scope plugin identity is ambiguous across markets

## Affected sites
- `src/platform/registry.ts:145` — `records.find((p) => p.baseId === id)` returns the first registration sharing `baseId`; no `baseId` clash check exists in `registerManifest`.
- `src/platform/usePluginSdkFor.ts:19` — `getPluginSdk` caches by the bare `pluginId`, so the second install never gets a distinct SDK instance.
- `src/components/tools/mockserver/requestLogStore.ts:34`, `src/components/tools/apiclient/request.ts:23` — module-scope consumers that call `getPluginSdk(bareId)`.

## Required outcome
- Decide whether duplicate-`baseId` installs across markets are acceptable as first-wins, or whether module-scope identity needs to be bundle-scoped (host injects the resolved registry id before `import()` of the bundle) so each install resolves itself.

## Open questions for the human
- Is installing the same plugin id from two different markets a supported scenario worth solving now, or an accepted edge to document/track?

Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

Comment thread src/platform/installer.ts Outdated
Both existing marketId tests checked the prefixed id/route but never
baseId itself — deleting installer.ts's `baseId: record.manifest.id`
line kept every test green while reintroducing the exact bug this PR
fixes (usePluginSdkFor/getPluginSdk throwing "not in registry" for any
market-installed plugin). Confirmed by temporarily removing that line
locally: these two new assertions fail, everything else still passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2

Copy link
Copy Markdown
Owner Author

Re: the module-scope getPluginSdk(bareId) two-market-same-id ambiguity Pullfrog flagged — this is an accepted, already-documented limitation, not something this PR needs to solve. registry.ts's getPlugin() doc comment (added in this PR) calls it out explicitly: falls back to "bản ghi ĐĂNG KÝ TRƯỚC" (first-registered) and notes that's a pre-existing limitation of a module-scope singleton identifying itself by one id string, not a new failure mode this fallback introduces.

Before this PR, installing the same plugin id from two markets meant BOTH installs' getPluginSdk/usePluginSdkFor calls threw outright (this bug). After it, the common case (one market) works correctly, and the two-market-same-id edge degrades to "second install silently shares the first's SDK identity" instead of "both throw" — a narrower, more usable failure mode, not a regression. Solving it properly needs bundle-scoped identity injection (the host would have to tell each plugin bundle its own resolved registry id at load time, which the plugin's own module-scope code would need to read instead of hardcoding its bare id) — real scope creep for a bug-fix PR, and this repo doesn't currently support/document installing the same plugin from two markets as a use case anyone's asked for.

Not blocking merge; happy to open a follow-up if this becomes a real scenario.


Generated by Claude Code

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

✅ No new issues found.

Reviewed changes

  • Coverage gap closed — installer.test.ts now asserts baseId in both the non-market and market-prefixed cases, so deleting installer.ts's baseId: record.manifest.id line fails these assertions instead of silently reintroducing the original "not in registry" bug.

The remaining body note from the prior review (module-scope getPluginSdk(bareId) is first-wins when two markets ship the same bare id) is unchanged and still stands as an accepted-limitation decision for the author.


Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

…y it

Follow-up to the market-prefix fix: a plugin installed from a bare URL
(no marketId, keeps its unprefixed id) and the SAME plugin installed
again from a market (id gets prefixed <marketId>-) don't collide on
id/route/order at all — those checks all compare different strings
between the two records, so both registered successfully.

That's worse than it sounds: getPluginSdk(bareId) (module-scope, no
React context — used by Kafka's/RabbitMQ's consumer stores) looks up
PLUGIN_MAP by the exact string first, before ever trying the baseId
fallback. With both plugins registered, that exact lookup hits the
bare-URL install directly regardless of which bundle's code is actually
calling — the market-installed instance's module-scope calls would
silently receive the URL install's SDK: wrong storage namespace, wrong
audit id, wrong permission set. Two independent installs would leak
into each other's persisted state.

Added a seenBaseIds guard alongside the existing seenIds/seenRoutes/
seenOrders in registerManifest — a second registration sharing an
effective baseId (m.baseId ?? m.id) is now rejected into PLUGIN_ERRORS
like any other collision, instead of silently coexisting and letting
getPlugin()'s baseId fallback (and getPluginSdk's cache) misattribute
identity. Also simplified getPlugin()'s doc comment: with duplicates
now rejected at registration, its baseId scan can only ever match one
record, no more first-wins ambiguity to describe.

New test reproduces the exact scenario (URL install + market install of
the same plugin) and confirms it now fails closed instead of silently
misidentifying; confirmed by temporarily reverting the guard, which
makes the test fail for the right reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2

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

✅ No new issues found.

Reviewed changes

  • Duplicate baseId now rejected at registration — registerManifest keys a new seenBaseIds map by m.baseId ?? m.id and refuses the second registration sharing a bare id, so getPlugin(baseId)/getPluginSdk(baseId) can only ever resolve one record. This closes the module-scope ambiguity raised in the prior review rather than leaving it as an accepted limitation.
  • Regression test added — registry.installer.test.ts covers a URL-installed copy plus a market-installed copy of the same bare id (all of id/route/order distinct) and asserts the second is rejected with a PLUGIN_ERRORS entry. It fails without the seenBaseIds clause, so it is real coverage.
  • Doc comments updated — getPlugin's comment no longer describes duplicate-baseId ambiguity as a known limitation, matching the new invariant.

Verified tsc --noEmit and the platform suite (registry.installer, registry, migration, sdk, installer) — all green.


Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

Supersedes this PR's earlier baseId field with what it was really
approximating: every plugin now has an `id` (self-authored, what the
plugin's own bundle calls itself via usePluginSdkFor/getPluginSdk) and a
`group` (where it was installed from — "core" for the 26 compile-time
plugins, "url" for a bare-URL install, or the marketId for a market
install). Metadata only: storage/secrets/audit keys stay keyed by bare
`id` exactly as before, so no migration for anyone's existing data.

`id` still ends up unique system-wide in practice, not just per group:
usePluginSdkFor/getPluginSdk are called by a plugin's own bundle code
with just the bare id string, no group — module-scope calls (Kafka's/
RabbitMQ's background consumer stores) have no React context to resolve
group from. Letting the same id genuinely coexist under two groups would
make that lookup ambiguous and silently misattribute storage/audit/
permissions to the wrong plugin.

So installer.ts now enforces it at the one point that matters — install
time, not registry time: `assertNoConflictingInstall(id, marketId)`
checks the already-installed list for the same bare id under a
DIFFERENT group and throws a clear "uninstall the old one first" error
before Rust ever writes to disk, called from both install call sites
(ExtensionInstallDialog, SettingsExtensionInstaller) using the manifest
they've already fetched for the confirmation screen — not fetched a
second time inside installArtifact itself, which would have doubled a
network round-trip AND changed the call count contract every existing
install-flow test asserted on.

registry.ts drops the seenBaseIds/getPlugin-fallback machinery entirely
now that id is flat again: registerManifest just gets group defaulted to
'core' for the 26 compile-time plugins that don't declare it (installer.ts
always declares it explicitly for runtime installs), and getPlugin(id)
is back to a plain PLUGIN_MAP.get(id) — no ambiguity left to fall back on.

Route generation for installed plugins is now uniform for both URL and
market installs: /installed/<group>/<id>, replacing the old asymmetric
"market gets a generated route, URL install keeps its own" split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2
@DianaSensei DianaSensei changed the title Fix market-installed plugins throwing "not in registry" from their own code Plugin identity: explicit group + id, with an install-time conflict gate Sep 18, 2026
Codecov flagged a partial branch: the new `preview?.kind === 'plugin'`
check gating assertNoConflictingInstall had only its true side (plugin
install) exercised by any test — no test ever clicked Install for a
kind=service preview. Extended the existing service-preview test to also
install, asserting it skips straight to artifact_installer_install with
no list call in between (assertNoConflictingInstall only applies to
plugins).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2

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

ℹ️ No critical issues — the redesign is sound; a couple of minor suggestions inline plus stale docs.

Reviewed changes

  • baseId replaced by group + bare id — PluginManifest.group?, PluginSdk.group, and a required PluginRecord.group; id is the bare, self-authored id again and remains the storage/secrets/audit namespace key.
  • installer.ts emits bare id + group + a uniform route — installedPluginManifests() now generates /installed/<group>/<id> for every runtime install and no longer prefixes the registry id.
  • New install-time conflict gate — assertNoConflictingInstall(id, marketId) rejects installing a bare id already installed under a different group, called from both install UIs before installArtifact.
  • registry.ts simplified — flat seenIds and plain PLUGIN_MAP.get(id); the seenBaseIds fallback machinery is removed.
  • Tests — conflict-gate cases, bare-id market registration + duplicate-id rejection, group defaulting, and updated id/route assertions.

Verified tsc --noEmit and the targeted platform suite (installer, registry.installer, sdk, registry) — 79/79 green.

ℹ️ Nitpicks

  • docs/plugin-sdk/02-manifest.md's [^market-id] footnote (lines 45-55) still documents the removed behavior — two markets with the same id coexisting, registry id <marketId>-<id>, route /installed/<marketId>-<id>, and bare-URL installs keeping their own route. All three are now false; since this is the plugin-author contract, it should be updated.
  • src/lib/market.ts:95-100 justifies the kebab-case market-id rule by "installedPluginManifests() ... ghép ... (${marketId}-${pluginId})" — that join no longer exists.
  • Bare-URL installs now use the generated /installed/url/<id> route instead of the route they declare in their remote manifest, so an already-installed plugin's URL changes with no redirect. Probably fine, but worth a release note.

Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

Comment thread src/platform/installer.ts
// tố market, xem `PluginManifest.group`. `installArtifact()` chặn cài
// nếu id này đã bị một group khác chiếm, nên tại registry nó vẫn
// nhất quán duy nhất toàn app dù không ép kiểu ở đây.
id: record.manifest.id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Making id bare changes the storage/secrets namespace for existing market installs: on main a market install registered as <marketId>-<id>, and storageKey/vaultKey/audit all key off that registry id, so its data lives under devtool:official-<id>:… / official-<id>/…. After this change the plugin reads devtool:<id>:… / <id>/… with no migration, so the PR body's "no data migration for anyone's existing installs" isn't accurate. Impact is probably small (market plugins couldn't resolve an SDK before this fix, so they likely never wrote), but the claim should be corrected or a one-time move added.

Technical details
# Market-install storage namespace changes from `<marketId>-<id>` to `<id>`

## Affected sites
- `src/platform/installer.ts:538` — `id: record.manifest.id` (was `id: registryId`, `registryId = marketId ? `${marketId}-${id}` : id`).
- `src/platform/sdk.ts:228` — `storageKey(id, key)` → `devtool:<id>:<key>`.
- `src/platform/secrets.ts:51` — `vaultKey(id, key)` → `<id>/<key>`.
- `src/platform/audit.ts` — in-RAM only, so audit history is unaffected.

## Required outcome
- Either add a one-time migration (the `usePluginState` `legacyKey` mechanism is the house pattern) from `devtool:<marketId>-<id>:*` / `<marketId>-<id>/*` to the bare-id namespace, or correct the claim that no migration is needed.

## Open questions for the human
- Are there any released market installs with persisted settings/secrets worth migrating, or is this a no-op in practice because those plugins were broken?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Correct that the namespace string itself changes — my claim was imprecise. Answering the actual question though: no data to migrate. usePluginSdkFor(id) threw before any market-installed plugin's component could reach a single sdk.storage/sdk.secrets call — that's the exact bug this whole PR chain fixes. So the old <marketId>-<id> namespace was never written to for any market install; there's nothing under it to move. Documented this reasoning inline at installer.ts's id: record.manifest.id line in c7946d2 rather than leaving it only in commit history, per your point that the claim should be justified, not just asserted.


Generated by Claude Code

Comment thread src/platform/installer.ts

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

✅ No new issues found.

Reviewed changes

  • kind=service install path covered — SettingsExtensionInstaller.test.tsx now installs from a service preview and asserts the flow reaches artifact_installer_install with no preceding listInstalledPlugins call, locking in that assertNoConflictingInstall runs only for kind: "plugin".

The new test can fail for the right reason: widening the preview?.kind === 'plugin' guard to all kinds would insert an artifact_installer_list call that consumes the mocked install response, so the install assertion would break.


Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

Pullfrog review, PR #153:

1. assertNoConflictingInstall() only scanned listInstalledPlugins() (the
   on-disk index) — it had no visibility into the 26 compile-time plugins,
   which never appear there. Installing a plugin whose id collides with a
   built-in (e.g. "json") passed the gate, got written to disk, and was
   only THEN silently rejected by registerManifest's flat seenIds on the
   next launch — exactly the deferred-to-restart PLUGIN_ERRORS outcome
   this gate exists to front-load.

   Fixed by adding an optional `registeredGroup` parameter: the two UI call
   sites (ExtensionInstallDialog, SettingsExtensionInstaller) now pass
   `getPlugin(id)?.group` — already exported from '@/platform', no new
   registry->installer import needed (installer.ts can't import registry.ts;
   registry.ts already imports installer.ts). A `group: 'core'` conflict
   gets its own error message ("trùng id với một tool có sẵn") since
   "uninstall the old one" doesn't apply to a built-in tool.

2. Corrected the PR's "no data migration needed" claim into an actual
   justification rather than an assertion: storage/secrets namespacing for
   a market install did change from the old `<marketId>-<id>` prefix to a
   bare `id`, but no market-installed plugin ever successfully read/wrote
   sdk.storage/sdk.secrets before this PR — usePluginSdkFor(id) threw
   before the component could reach any such call. Documented inline at
   the exact line the namespace is set, instead of just in commit history.

3. Updated the two doc/comment sites Pullfrog found still describing the
   removed <marketId>-<id> registry-id scheme: 02-manifest.md's
   plugin-author-facing footnote, and market.ts's kebab-case rationale
   comment.

New tests cover the registeredGroup parameter (core collision throws its
own message, matching group is a no-op); confirmed by temporarily
reverting the registeredGroup branch, which makes the new test fail for
the right reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2

Copy link
Copy Markdown
Owner Author

The github-advanced-security check (GitHub's own Copilot Autofix agent, run as a dynamic dynamic/agents/github-advanced-security workflow — not a file in this repo) failed with:

CAPIError: 400 The requested model is not supported.
  at ... getCompletionWithToolsCore ...

This is a failure in GitHub's own Copilot backend configuration (sweagent-capi:claude-opus-5[ReasoningEffort=medium] rejected by their API), not something in this PR's diff — every actual CI check (typecheck/tests, CodeQL analyze, SonarCloud, npm/cargo audit, secret scan, dependency review) passed. There's no fix I can port for a third-party backend outage, and rerun_failed_jobs on this run is rejected (403 This workflow run cannot be retried) since it's not a standard rerunnable Actions workflow. Leaving this PR watched; will retry if a rerun becomes possible or the underlying service recovers.


Generated by Claude Code

Codecov flagged a gap: the two new registeredGroup tests only covered
the "core" collision message and the "matches, falls through to disk
scan" case — never the plain "already installed under a different
group" throw reached via the registeredGroup parameter itself (as
opposed to the pre-existing disk-scan path that produces the same
message text).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2

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

ℹ️ No critical issues — one minor suggestion inline.

Reviewed changes

  • registeredGroup added to the install-time conflict gate — assertNoConflictingInstall now accepts the registry group of an existing record and rejects a core id collision with its own "tool có sẵn" message; both install UIs pass getPlugin(id)?.group, closing the built-in-id blind spot from the prior review.
  • Docs and comments realigned to the group + bare-id model — the [^market-id] footnote in docs/plugin-sdk/02-manifest.md and the kebab-case rationale in src/lib/market.ts no longer describe the removed <marketId>-<id> scheme; the storage-namespace change is now justified inline at installer.ts.
  • Tests — new registeredGroup cases: a core collision throws the built-in-specific message without touching listInstalledPlugins, and a matching group falls through to the on-disk scan.

Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

Comment thread src/platform/installer.ts Outdated

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

ℹ️ No new issues in the latest commit — test-only coverage.

Reviewed changes

  • Non-core registeredGroup branch covered — installer.test.ts now asserts that a registry record under a different non-core group throws the "gỡ bản đó" message before listInstalledPlugins is called, mirroring the existing core-collision case and failing if that branch is removed.

Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

assertNoConflictingInstall's early registeredGroup check threw for ANY
registeredGroup !== newGroup, not just 'core'. But the in-memory registry
(PLUGIN_MAP) only loads once at app bootstrap and never updates when a
plugin is installed/uninstalled during a running session. So a user who
uninstalls a market plugin and immediately reinstalls the same id from a
different source (without restarting) would be incorrectly blocked by a
stale registeredGroup — even though the disk scan below it would
correctly show no conflict.

Fix: restrict the early-throw to registeredGroup === 'core' only, since
compile-time plugins never disappear from the registry mid-session and
are safe to short-circuit on. Every other registeredGroup value now
falls through unconditionally to the disk-scan-based check, which is
always accurate for non-compile-time plugins.

Verified by temporarily reverting the fix locally and confirming the new
STALE test fails for exactly the right reason (throws the "uninstall the
old one first" error when it should resolve cleanly), then restoring it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCypCuViyQDKGWxWspXKs2
@sonarqubecloud

Copy link
Copy Markdown

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

ℹ️ No new issues in this delta — the prior false-block finding is fixed.

Reviewed changes

  • Non-core registeredGroup short-circuit removed — assertNoConflictingInstall now early-throws only when registeredGroup === 'core'; every other registry group falls through to the on-disk listInstalledPlugins() scan, so a stale registry record left by an in-session uninstall no longer false-blocks a legitimate reinstall, while a real non-core conflict still throws.
  • Tests updated — the old non-core short-circuit test is replaced by a stale-record case (empty disk → resolves) plus a real-conflict case (disk still holds the other group → throws), and both fail for the right reason if the branch regresses.

The remaining open thread from the 2414485 review (the PR body still describes the storage namespace as "unchanged from before this PR" for market installs) is untouched by this commit and keeps this review non-approving.


Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

@DianaSensei DianaSensei left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — the PR body's "unchanged from before this PR" claim was inaccurate; updated it to correctly describe the namespace as changed for market installs, with the actual justification for why no data migration is needed (market installs crashed in usePluginSdkFor before ever reaching a storage/secrets call under the old scheme, so there's no data to lose) — same reasoning already in the inline comment above installedPluginManifests() in installer.ts.


Generated by Claude Code

@DianaSensei
DianaSensei merged commit e6808d1 into main Sep 18, 2026
20 of 21 checks passed
@DianaSensei
DianaSensei deleted the plugin-market-baseid-fix branch September 18, 2026 06:19
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.

2 participants