mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
revert(codex): revert first-party marketplace allowlist
Reverts openclaw/openclaw#82219.
This commit is contained in:
@@ -218,17 +218,6 @@ Target-side auth-required installs are reported on the affected plugin item with
|
||||
Their explicit config entries are written disabled until you reauthorize and
|
||||
enable them. Other install failures are item-scoped `error` results.
|
||||
|
||||
The native Codex plugin config also accepts first-party `openai-bundled` and
|
||||
`openai-primary-runtime` marketplace identities, but migration does not
|
||||
auto-discover or install them from source state.
|
||||
|
||||
OpenAI-side app/plugin availability still comes from the signed-in Codex
|
||||
account and workspace app controls. See
|
||||
[Using Codex with your ChatGPT plan](https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan)
|
||||
for OpenAI's account and workspace-control overview, then use
|
||||
[Native Codex plugins](/plugins/codex-native-plugins#manual-first-party-marketplace-entries)
|
||||
for manual first-party marketplace entries.
|
||||
|
||||
If Codex app-server plugin inventory is unavailable during planning, migration
|
||||
falls back to cached bundle advisory items instead of failing the whole
|
||||
migration.
|
||||
|
||||
@@ -316,10 +316,7 @@ conversation bindings, or any non-Codex harness.
|
||||
migrated plugin entry when global `codexPlugins.enabled` is also true.
|
||||
Default: `true` for explicit entries.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.marketplaceName`:
|
||||
stable marketplace identity. V1 supports `"openai-curated"`,
|
||||
`"openai-bundled"`, and `"openai-primary-runtime"`. See
|
||||
[Native Codex plugins](/plugins/codex-native-plugins#manual-first-party-marketplace-entries)
|
||||
for manual bundled and primary-runtime examples.
|
||||
stable marketplace identity. V1 only supports `"openai-curated"`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.pluginName`: stable
|
||||
Codex plugin identity from migration, for example `"google-calendar"`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.allow_destructive_actions`:
|
||||
|
||||
@@ -38,14 +38,14 @@ All Codex harness settings live under `plugins.entries.codex.config`.
|
||||
|
||||
Supported top-level fields:
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| -------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `discovery` | enabled | Model discovery settings for Codex app-server `model/list`. |
|
||||
| `appServer` | managed stdio app-server | Transport, command, auth, approval, sandbox, and timeout settings. |
|
||||
| `codexDynamicToolsLoading` | `"searchable"` | Use `"direct"` to put OpenClaw dynamic tools directly in the initial Codex tool context. |
|
||||
| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. |
|
||||
| `codexPlugins` | disabled | Native Codex plugin/app support for configured first-party Codex plugins. See [Native Codex plugins](/plugins/codex-native-plugins). |
|
||||
| `computerUse` | disabled | Codex Computer Use setup. See [Codex Computer Use](/plugins/codex-computer-use). |
|
||||
| Field | Default | Meaning |
|
||||
| -------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `discovery` | enabled | Model discovery settings for Codex app-server `model/list`. |
|
||||
| `appServer` | managed stdio app-server | Transport, command, auth, approval, sandbox, and timeout settings. |
|
||||
| `codexDynamicToolsLoading` | `"searchable"` | Use `"direct"` to put OpenClaw dynamic tools directly in the initial Codex tool context. |
|
||||
| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. |
|
||||
| `codexPlugins` | disabled | Native Codex plugin/app support for migrated source-installed curated plugins. See [Native Codex plugins](/plugins/codex-native-plugins). |
|
||||
| `computerUse` | disabled | Codex Computer Use setup. See [Codex Computer Use](/plugins/codex-computer-use). |
|
||||
|
||||
## App-server transport
|
||||
|
||||
|
||||
@@ -526,7 +526,7 @@ Supported top-level Codex plugin fields:
|
||||
| -------------------------- | -------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `codexDynamicToolsLoading` | `"searchable"` | Use `"direct"` to put OpenClaw dynamic tools directly in the initial Codex tool context. |
|
||||
| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. |
|
||||
| `codexPlugins` | disabled | Native Codex plugin/app support for configured first-party Codex plugins. |
|
||||
| `codexPlugins` | disabled | Native Codex plugin/app support for migrated source-installed curated plugins. |
|
||||
|
||||
Supported `appServer` fields:
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ summary: "Configure migrated native Codex plugins for Codex-mode OpenClaw agents
|
||||
title: "Native Codex plugins"
|
||||
read_when:
|
||||
- You want Codex-mode OpenClaw agents to use native Codex plugins
|
||||
- You are configuring first-party Codex plugin marketplaces
|
||||
- You are migrating source-installed openai-curated Codex plugins
|
||||
- You are troubleshooting codexPlugins, app inventory, destructive actions, or plugin app diagnostics
|
||||
---
|
||||
|
||||
@@ -22,9 +22,7 @@ Use this page after the base [Codex harness](/plugins/codex-harness) is working.
|
||||
- The selected OpenClaw agent runtime must be the native Codex harness.
|
||||
- `plugins.entries.codex.enabled` must be true.
|
||||
- `plugins.entries.codex.config.codexPlugins.enabled` must be true.
|
||||
- V1 supports first-party Codex plugin marketplaces: `openai-curated`,
|
||||
`openai-bundled`, and `openai-primary-runtime`.
|
||||
- Migration only auto-discovers `openai-curated` plugins that it observed as
|
||||
- V1 supports only `openai-curated` plugins that migration observed as
|
||||
source-installed in the source Codex home.
|
||||
- The target Codex app-server must be able to see the expected marketplace,
|
||||
plugin, and app inventory.
|
||||
@@ -58,11 +56,9 @@ Apply the migration when the plan looks right:
|
||||
openclaw migrate apply codex --yes
|
||||
```
|
||||
|
||||
Migration writes explicit `codexPlugins` entries for eligible curated plugins
|
||||
and calls Codex app-server `plugin/install` for selected plugins. Explicit
|
||||
config may also reference Codex's bundled and primary-runtime first-party
|
||||
marketplaces when the target app-server inventory exposes those plugin apps. A
|
||||
typical migrated config looks like this:
|
||||
Migration writes explicit `codexPlugins` entries for eligible plugins and calls
|
||||
Codex app-server `plugin/install` for selected plugins. A typical migrated
|
||||
config looks like this:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -93,49 +89,6 @@ After changing `codexPlugins`, new Codex conversations pick up the updated app
|
||||
set automatically. Use `/new` or `/reset` to refresh the current conversation.
|
||||
A gateway restart is not required for plugin enable or disable changes.
|
||||
|
||||
## Manual first-party marketplace entries
|
||||
|
||||
Migration writes `openai-curated` entries for eligible source-installed plugins.
|
||||
For first-party plugins that live in Codex's bundled or primary-runtime
|
||||
marketplaces, add explicit entries after confirming the target Codex app-server
|
||||
inventory exposes that marketplace and plugin.
|
||||
|
||||
Use the same config shape for every first-party marketplace:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
codex: {
|
||||
enabled: true,
|
||||
config: {
|
||||
codexPlugins: {
|
||||
enabled: true,
|
||||
plugins: {
|
||||
chrome: {
|
||||
enabled: true,
|
||||
marketplaceName: "openai-bundled",
|
||||
pluginName: "chrome",
|
||||
},
|
||||
documents: {
|
||||
enabled: true,
|
||||
marketplaceName: "openai-primary-runtime",
|
||||
pluginName: "documents",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The key under `plugins` is OpenClaw's local config key. `pluginName` and
|
||||
`marketplaceName` must match the Codex app-server inventory exactly. If the
|
||||
plugin is not listed in `/codex plugins list` or Codex app diagnostics, OpenClaw
|
||||
keeps the entry configured but cannot expose its apps to Codex turns.
|
||||
|
||||
## Manage plugins from chat
|
||||
|
||||
Use `/codex plugins` when you want to inspect or change configured native Codex
|
||||
@@ -197,10 +150,8 @@ up the updated app set.
|
||||
|
||||
V1 is intentionally narrow:
|
||||
|
||||
- Runtime config accepts `openai-curated`, `openai-bundled`, and
|
||||
`openai-primary-runtime` plugin identities.
|
||||
- Only `openai-curated` plugins that were already installed in the source Codex
|
||||
app-server inventory are migration-eligible for automatic migration.
|
||||
app-server inventory are migration-eligible.
|
||||
- App-backed source plugins must pass the migration-time subscription gate.
|
||||
`--verify-plugin-apps` adds the source app-inventory gate. Subscription-gated
|
||||
accounts plus, in verification mode, inaccessible, disabled, missing source
|
||||
@@ -213,9 +164,7 @@ V1 is intentionally narrow:
|
||||
- There is no `plugins["*"]` wildcard and no config key that grants arbitrary
|
||||
install authority.
|
||||
- Unsupported marketplaces, cached plugin bundles, hooks, and Codex config files
|
||||
are preserved in the migration report for manual review. Bundled and
|
||||
primary-runtime first-party plugins can still be added manually through
|
||||
explicit `codexPlugins` config.
|
||||
are preserved in the migration report for manual review.
|
||||
|
||||
## App inventory and ownership
|
||||
|
||||
@@ -303,10 +252,8 @@ app-server auth or rerun with `--verify-plugin-apps` if you want source app
|
||||
inventory to decide eligibility when account lookup fails.
|
||||
|
||||
**`marketplace_missing` or `plugin_missing`:** the target Codex app-server
|
||||
cannot see the expected first-party marketplace or plugin. Rerun migration
|
||||
against the target runtime, inspect Codex app-server plugin status, or confirm
|
||||
the explicit `marketplaceName` is one of `openai-curated`, `openai-bundled`, or
|
||||
`openai-primary-runtime`.
|
||||
cannot see the expected `openai-curated` marketplace or plugin. Rerun migration
|
||||
against the target runtime or inspect Codex app-server plugin status.
|
||||
|
||||
**`app_inventory_missing` or `app_inventory_stale`:** app readiness came from an
|
||||
empty or stale cache. OpenClaw schedules an async refresh and excludes plugin
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"marketplaceName": {
|
||||
"type": "string",
|
||||
"enum": ["openai-curated", "openai-bundled", "openai-primary-runtime"]
|
||||
"enum": ["openai-curated"]
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
|
||||
@@ -653,59 +653,6 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||
expect(resolveCodexPluginsPolicy(config).pluginPolicies).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("accepts native plugin identities from every first-party OpenAI marketplace", () => {
|
||||
// OpenAI ships first-party Codex plugins across three marketplaces: the local
|
||||
// openai-bundled marketplace shipped with Codex.app (chrome, browser, computer-use,
|
||||
// latex-tectonic), the remote openai-curated marketplace, and the
|
||||
// openai-primary-runtime marketplace owned by the Codex primary runtime
|
||||
// (documents, spreadsheets, presentations). All three should resolve.
|
||||
const config = readCodexPluginConfig({
|
||||
codexPlugins: {
|
||||
enabled: true,
|
||||
plugins: {
|
||||
chrome: {
|
||||
marketplaceName: "openai-bundled",
|
||||
pluginName: "chrome",
|
||||
},
|
||||
"google-calendar": {
|
||||
marketplaceName: "openai-curated",
|
||||
pluginName: "google-calendar",
|
||||
},
|
||||
documents: {
|
||||
marketplaceName: "openai-primary-runtime",
|
||||
pluginName: "documents",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.codexPlugins?.enabled).toBe(true);
|
||||
const policy = resolveCodexPluginsPolicy(config);
|
||||
expect(policy.pluginPolicies).toEqual([
|
||||
{
|
||||
configKey: "chrome",
|
||||
marketplaceName: "openai-bundled",
|
||||
pluginName: "chrome",
|
||||
enabled: true,
|
||||
allowDestructiveActions: true,
|
||||
},
|
||||
{
|
||||
configKey: "documents",
|
||||
marketplaceName: "openai-primary-runtime",
|
||||
pluginName: "documents",
|
||||
enabled: true,
|
||||
allowDestructiveActions: true,
|
||||
},
|
||||
{
|
||||
configKey: "google-calendar",
|
||||
marketplaceName: "openai-curated",
|
||||
pluginName: "google-calendar",
|
||||
enabled: true,
|
||||
allowDestructiveActions: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats configured and environment commands as explicit overrides", () => {
|
||||
expectFields(
|
||||
resolveRuntimeForTest({
|
||||
|
||||
@@ -60,30 +60,7 @@ type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "
|
||||
export type CodexDynamicToolsLoading = "searchable" | "direct";
|
||||
export type CodexPluginDestructivePolicy = boolean;
|
||||
|
||||
// OpenAI ships first-party Codex plugins across three marketplaces:
|
||||
// - openai-curated: remote curated marketplace, fetched via `codex plugin marketplace add`
|
||||
// - openai-bundled: local marketplace that ships with Codex.app and the Codex CLI
|
||||
// (browser, chrome, computer-use, latex-tectonic)
|
||||
// - openai-primary-runtime: marketplace owned by the Codex primary runtime
|
||||
// (documents, spreadsheets, presentations)
|
||||
// All three are owned by OpenAI. Allow activating plugins from any of them.
|
||||
export const CODEX_PLUGINS_MARKETPLACE_NAMES = [
|
||||
"openai-curated",
|
||||
"openai-bundled",
|
||||
"openai-primary-runtime",
|
||||
] as const;
|
||||
export type CodexPluginsMarketplaceName = (typeof CODEX_PLUGINS_MARKETPLACE_NAMES)[number];
|
||||
|
||||
// Back-compat constant for callers that still reference the curated marketplace by name.
|
||||
export const CODEX_PLUGINS_MARKETPLACE_NAME: CodexPluginsMarketplaceName = "openai-curated";
|
||||
|
||||
export function isCodexPluginsMarketplaceName(
|
||||
name: string | undefined,
|
||||
): name is CodexPluginsMarketplaceName {
|
||||
return (
|
||||
name !== undefined && (CODEX_PLUGINS_MARKETPLACE_NAMES as readonly string[]).includes(name)
|
||||
);
|
||||
}
|
||||
export const CODEX_PLUGINS_MARKETPLACE_NAME = "openai-curated";
|
||||
|
||||
export type CodexComputerUseConfig = {
|
||||
enabled?: boolean;
|
||||
@@ -126,7 +103,7 @@ export type CodexAppServerExperimentalConfig = {
|
||||
|
||||
export type ResolvedCodexPluginPolicy = {
|
||||
configKey: string;
|
||||
marketplaceName: CodexPluginsMarketplaceName;
|
||||
marketplaceName: typeof CODEX_PLUGINS_MARKETPLACE_NAME;
|
||||
pluginName: string;
|
||||
enabled: boolean;
|
||||
allowDestructiveActions: CodexPluginDestructivePolicy;
|
||||
@@ -278,7 +255,7 @@ const codexAppServerExperimentalSchema = z
|
||||
const codexPluginEntryConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
marketplaceName: z.enum(CODEX_PLUGINS_MARKETPLACE_NAMES).optional(),
|
||||
marketplaceName: z.literal(CODEX_PLUGINS_MARKETPLACE_NAME).optional(),
|
||||
pluginName: z.string().trim().min(1).optional(),
|
||||
allow_destructive_actions: z.boolean().optional(),
|
||||
})
|
||||
@@ -388,13 +365,13 @@ export function resolveCodexPluginsPolicy(pluginConfig?: unknown): ResolvedCodex
|
||||
const allowDestructiveActions = config?.allow_destructive_actions ?? true;
|
||||
const pluginPolicies = Object.entries(config?.plugins ?? {})
|
||||
.flatMap(([configKey, entry]): ResolvedCodexPluginPolicy[] => {
|
||||
if (!isCodexPluginsMarketplaceName(entry.marketplaceName) || !entry.pluginName) {
|
||||
if (entry.marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME || !entry.pluginName) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
configKey,
|
||||
marketplaceName: entry.marketplaceName,
|
||||
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
pluginName: entry.pluginName,
|
||||
enabled: enabled && entry.enabled !== false,
|
||||
allowDestructiveActions: entry.allow_destructive_actions ?? allowDestructiveActions,
|
||||
|
||||
@@ -22,59 +22,6 @@ describe("Codex plugin activation", () => {
|
||||
expect((params as Record<string, unknown> | undefined)?.[key]).toBe(expected);
|
||||
}
|
||||
|
||||
it("activates plugins from every first-party OpenAI marketplace", async () => {
|
||||
// chrome ships in openai-bundled (with Codex.app), documents ships in
|
||||
// openai-primary-runtime (Codex primary runtime). Both should activate the
|
||||
// same way openai-curated plugins do.
|
||||
for (const { plugin, marketplace } of [
|
||||
{ plugin: "chrome", marketplace: "openai-bundled" as const },
|
||||
{ plugin: "documents", marketplace: "openai-primary-runtime" as const },
|
||||
]) {
|
||||
const calls: string[] = [];
|
||||
const result = await ensureCodexPluginActivation({
|
||||
identity: identity(plugin, marketplace),
|
||||
request: async (method) => {
|
||||
calls.push(method);
|
||||
if (method === "plugin/list") {
|
||||
return pluginListFor(marketplace, [
|
||||
pluginSummary(plugin, { installed: true, enabled: true }),
|
||||
]);
|
||||
}
|
||||
throw new Error(`unexpected request ${method}`);
|
||||
},
|
||||
});
|
||||
|
||||
expectActivationResult(result, {
|
||||
ok: true,
|
||||
reason: "already_active",
|
||||
installAttempted: false,
|
||||
});
|
||||
expect(result.marketplace?.name).toBe(marketplace);
|
||||
expect(calls).toEqual(["plugin/list"]);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects activation requests for marketplaces outside the openai allowlist", async () => {
|
||||
const result = await ensureCodexPluginActivation({
|
||||
identity: {
|
||||
configKey: "rogue",
|
||||
marketplaceName: "third-party" as never,
|
||||
pluginName: "rogue",
|
||||
enabled: true,
|
||||
allowDestructiveActions: false,
|
||||
},
|
||||
request: async () => {
|
||||
throw new Error("plugin/list should not be reached when marketplace is rejected");
|
||||
},
|
||||
});
|
||||
|
||||
expectActivationResult(result, {
|
||||
ok: false,
|
||||
reason: "marketplace_missing",
|
||||
installAttempted: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips plugin/install when the migrated plugin is already active", async () => {
|
||||
const calls: string[] = [];
|
||||
const result = await ensureCodexPluginActivation({
|
||||
@@ -348,13 +295,10 @@ describe("Codex plugin activation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function identity(
|
||||
pluginName: string,
|
||||
marketplaceName: ResolvedCodexPluginPolicy["marketplaceName"] = CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
): ResolvedCodexPluginPolicy {
|
||||
function identity(pluginName: string): ResolvedCodexPluginPolicy {
|
||||
return {
|
||||
configKey: pluginName,
|
||||
marketplaceName,
|
||||
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
pluginName,
|
||||
enabled: true,
|
||||
allowDestructiveActions: false,
|
||||
@@ -376,24 +320,6 @@ function pluginList(plugins: v2.PluginSummary[]): v2.PluginListResponse {
|
||||
};
|
||||
}
|
||||
|
||||
function pluginListFor(
|
||||
marketplaceName: ResolvedCodexPluginPolicy["marketplaceName"],
|
||||
plugins: v2.PluginSummary[],
|
||||
): v2.PluginListResponse {
|
||||
return {
|
||||
marketplaces: [
|
||||
{
|
||||
name: marketplaceName,
|
||||
path: `/marketplaces/${marketplaceName}`,
|
||||
interface: null,
|
||||
plugins,
|
||||
},
|
||||
],
|
||||
marketplaceLoadErrors: [],
|
||||
featuredPluginIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
function pluginSummary(id: string, overrides: Partial<v2.PluginSummary> = {}): v2.PluginSummary {
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { CodexAppInventoryCache, CodexAppInventoryRequest } from "./app-inventory-cache.js";
|
||||
import {
|
||||
CODEX_PLUGINS_MARKETPLACE_NAMES,
|
||||
isCodexPluginsMarketplaceName,
|
||||
type ResolvedCodexPluginPolicy,
|
||||
} from "./config.js";
|
||||
import { CODEX_PLUGINS_MARKETPLACE_NAME, type ResolvedCodexPluginPolicy } from "./config.js";
|
||||
import {
|
||||
findOpenAiCuratedPluginSummary,
|
||||
pluginReadParams,
|
||||
@@ -52,32 +48,27 @@ export type CodexPluginRuntimeRefreshResult = {
|
||||
export async function ensureCodexPluginActivation(
|
||||
params: EnsureCodexPluginActivationParams,
|
||||
): Promise<CodexPluginActivationResult> {
|
||||
if (!isCodexPluginsMarketplaceName(params.identity.marketplaceName)) {
|
||||
if (params.identity.marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME) {
|
||||
return activationFailure(params.identity, "marketplace_missing", {
|
||||
message:
|
||||
"Only " + CODEX_PLUGINS_MARKETPLACE_NAMES.join(" or ") + " plugins can be activated.",
|
||||
message: "Only openai-curated plugins can be activated.",
|
||||
});
|
||||
}
|
||||
|
||||
const listed = (await params.request("plugin/list", {
|
||||
cwds: [],
|
||||
} satisfies v2.PluginListParams)) as v2.PluginListResponse;
|
||||
const resolved = findOpenAiCuratedPluginSummary(
|
||||
listed,
|
||||
params.identity.pluginName,
|
||||
params.identity.marketplaceName,
|
||||
);
|
||||
const resolved = findOpenAiCuratedPluginSummary(listed, params.identity.pluginName);
|
||||
if (!resolved) {
|
||||
const hasMarketplace = listed.marketplaces.some(
|
||||
(marketplace) => marketplace.name === params.identity.marketplaceName,
|
||||
const hasCuratedMarketplace = listed.marketplaces.some(
|
||||
(marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
);
|
||||
if (!hasMarketplace) {
|
||||
if (!hasCuratedMarketplace) {
|
||||
return activationFailure(params.identity, "marketplace_missing", {
|
||||
message: `Codex marketplace ${params.identity.marketplaceName} was not found.`,
|
||||
message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.`,
|
||||
});
|
||||
}
|
||||
return activationFailure(params.identity, "plugin_missing", {
|
||||
message: `${params.identity.pluginName} was not found in ${params.identity.marketplaceName}.`,
|
||||
message: `${params.identity.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ import type {
|
||||
} from "./app-inventory-cache.js";
|
||||
import {
|
||||
CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
CODEX_PLUGINS_MARKETPLACE_NAMES,
|
||||
isCodexPluginsMarketplaceName,
|
||||
resolveCodexPluginsPolicy,
|
||||
type CodexPluginsMarketplaceName,
|
||||
type ResolvedCodexPluginPolicy,
|
||||
type ResolvedCodexPluginsPolicy,
|
||||
} from "./config.js";
|
||||
@@ -18,7 +15,7 @@ import type { v2 } from "./protocol.js";
|
||||
export type CodexPluginRuntimeRequest = (method: string, params?: unknown) => Promise<unknown>;
|
||||
|
||||
export type CodexPluginMarketplaceRef = {
|
||||
name: CodexPluginsMarketplaceName;
|
||||
name: typeof CODEX_PLUGINS_MARKETPLACE_NAME;
|
||||
path?: string;
|
||||
remoteMarketplaceName?: string;
|
||||
};
|
||||
@@ -60,6 +57,7 @@ export type CodexPluginInventoryRecord = {
|
||||
|
||||
export type CodexPluginInventory = {
|
||||
policy: ResolvedCodexPluginsPolicy;
|
||||
marketplace?: CodexPluginMarketplaceRef;
|
||||
records: CodexPluginInventoryRecord[];
|
||||
diagnostics: CodexPluginInventoryDiagnostic[];
|
||||
appInventory?: CodexAppInventoryCacheRead;
|
||||
@@ -97,14 +95,25 @@ export async function readCodexPluginInventory(
|
||||
const listed = (await params.request("plugin/list", {
|
||||
cwds: [],
|
||||
} satisfies v2.PluginListParams)) as v2.PluginListResponse;
|
||||
// Index the supported marketplaces (curated + bundled) by name so each plugin
|
||||
// policy is matched to the marketplace its config actually points at.
|
||||
const marketplaceByName = new Map<CodexPluginsMarketplaceName, v2.PluginMarketplaceEntry>();
|
||||
for (const marketplace of listed.marketplaces) {
|
||||
if (isCodexPluginsMarketplaceName(marketplace.name)) {
|
||||
marketplaceByName.set(marketplace.name, marketplace);
|
||||
}
|
||||
const marketplaceEntry = listed.marketplaces.find(
|
||||
(marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
);
|
||||
if (!marketplaceEntry) {
|
||||
return {
|
||||
policy,
|
||||
records: [],
|
||||
diagnostics: policy.pluginPolicies
|
||||
.filter((pluginPolicy) => pluginPolicy.enabled)
|
||||
.map((pluginPolicy) => ({
|
||||
code: "marketplace_missing",
|
||||
plugin: pluginPolicy,
|
||||
message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.`,
|
||||
})),
|
||||
...(appInventory ? { appInventory } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const marketplace = marketplaceRef(marketplaceEntry);
|
||||
const diagnostics: CodexPluginInventoryDiagnostic[] = [];
|
||||
const records: CodexPluginInventoryRecord[] = [];
|
||||
if (appInventory?.state === "missing") {
|
||||
@@ -123,22 +132,12 @@ export async function readCodexPluginInventory(
|
||||
if (!pluginPolicy.enabled) {
|
||||
continue;
|
||||
}
|
||||
const marketplaceEntry = marketplaceByName.get(pluginPolicy.marketplaceName);
|
||||
if (!marketplaceEntry) {
|
||||
diagnostics.push({
|
||||
code: "marketplace_missing",
|
||||
plugin: pluginPolicy,
|
||||
message: `Codex marketplace ${pluginPolicy.marketplaceName} was not found.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const marketplace = marketplaceRef(marketplaceEntry);
|
||||
const summary = findPluginSummary(marketplaceEntry, pluginPolicy.pluginName);
|
||||
if (!summary) {
|
||||
diagnostics.push({
|
||||
code: "plugin_missing",
|
||||
plugin: pluginPolicy,
|
||||
message: `${pluginPolicy.pluginName} was not found in ${pluginPolicy.marketplaceName}.`,
|
||||
message: `${pluginPolicy.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -188,6 +187,7 @@ export async function readCodexPluginInventory(
|
||||
|
||||
const inventory = {
|
||||
policy,
|
||||
marketplace,
|
||||
records,
|
||||
diagnostics,
|
||||
...(appInventory ? { appInventory } : {}),
|
||||
@@ -198,32 +198,15 @@ export async function readCodexPluginInventory(
|
||||
export function findOpenAiCuratedPluginSummary(
|
||||
listed: v2.PluginListResponse,
|
||||
pluginName: string,
|
||||
marketplaceName?: CodexPluginsMarketplaceName,
|
||||
): { marketplace: CodexPluginMarketplaceRef; summary: v2.PluginSummary } | undefined {
|
||||
if (marketplaceName) {
|
||||
const marketplaceEntry = listed.marketplaces.find(
|
||||
(marketplace) => marketplace.name === marketplaceName,
|
||||
);
|
||||
if (!marketplaceEntry) {
|
||||
return undefined;
|
||||
}
|
||||
const summary = findPluginSummary(marketplaceEntry, pluginName);
|
||||
return summary ? { marketplace: marketplaceRef(marketplaceEntry), summary } : undefined;
|
||||
const marketplaceEntry = listed.marketplaces.find(
|
||||
(marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
);
|
||||
if (!marketplaceEntry) {
|
||||
return undefined;
|
||||
}
|
||||
// No marketplace hint: search every supported marketplace and return the first hit.
|
||||
for (const allowedName of CODEX_PLUGINS_MARKETPLACE_NAMES) {
|
||||
const marketplaceEntry = listed.marketplaces.find(
|
||||
(marketplace) => marketplace.name === allowedName,
|
||||
);
|
||||
if (!marketplaceEntry) {
|
||||
continue;
|
||||
}
|
||||
const summary = findPluginSummary(marketplaceEntry, pluginName);
|
||||
if (summary) {
|
||||
return { marketplace: marketplaceRef(marketplaceEntry), summary };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
const summary = findPluginSummary(marketplaceEntry, pluginName);
|
||||
return summary ? { marketplace: marketplaceRef(marketplaceEntry), summary } : undefined;
|
||||
}
|
||||
|
||||
export function pluginReadParams(
|
||||
@@ -366,12 +349,8 @@ function pluginNameFromPluginId(pluginId: string, marketplaceName: string): stri
|
||||
}
|
||||
|
||||
function marketplaceRef(marketplace: v2.PluginMarketplaceEntry): CodexPluginMarketplaceRef {
|
||||
// marketplace.name is validated at every call site via isCodexPluginsMarketplaceName.
|
||||
const name = isCodexPluginsMarketplaceName(marketplace.name)
|
||||
? marketplace.name
|
||||
: CODEX_PLUGINS_MARKETPLACE_NAME;
|
||||
return {
|
||||
name,
|
||||
name: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
...(marketplace.path ? { path: marketplace.path } : {}),
|
||||
...(!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}),
|
||||
};
|
||||
|
||||
@@ -107,36 +107,6 @@ describe("codex app-server session binding", () => {
|
||||
expect(binding?.pluginAppPolicyContext).toEqual(pluginAppPolicyContext);
|
||||
});
|
||||
|
||||
it("round-trips plugin app policy context for openai-bundled marketplace plugins", async () => {
|
||||
// The chrome plugin lives in openai-bundled (ships with Codex.app), so
|
||||
// its policy must persist across reads/writes the same way curated entries do.
|
||||
const sessionFile = path.join(tempDir, "session-bundled.json");
|
||||
const pluginAppPolicyContext = {
|
||||
fingerprint: "plugin-policy-bundled-1",
|
||||
apps: {
|
||||
"chrome-app": {
|
||||
configKey: "chrome",
|
||||
marketplaceName: "openai-bundled" as const,
|
||||
pluginName: "chrome",
|
||||
allowDestructiveActions: true,
|
||||
mcpServerNames: ["chrome"],
|
||||
},
|
||||
},
|
||||
pluginAppIds: {
|
||||
chrome: ["chrome-app"],
|
||||
},
|
||||
};
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
threadId: "thread-bundled",
|
||||
cwd: tempDir,
|
||||
pluginAppPolicyContext,
|
||||
});
|
||||
|
||||
const binding = await readCodexAppServerBinding(sessionFile);
|
||||
|
||||
expect(binding?.pluginAppPolicyContext).toEqual(pluginAppPolicyContext);
|
||||
});
|
||||
|
||||
it("round-trips context-engine binding metadata", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.json");
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type AuthProfileStore,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import {
|
||||
isCodexPluginsMarketplaceName,
|
||||
CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
normalizeCodexServiceTier,
|
||||
type CodexAppServerApprovalPolicy,
|
||||
type CodexAppServerSandboxMode,
|
||||
@@ -257,8 +257,7 @@ function readPluginAppPolicyContext(value: unknown): PluginAppPolicyContext | un
|
||||
if (
|
||||
"appId" in entry ||
|
||||
typeof entry.configKey !== "string" ||
|
||||
typeof entry.marketplaceName !== "string" ||
|
||||
!isCodexPluginsMarketplaceName(entry.marketplaceName) ||
|
||||
entry.marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME ||
|
||||
typeof entry.pluginName !== "string" ||
|
||||
typeof entry.allowDestructiveActions !== "boolean" ||
|
||||
!Array.isArray(entry.mcpServerNames) ||
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
} from "../app-server/auth-bridge.js";
|
||||
import {
|
||||
CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
isCodexPluginsMarketplaceName,
|
||||
readCodexPluginConfig,
|
||||
resolveCodexAppServerRuntimeOptions,
|
||||
type ResolvedCodexPluginPolicy,
|
||||
@@ -355,13 +354,12 @@ function hasOpenAiCuratedMarketplace(response: unknown): boolean {
|
||||
const marketplaces = (response as { marketplaces?: unknown }).marketplaces;
|
||||
return (
|
||||
Array.isArray(marketplaces) &&
|
||||
marketplaces.some((marketplace) => {
|
||||
if (!marketplace || typeof marketplace !== "object") {
|
||||
return false;
|
||||
}
|
||||
const name = (marketplace as { name?: unknown }).name;
|
||||
return name === CODEX_PLUGINS_MARKETPLACE_NAME;
|
||||
})
|
||||
marketplaces.some(
|
||||
(marketplace) =>
|
||||
marketplace &&
|
||||
typeof marketplace === "object" &&
|
||||
(marketplace as { name?: unknown }).name === CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -498,15 +496,14 @@ function readCodexPluginPolicy(item: MigrationItem): ResolvedCodexPluginPolicy |
|
||||
const pluginName = item.details?.pluginName;
|
||||
if (
|
||||
typeof configKey !== "string" ||
|
||||
typeof marketplaceName !== "string" ||
|
||||
!isCodexPluginsMarketplaceName(marketplaceName) ||
|
||||
marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME ||
|
||||
typeof pluginName !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
configKey,
|
||||
marketplaceName,
|
||||
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
pluginName,
|
||||
enabled: true,
|
||||
allowDestructiveActions: true,
|
||||
|
||||
@@ -1462,7 +1462,7 @@ describe("buildCodexMigrationProvider", () => {
|
||||
if (method === "plugin/list" && isTarget) {
|
||||
targetPluginListCalls += 1;
|
||||
if (targetPluginListCalls === 1) {
|
||||
return pluginList([], "openai-bundled");
|
||||
return { marketplaces: [], marketplaceLoadErrors: [], featuredPluginIds: [] };
|
||||
}
|
||||
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
|
||||
}
|
||||
@@ -2225,15 +2225,12 @@ function createConfigRuntime(
|
||||
} as unknown as MigrationProviderContext["runtime"];
|
||||
}
|
||||
|
||||
function pluginList(
|
||||
plugins: v2.PluginSummary[],
|
||||
marketplaceName = CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
): v2.PluginListResponse {
|
||||
function pluginList(plugins: v2.PluginSummary[]): v2.PluginListResponse {
|
||||
return {
|
||||
marketplaces: [
|
||||
{
|
||||
name: marketplaceName,
|
||||
path: `/marketplaces/${marketplaceName}`,
|
||||
name: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
path: "/marketplaces/openai-curated",
|
||||
interface: null,
|
||||
plugins,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ResolvedSmsAccount } from "./types.js";
|
||||
|
||||
type ChannelModule = typeof import("./channel.js");
|
||||
|
||||
@@ -49,16 +48,13 @@ describe("smsPlugin status", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot).toMatchObject({
|
||||
expect(snapshot).toEqual({
|
||||
accountId: "support",
|
||||
name: "+15557654321",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
statusState: "configured",
|
||||
running: false,
|
||||
webhookPath: "/webhooks/sms",
|
||||
});
|
||||
expect(snapshot).not.toHaveProperty("connected");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,33 +152,4 @@ describe("smsPlugin outbound", () => {
|
||||
}),
|
||||
).toEqual({ ok: true, to: "+15551234567" });
|
||||
});
|
||||
|
||||
it("preserves inspected account status fields", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
sms: {
|
||||
accountSid: "AC123",
|
||||
authToken: "secret",
|
||||
fromNumber: "+15557654321",
|
||||
webhookPath: "/twilio/sms",
|
||||
},
|
||||
},
|
||||
};
|
||||
const account = smsPlugin.config.inspectAccount?.(cfg);
|
||||
expect(account).toBeDefined();
|
||||
|
||||
const snapshot = await smsPlugin.status?.buildAccountSnapshot?.({
|
||||
account: account as ResolvedSmsAccount,
|
||||
cfg,
|
||||
});
|
||||
|
||||
expect(snapshot).toMatchObject({
|
||||
configured: true,
|
||||
enabled: true,
|
||||
statusState: "configured",
|
||||
tokenStatus: "available",
|
||||
webhookPath: "/twilio/sms",
|
||||
});
|
||||
expect(snapshot).not.toHaveProperty("connected");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,46 +35,6 @@ import type { ResolvedSmsAccount } from "./types.js";
|
||||
|
||||
const CHANNEL_ID = "sms";
|
||||
|
||||
type SmsStatusSnapshotAccount = Partial<ResolvedSmsAccount> & {
|
||||
configured?: boolean;
|
||||
tokenStatus?: string;
|
||||
webhookPath?: string;
|
||||
};
|
||||
|
||||
function buildSmsAccountSnapshot(params: {
|
||||
account: ResolvedSmsAccount;
|
||||
runtime?: {
|
||||
running?: boolean;
|
||||
connected?: boolean;
|
||||
lastConnectedAt?: number | null;
|
||||
lastError?: string | null;
|
||||
lastInboundAt?: number | null;
|
||||
lastOutboundAt?: number | null;
|
||||
};
|
||||
}) {
|
||||
const account = params.account as SmsStatusSnapshotAccount;
|
||||
const configured =
|
||||
typeof account.configured === "boolean"
|
||||
? account.configured
|
||||
: isSmsAccountConfigured(params.account);
|
||||
return {
|
||||
accountId: account.accountId ?? "",
|
||||
name: account.fromNumber || account.messagingServiceSid || "SMS",
|
||||
enabled: account.enabled,
|
||||
configured,
|
||||
statusState:
|
||||
account.enabled === false ? "disabled" : configured ? "configured" : "unconfigured",
|
||||
...(account.tokenStatus ? { tokenStatus: account.tokenStatus } : {}),
|
||||
...(account.webhookPath ? { webhookPath: account.webhookPath } : {}),
|
||||
running: params.runtime?.running ?? false,
|
||||
...(params.runtime?.connected !== undefined ? { connected: params.runtime.connected } : {}),
|
||||
lastConnectedAt: params.runtime?.lastConnectedAt ?? null,
|
||||
lastError: params.runtime?.lastError ?? null,
|
||||
lastInboundAt: params.runtime?.lastInboundAt ?? null,
|
||||
lastOutboundAt: params.runtime?.lastOutboundAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const smsConfigAdapter = createHybridChannelConfigAdapter<ResolvedSmsAccount>({
|
||||
sectionKey: CHANNEL_ID,
|
||||
listAccountIds: listSmsAccountIds,
|
||||
@@ -296,17 +256,18 @@ export const smsPlugin: ChannelPlugin<ResolvedSmsAccount, SmsProbe> = createChat
|
||||
},
|
||||
},
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
running: false,
|
||||
lastConnectedAt: null,
|
||||
lastError: null,
|
||||
lastInboundAt: null,
|
||||
lastOutboundAt: null,
|
||||
buildAccountSnapshot: ({ account }) => {
|
||||
const configured = isSmsAccountConfigured(account);
|
||||
return {
|
||||
accountId: account.accountId,
|
||||
name: account.fromNumber || account.messagingServiceSid || "SMS",
|
||||
enabled: account.enabled,
|
||||
configured,
|
||||
statusState: !account.enabled ? "disabled" : configured ? "configured" : "unconfigured",
|
||||
};
|
||||
},
|
||||
probeAccount: async ({ account, timeoutMs }) => await probeSmsAccount({ account, timeoutMs }),
|
||||
formatCapabilitiesProbe: ({ probe }) => formatSmsProbeLines(probe),
|
||||
buildAccountSnapshot: buildSmsAccountSnapshot,
|
||||
buildCapabilitiesDiagnostics: async ({ account }) => ({
|
||||
lines: collectSmsStartupWarnings(account).map((text) => ({ text, tone: "warn" })),
|
||||
}),
|
||||
|
||||
@@ -109,15 +109,12 @@ describe("dispatchSmsInboundEvent", () => {
|
||||
meta: undefined,
|
||||
});
|
||||
expect(sendSmsViaTwilio).toHaveBeenCalledOnce();
|
||||
const firstSendCall = sendSmsViaTwilio.mock.calls[0];
|
||||
expect(firstSendCall).toBeDefined();
|
||||
if (!firstSendCall) {
|
||||
throw new Error("Expected SMS send call");
|
||||
}
|
||||
expect(firstSendCall[0]).toMatchObject({
|
||||
to: "+15551234567",
|
||||
});
|
||||
expect(firstSendCall[0].text).toContain("PAIR123");
|
||||
expect(sendSmsViaTwilio).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "+15551234567",
|
||||
text: expect.stringContaining("PAIR123"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the canonical routed session key for authorized SMS turns", async () => {
|
||||
|
||||
@@ -176,17 +176,8 @@ describe("Twilio SMS helpers", () => {
|
||||
status: "queued",
|
||||
});
|
||||
|
||||
const firstFetchCall = fetchImpl.mock.calls[0];
|
||||
expect(firstFetchCall).toBeDefined();
|
||||
if (!firstFetchCall) {
|
||||
throw new Error("Expected Twilio fetch call");
|
||||
}
|
||||
const [url, init] = firstFetchCall;
|
||||
const [url, init] = fetchImpl.mock.calls[0] ?? [];
|
||||
expect(url).toBe("https://api.twilio.com/2010-04-01/Accounts/AC123/Messages.json");
|
||||
expect(init).toBeDefined();
|
||||
if (!init) {
|
||||
throw new Error("Expected Twilio request init");
|
||||
}
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(init?.headers).toMatchObject({
|
||||
authorization: `Basic ${Buffer.from("AC123:secret").toString("base64")}`,
|
||||
|
||||
@@ -386,9 +386,6 @@ function listProposalEntries(params: {
|
||||
proposal.skillName,
|
||||
proposal.skillKey,
|
||||
].some((value) => {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const lower = value.toLowerCase();
|
||||
return (
|
||||
lower.includes(query) ||
|
||||
|
||||
@@ -314,8 +314,8 @@ describe("Session Store Cache", () => {
|
||||
if (!entry) {
|
||||
throw new Error("Expected cached entry");
|
||||
}
|
||||
expect(entry.polluted).toBeUndefined();
|
||||
expect(Object.hasOwn(entry, "__proto__")).toBe(true);
|
||||
expect(entry?.polluted).toBeUndefined();
|
||||
expect(Object.hasOwn(entry as object, "__proto__")).toBe(true);
|
||||
expect(Object.prototype).not.toHaveProperty("polluted");
|
||||
});
|
||||
|
||||
|
||||
@@ -360,10 +360,11 @@ describe("gateway server agent", () => {
|
||||
});
|
||||
|
||||
test("agent errors when deliver=true and last channel is webchat", async () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
await writeMainSessionEntry({
|
||||
sessionId: "sess-main-webchat",
|
||||
lastChannel: "webchat",
|
||||
lastTo: "webchat-room",
|
||||
lastTo: "+1555",
|
||||
});
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
|
||||
Reference in New Issue
Block a user