diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a85092b07..b00ba522bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky. +- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc. - Plugins/Codex: allow the official npm Codex plugin to install without the unsafe-install override, keep `/codex` command ownership, and cover the real npm Docker live path through managed `.openclaw/npm` dependencies plus uninstall failure proof. ## 2026.5.2 diff --git a/extensions/diagnostics-otel/package.json b/extensions/diagnostics-otel/package.json index 056065595f0..1aac7ccb753 100644 --- a/extensions/diagnostics-otel/package.json +++ b/extensions/diagnostics-otel/package.json @@ -30,7 +30,7 @@ "install": { "clawhubSpec": "clawhub:@openclaw/diagnostics-otel", "npmSpec": "@openclaw/diagnostics-otel", - "defaultChoice": "clawhub", + "defaultChoice": "npm", "minHostVersion": ">=2026.4.25" }, "compat": { diff --git a/extensions/diagnostics-prometheus/package.json b/extensions/diagnostics-prometheus/package.json index 4c6c34a5cdc..a5d4fc08ee2 100644 --- a/extensions/diagnostics-prometheus/package.json +++ b/extensions/diagnostics-prometheus/package.json @@ -17,7 +17,7 @@ "install": { "clawhubSpec": "clawhub:@openclaw/diagnostics-prometheus", "npmSpec": "@openclaw/diagnostics-prometheus", - "defaultChoice": "clawhub", + "defaultChoice": "npm", "minHostVersion": ">=2026.4.25" }, "compat": { diff --git a/package.json b/package.json index c082991b526..b4e900561ca 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "!dist/extensions/diagnostics-otel/**", "!dist/extensions/diagnostics-prometheus/**", "!dist/extensions/diffs/**", + "!dist/extensions/discord/**", "!dist/extensions/feishu/**", "!dist/extensions/google-meet/**", "!dist/extensions/googlechat/**", diff --git a/src/cli/plugins-location-bridges.test.ts b/src/cli/plugins-location-bridges.test.ts new file mode 100644 index 00000000000..4b2167f93f8 --- /dev/null +++ b/src/cli/plugins-location-bridges.test.ts @@ -0,0 +1,145 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js"; +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; + +const readPersistedInstalledPluginIndexMock = vi.fn(); +const loadPluginManifestRegistryForInstalledIndexMock = vi.fn(); + +vi.mock("../plugins/installed-plugin-index-store.js", () => ({ + readPersistedInstalledPluginIndex: (...args: unknown[]) => + readPersistedInstalledPluginIndexMock(...args), +})); + +vi.mock("../plugins/manifest-registry-installed.js", () => ({ + loadPluginManifestRegistryForInstalledIndex: (...args: unknown[]) => + loadPluginManifestRegistryForInstalledIndexMock(...args), +})); + +const { listPersistedBundledPluginLocationBridges } = await import("./plugins-location-bridges.js"); + +function makeIndex(record: InstalledPluginIndex["plugins"][number]): InstalledPluginIndex { + return { + version: 1, + hostContractVersion: "2026.5.2", + compatRegistryVersion: "test", + migrationVersion: 1, + policyHash: "test", + generatedAtMs: 1, + refreshReason: "test", + installRecords: {}, + plugins: [record], + }; +} + +function makeRegistry(pluginId: string): PluginManifestRegistry { + return { + plugins: [ + { + id: pluginId, + name: pluginId, + rootDir: `/app/dist/extensions/${pluginId}`, + source: `/app/dist/extensions/${pluginId}/index.js`, + origin: "bundled", + channels: [pluginId], + providers: [], + cliBackends: [], + syntheticAuthRefs: [], + nonSecretAuthMarkers: [], + skills: [], + settingsFiles: [], + hooks: [], + configContracts: [], + activation: {}, + startup: {}, + packageInstall: { + clawhubSpec: `clawhub:@openclaw/${pluginId}`, + npmSpec: `@openclaw/${pluginId}`, + defaultChoice: "clawhub", + }, + }, + ], + diagnostics: [], + } as unknown as PluginManifestRegistry; +} + +describe("listPersistedBundledPluginLocationBridges", () => { + beforeEach(() => { + readPersistedInstalledPluginIndexMock.mockReset(); + loadPluginManifestRegistryForInstalledIndexMock.mockReset(); + }); + + it("keeps persisted bundled relocations npm-first for launch", async () => { + readPersistedInstalledPluginIndexMock.mockResolvedValue( + makeIndex({ + pluginId: "diagnostics-otel", + manifestPath: "/app/dist/extensions/diagnostics-otel/openclaw.plugin.json", + manifestHash: "hash", + source: "/app/dist/extensions/diagnostics-otel/index.js", + rootDir: "/app/dist/extensions/diagnostics-otel", + origin: "bundled", + enabled: true, + startup: {}, + compat: [], + packageInstall: { + defaultChoice: "clawhub", + clawhub: { + spec: "clawhub:@openclaw/diagnostics-otel", + packageName: "@openclaw/diagnostics-otel", + exactVersion: false, + }, + npm: { + spec: "@openclaw/diagnostics-otel", + packageName: "@openclaw/diagnostics-otel", + selectorKind: "none", + exactVersion: false, + pinState: "floating-without-integrity", + }, + warnings: [], + }, + }), + ); + loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue( + makeRegistry("diagnostics-otel"), + ); + + await expect(listPersistedBundledPluginLocationBridges({})).resolves.toEqual([ + { + bundledPluginId: "diagnostics-otel", + pluginId: "diagnostics-otel", + preferredSource: "npm", + npmSpec: "@openclaw/diagnostics-otel", + channelIds: ["diagnostics-otel"], + }, + ]); + }); + + it("does not create a relocation bridge without npm metadata", async () => { + readPersistedInstalledPluginIndexMock.mockResolvedValue( + makeIndex({ + pluginId: "diagnostics-otel", + manifestPath: "/app/dist/extensions/diagnostics-otel/openclaw.plugin.json", + manifestHash: "hash", + source: "/app/dist/extensions/diagnostics-otel/index.js", + rootDir: "/app/dist/extensions/diagnostics-otel", + origin: "bundled", + enabled: true, + startup: {}, + compat: [], + packageInstall: { + defaultChoice: "clawhub", + clawhub: { + spec: "clawhub:@openclaw/diagnostics-otel", + packageName: "@openclaw/diagnostics-otel", + exactVersion: false, + }, + warnings: [], + }, + }), + ); + loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue( + makeRegistry("diagnostics-otel"), + ); + + await expect(listPersistedBundledPluginLocationBridges({})).resolves.toEqual([]); + }); +}); diff --git a/test/plugin-clawhub-release.test.ts b/test/plugin-clawhub-release.test.ts index ebfbaecd2af..d5363f8690e 100644 --- a/test/plugin-clawhub-release.test.ts +++ b/test/plugin-clawhub-release.test.ts @@ -104,8 +104,8 @@ describe("collectClawHubPublishablePluginPackages", () => { }); }); -describe("OpenClaw ClawHub-preferred plugin metadata", () => { - const clawHubPreferredPlugins = [ +describe("OpenClaw dual-published plugin metadata", () => { + const dualPublishedPlugins = [ { extensionId: "diagnostics-otel", packageName: "@openclaw/diagnostics-otel", @@ -117,7 +117,7 @@ describe("OpenClaw ClawHub-preferred plugin metadata", () => { ] as const; it("keeps diagnostics plugins selectable through both ClawHub and npm release paths", () => { - const packageNames = clawHubPreferredPlugins.map((plugin) => plugin.packageName); + const packageNames = dualPublishedPlugins.map((plugin) => plugin.packageName); const clawHubPublishable = collectClawHubPublishablePluginPackages(undefined, { packageNames, }); @@ -128,7 +128,7 @@ describe("OpenClaw ClawHub-preferred plugin metadata", () => { expect(clawHubPublishable.map((plugin) => plugin.packageName)).toEqual(packageNames); expect(npmPublishable.map((plugin) => plugin.packageName)).toEqual(packageNames); - for (const plugin of clawHubPreferredPlugins) { + for (const plugin of dualPublishedPlugins) { const packageJson = JSON.parse( readFileSync(`extensions/${plugin.extensionId}/package.json`, "utf8"), ) as { @@ -147,7 +147,7 @@ describe("OpenClaw ClawHub-preferred plugin metadata", () => { expect(packageJson.openclaw?.install).toMatchObject({ clawhubSpec: `clawhub:${plugin.packageName}`, - defaultChoice: "clawhub", + defaultChoice: "npm", npmSpec: plugin.packageName, }); expect(packageJson.openclaw?.release).toMatchObject({