Skip to content

fix(cli): register marketplace plugin installs#3245

Open
roboomp wants to merge 6 commits into
mainfrom
farm/07010305/marketplace-plugin-registration
Open

fix(cli): register marketplace plugin installs#3245
roboomp wants to merge 6 commits into
mainfrom
farm/07010305/marketplace-plugin-registration

Conversation

@roboomp

@roboomp roboomp commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Repro

A temp marketplace fixture installed with MarketplaceManager.installPlugin("hello-plugin", "test-marketplace") cached the plugin and wrote installed_plugins.json, but the runtime plugin root had no node_modules/hello-plugin, no omp-plugins.lock.json, and getAllPluginExtensionPaths(process.cwd()) returned [].

Cause

packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts only maintained the Claude-compatible marketplace registry/cache. The runtime loader in packages/coding-agent/src/extensibility/plugins/loader.ts discovers plugins from the plugin root node_modules tree plus omp-plugins.lock.json#plugins, so marketplace installs were invisible even after successful CLI installation.

Fix

  • Added marketplace runtime bookkeeping: install creates/replaces the package symlink or junction and writes omp-plugins.lock.json with the previous enabled state preserved.
  • Synced marketplace enable/disable and uninstall paths with runtime lockfile state, including removing the runtime symlink and settings on uninstall.
  • Added regression coverage that proves marketplace installs are visible to getEnabledPlugins() and that uninstall/reinstall/enablement keep runtime state consistent.
  • Added the coding-agent changelog entry for Marketplace plugin install does not symlink/register plugin, so slash commands and extensions are never loaded #3244.

Verification

bun test packages/coding-agent/test/marketplace/manager.test.ts passed with 39 tests; bun check passed; the repro script now reports nodeModulesLinkExists: true, lockfileExists: true, and a non-empty extensionPaths entry through node_modules/hello-plugin/extensions/index.ts. Fixes #3244

Added runtime symlink and lockfile bookkeeping for marketplace installs and removals so installed plugins are visible to the plugin loader. Updated manager coverage for install, uninstall, enablement, and loader discovery.\n\nFixes #3244

@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: dc9be7c9de

ℹ️ 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".

await this.#removeRuntimePlugin(scope, previousPackageName);
}
}
await this.#registerRuntimePlugin(scope, packageName, cachePath, version, wasDisabled ? false : undefined);

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 Make project-scoped runtime installs discoverable

When installPlugin(..., { scope: "project" }) reaches this call, #registerRuntimePlugin writes the symlink and omp-plugins.lock.json under the project registry directory, but the runtime loader still enumerates only getPluginsNodeModules(home) / getPluginsLockfile(home) in loader.ts#getEnabledPlugins and uses project data only for overrides. In the documented omp plugin install --scope project name@marketplace flow, the new runtime bookkeeping is therefore written to a tree that slash commands/extensions never read, so project-scoped marketplace packages remain invisible.

Useful? React with 👍 / 👎.

