revert(codex): revert first-party marketplace allowlist

Reverts openclaw/openclaw#82219.
This commit is contained in:
Kevin Lin
2026-06-03 15:35:35 -07:00
committed by GitHub
parent d5c8e90e28
commit fce002ad03
22 changed files with 101 additions and 471 deletions

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -114,7 +114,7 @@
},
"marketplaceName": {
"type": "string",
"enum": ["openai-curated", "openai-bundled", "openai-primary-runtime"]
"enum": ["openai-curated"]
},
"pluginName": {
"type": "string"

View File

@@ -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({

View File

@@ -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,

View File

@@ -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,

View File

@@ -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}.`,
});
}

View File

@@ -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 } : {}),
};

View File

@@ -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, {

View File

@@ -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) ||

View File

@@ -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,

View File

@@ -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,
},

View File

@@ -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");
});
});

View File

@@ -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" })),
}),

View File

@@ -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 () => {

View File

@@ -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")}`,

View File

@@ -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) ||

View File

@@ -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");
});

View File

@@ -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",