diff --git a/docs/plugins/codex-native-plugins.md b/docs/plugins/codex-native-plugins.md index 0254ced32acd..25948d243eb5 100644 --- a/docs/plugins/codex-native-plugins.md +++ b/docs/plugins/codex-native-plugins.md @@ -28,7 +28,10 @@ working. - The target Codex app-server can see the expected marketplace, plugin, and app inventory. - Migration supports only `openai-curated` plugins that it observed as - source-installed in the source Codex home. + source-installed in the source Codex home. Codex serves the same catalog to + API-key and Bedrock accounts under the `openai-api-curated` wire name; + OpenClaw treats both names as the one curated catalog, so configured + `openai-curated` plugins resolve from either. - Manually configured `workspace-directory` plugins must already appear installed and enabled under their exact marketplace-qualified identity in `plugin/installed`. Their owned apps must be accessible and callable for the diff --git a/extensions/codex/src/app-server/computer-use.ts b/extensions/codex/src/app-server/computer-use.ts index be55de47e6d8..d3a258bf569a 100644 --- a/extensions/codex/src/app-server/computer-use.ts +++ b/extensions/codex/src/app-server/computer-use.ts @@ -187,6 +187,7 @@ const CURATED_MARKETPLACE_POLL_INTERVAL_MS = 2_000; const COMPUTER_USE_MARKETPLACE_NAME_PRIORITY = [ "openai-bundled", "openai-curated", + "openai-api-curated", "openai-curated-remote", "local", ]; diff --git a/extensions/codex/src/app-server/plugin-inventory.test.ts b/extensions/codex/src/app-server/plugin-inventory.test.ts index cb96ab6c57cb..e66e3ddd483b 100644 --- a/extensions/codex/src/app-server/plugin-inventory.test.ts +++ b/extensions/codex/src/app-server/plugin-inventory.test.ts @@ -277,6 +277,67 @@ describe("Codex plugin inventory", () => { expect(inventory.diagnostics).toStrictEqual([]); }); + it("accepts the API-key curated marketplace wire name", async () => { + const appCache = new CodexAppInventoryCache(); + await appCache.refreshNow({ + key: "runtime", + nowMs: 0, + request: async (method, params) => + codexAppInventoryResponse(method, [appInfo("google-calendar-app", true)], params), + }); + const listed = { + marketplaces: [ + { + name: "openai-api-curated", + path: "/codex-home/.tmp/plugins/.agents/plugins/api_marketplace.json", + interface: null, + plugins: [ + pluginSummary("google-calendar@openai-api-curated", { + name: "google-calendar", + installed: true, + enabled: true, + }), + ], + }, + ], + marketplaceLoadErrors: [], + } satisfies v2.PluginInstalledResponse; + + const inventory = await readCodexPluginInventory({ + pluginConfig: { + codexPlugins: { + enabled: true, + plugins: { + "google-calendar": { + marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME, + pluginName: "google-calendar", + }, + }, + }, + }, + appCache, + appCacheKey: "runtime", + nowMs: 1, + request: async (method, params) => { + if (method === "plugin/installed") { + return listed; + } + if (method === "plugin/read") { + expect(params).toEqual({ + marketplacePath: "/codex-home/.tmp/plugins/.agents/plugins/api_marketplace.json", + pluginName: "google-calendar", + }); + return pluginDetail("google-calendar", [appSummary("google-calendar-app")]); + } + throw new Error(`unexpected request ${method}`); + }, + }); + + expect(inventory.records[0]?.ownedAppIds).toStrictEqual(["google-calendar-app"]); + expect(inventory.records[0]?.apps[0]?.accessible).toBe(true); + expect(inventory.diagnostics).toStrictEqual([]); + }); + it("fails closed when an installed remote curated plugin omits its opaque id", async () => { const calls: string[] = []; const inventory = await readCodexPluginInventory({ diff --git a/extensions/codex/src/app-server/plugin-inventory.ts b/extensions/codex/src/app-server/plugin-inventory.ts index b7c8c98f85cf..517dad6e7a00 100644 --- a/extensions/codex/src/app-server/plugin-inventory.ts +++ b/extensions/codex/src/app-server/plugin-inventory.ts @@ -23,6 +23,11 @@ import type { import type { CodexAppServerRequestResult, v2 } from "./protocol.js"; const CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME = `${CODEX_PLUGINS_MARKETPLACE_NAME}-remote`; +// Codex serves the curated catalog under this wire name for API-key/Bedrock +// accounts (codex-rs/core-plugins is_openai_curated_marketplace_name). It is +// the same logical catalog, so configured `openai-curated` plugins resolve +// from it and marketplace refs normalize back to CODEX_PLUGINS_MARKETPLACE_NAME. +const CODEX_PLUGINS_API_MARKETPLACE_NAME = "openai-api-curated"; /** Request callback used to call Codex app-server plugin/app methods. */ export type CodexPluginRuntimeRequest = (method: string, params?: unknown) => Promise; @@ -545,15 +550,11 @@ function marketplaceRef( }; } -/** - * True for either supported OpenAI curated marketplace wire name. Codex also - * counts `openai-api-curated` (API-key/Bedrock accounts) as curated, but - * OpenClaw's native plugin flows are ChatGPT-account scoped, so that catalog - * stays out of discovery and activation until it gets end-to-end support. - */ +/** True for any supported OpenAI curated marketplace wire name, matching Codex's own curated predicate. */ export function isOpenAiCuratedMarketplace(marketplace: v2.PluginMarketplaceEntry): boolean { return ( marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME || - marketplace.name === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME + marketplace.name === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME || + marketplace.name === CODEX_PLUGINS_API_MARKETPLACE_NAME ); } diff --git a/extensions/codex/src/migration/provider.test.ts b/extensions/codex/src/migration/provider.test.ts index 6d3104ea4326..7f6fab8f1698 100644 --- a/extensions/codex/src/migration/provider.test.ts +++ b/extensions/codex/src/migration/provider.test.ts @@ -556,6 +556,48 @@ describe("buildCodexMigrationProvider", () => { expect(sourceAppServerClientScope).toHaveBeenCalledTimes(1); }); + it("discovers installed plugins from the API-key curated marketplace", async () => { + const fixture = await createCodexFixture(); + appServerRequest.mockImplementation(async ({ method }: { method: string }) => { + if (method === "plugin/installed") { + return { + marketplaces: [ + { + name: "openai-api-curated", + path: path.join( + fixture.codexHome, + ".tmp/plugins/.agents/plugins/api_marketplace.json", + ), + interface: null, + plugins: [ + pluginSummary("google-calendar@openai-api-curated", { + name: "google-calendar", + installed: true, + enabled: true, + }), + ], + }, + ], + marketplaceLoadErrors: [], + } satisfies v2.PluginInstalledResponse; + } + throw new Error(`unexpected request ${method}`); + }); + + const source = await discoverCodexSource({ input: fixture.codexHome }); + + expect(source.pluginDiscoveryError).toBeUndefined(); + expect(source.plugins).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + pluginName: "google-calendar", + marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME, + migratable: true, + }), + ]), + ); + }); + it("ignores unrelated marketplace errors when no curated plugins are installed", async () => { const fixture = await createCodexFixture(); appServerRequest.mockImplementation(async ({ method }: { method: string }) => { diff --git a/extensions/codex/src/migration/source.ts b/extensions/codex/src/migration/source.ts index aebe6730e0f3..7a1af2b77561 100644 --- a/extensions/codex/src/migration/source.ts +++ b/extensions/codex/src/migration/source.ts @@ -199,7 +199,10 @@ function discoverInstalledCuratedPluginSources( if (!isOpenAiCuratedMarketplace(marketplace)) { continue; } - const remote = marketplace.name !== CODEX_PLUGINS_MARKETPLACE_NAME; + // Remote catalog entries carry no local path; the API-key curated variant + // (`openai-api-curated`) is local like `openai-curated` and must not be + // routed through remote plugin ids it does not have. + const remote = !marketplace.path; for (const summary of marketplace.plugins) { if (!summary.installed) { continue; @@ -540,6 +543,7 @@ function pluginNameFromSummary(summary: v2.PluginSummary): string | undefined { } const marketplaceSuffix = [ `@${CODEX_PLUGINS_MARKETPLACE_NAME}-remote`, + `@openai-api-curated`, `@${CODEX_PLUGINS_MARKETPLACE_NAME}`, ].find((suffix) => trimmed.endsWith(suffix)); const withoutMarketplaceSuffix = marketplaceSuffix