async #resolvePluginPackageName(installPath: string, fallbackName: string): Promise<string> {
try {
const pkg: { name?: unknown } = await Bun.file(path.join(installPath, "package.json")).json();
return typeof pkg.name === "string" && pkg.name.length > 0 ? pkg.name : fallbackName;

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 Validate package names before linking

If a marketplace plugin's copied package.json contains a malformed name such as ../foo, this returns it unchecked and #registerRuntimePlugin later uses it in path.join(node_modules, packageName) before rm and symlink. Marketplace installs copy arbitrary plugin directories rather than going through npm package-name validation, so bad metadata can delete or replace paths outside node_modules during install/uninstall; validate the manifest name and/or verify the resolved link stays inside the plugin root before using it as a path component.

Useful? React with 👍 / 👎.

Made getEnabledPlugins walk both the user plugins root and the active project plugins root, with project entries shadowing same-named user entries. This makes `omp plugin install --scope project name@marketplace` discoverable to slash commands and the extension loader, matching the existing user-scope path. Added regression coverage that exercises the project-scope install through the runtime loader.\n\nFollow-up to #3244 review.
@roboomp

roboomp commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the project-scope feedback in 1d500ce: getEnabledPlugins now also enumerates <projectAnchor>/.omp/plugins via resolveActiveProjectRegistryPath, with project entries shadowing same-named user entries, so omp plugin install --scope project name@marketplace is discoverable by slash commands and extensions. Added a regression test that installs into the project scope and asserts the loader surfaces the plugin from the project plugins root.

roboomp added 2 commits June 22, 2026 08:51
Rejected malformed marketplace package.json names before runtime link creation and guarded node_modules link paths lexically.
@roboomp

roboomp commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author
  • Validated marketplace plugin package.json names before runtime registration/removal, rejecting traversal-style names like ../outside.
  • Added a lexical node_modules containment guard for runtime symlink paths so existing symlinks cannot confuse the safety check.
  • Added regression coverage proving a malformed package name leaves the outside path untouched and no runtime tree registered.

@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: 661b3e1672

ℹ️ 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 (projectRegistryPath) {
const projectRoot = path.dirname(projectRegistryPath);
if (projectRoot !== userRoot) {
projectPlugins = await collectPluginsAtRoot(projectRoot, projectOverrides);

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 Preserve project scope for runtime plugin roots

When this new branch mixes project-root installs into the same InstalledPlugin[] shape as user installs, the scope is lost; packages/coding-agent/src/discovery/omp-extension-roots.ts:listInstalledPluginRoots still maps every returned plugin to { level: "user" }. For a marketplace package installed with --scope project that contributes OMP commands/tools/skills, those contributions are now surfaced as user-level metadata, so level-based diagnostics/source controls treat project config as user config. Carry the root scope through before merging or return scoped roots separately.

Useful? React with 👍 / 👎.

Comment on lines +807 to +808
await fs.rm(linkPath, { recursive: true, force: true });
await fs.symlink(cachePath, linkPath, process.platform === "win32" ? "junction" : "dir");

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 Guard runtime links against package-name collisions

When two installed marketplace entries in the same scope resolve to the same package.json name (for example a forked plugin from another marketplace), this unconditional remove/symlink overwrites the first plugin's runtime link while both plugin IDs remain in installed_plugins.json; later uninstalling either ID can also delete the shared lock entry and leave the other installed-but-invisible. Since buildPluginId permits distinct name@marketplace entries but the runtime tree is keyed only by package name, detect this collision or reference-count it before replacing the link.

Useful? React with 👍 / 👎.

roboomp added 2 commits June 22, 2026 08:59
Carried user/project root scope through enabled plugin enumeration so project marketplace installs keep project-level discovery metadata.
@roboomp

roboomp commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author
  • Carried user/project scope through getEnabledPlugins before project entries shadow user entries.
  • Mapped installed plugin extension roots from the carried scope instead of forcing every root to user.
  • Added regression coverage for a project-scoped installed plugin contributing project-level sub-discovery metadata.

@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: dc74d09a8a

ℹ️ 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".

// Project entries shadow user entries with the same package name.
const merged = new Map<string, ScopedInstalledPlugin>();
for (const plugin of userPlugins) merged.set(plugin.name, plugin);
for (const plugin of projectPlugins) merged.set(plugin.name, plugin);

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 Preserve project shadowing by plugin ID

When the same marketplace plugin ID is installed in both user and project scopes but the two versions resolve to different runtime package names (for example, an upgrade adds or renames package.json#name), listInstalledPlugins marks the user entry as shadowed by the project entry, but this merge only replaces entries with the same package name. In that project, getEnabledPlugins() will return both the old user package and the project package, so both sets of commands/extensions can load despite the project install being intended to take precedence for that plugin ID.

Useful? React with 👍 / 👎.

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

Labels

vouched Passed the vouch gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Marketplace plugin install does not symlink/register plugin, so slash commands and extensions are never loaded

1 participant