feat(codex): support the openai-api-curated marketplace wire name (#115955)

This commit is contained in:
Peter Steinberger
2026-07-29 11:07:16 -04:00
committed by GitHub
parent 5fa8c4a4e5
commit 34e81ca0dc
6 changed files with 121 additions and 9 deletions
+4 -1
View File
@@ -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
@@ -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",
];
@@ -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({
@@ -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<unknown>;
@@ -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
);
}
@@ -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 }) => {
+5 -1
View File
@@ -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