mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(codex): accept first-party OpenAI plugin marketplaces
Allow Codex native plugin config to target first-party OpenAI marketplaces, including openai-curated, openai-bundled, and openai-primary-runtime.
Fixes #82216.
Thanks @yaanfpv for the contribution.
Verification:
- node scripts/run-vitest.mjs test/scripts/lint-suppressions.test.ts
- pnpm build:ci-artifacts
- OPENCLAW_VITEST_MAX_WORKERS=2 node scripts/run-vitest.mjs run --config test/vitest/vitest.full-core-support-boundary.config.ts test/scripts/lint-suppressions.test.ts
- node scripts/run-vitest.mjs extensions/codex/src/app-server/config.test.ts extensions/codex/src/app-server/plugin-activation.test.ts extensions/codex/src/app-server/session-binding.test.ts extensions/codex/src/migration/provider.test.ts extensions/sms/src/channel.test.ts extensions/sms/src/inbound.test.ts
- git diff --check
- ./.agents/skills/autoreview/scripts/autoreview --mode local
- GitHub PR CI on head 896640060b, including build-artifacts run 26709647050
This commit is contained in:
@@ -218,6 +218,10 @@ 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.
|
||||
|
||||
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,7 +316,8 @@ 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 only supports `"openai-curated"`.
|
||||
stable marketplace identity. V1 supports `"openai-curated"`,
|
||||
`"openai-bundled"`, and `"openai-primary-runtime"`.
|
||||
- `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 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). |
|
||||
| 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). |
|
||||
|
||||
## 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 migrated source-installed curated plugins. |
|
||||
| `codexPlugins` | disabled | Native Codex plugin/app support for configured first-party Codex 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 migrating source-installed openai-curated Codex plugins
|
||||
- You are configuring first-party Codex plugin marketplaces
|
||||
- You are troubleshooting codexPlugins, app inventory, destructive actions, or plugin app diagnostics
|
||||
---
|
||||
|
||||
@@ -22,7 +22,9 @@ 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 only `openai-curated` plugins that migration observed as
|
||||
- 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
|
||||
source-installed in the source Codex home.
|
||||
- The target Codex app-server must be able to see the expected marketplace,
|
||||
plugin, and app inventory.
|
||||
@@ -52,9 +54,11 @@ Apply the migration when the plan looks right:
|
||||
openclaw migrate apply codex --yes
|
||||
```
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -146,8 +150,10 @@ 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.
|
||||
app-server inventory are migration-eligible for automatic migration.
|
||||
- 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
|
||||
@@ -160,7 +166,9 @@ 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.
|
||||
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.
|
||||
|
||||
## App inventory and ownership
|
||||
|
||||
@@ -248,8 +256,10 @@ 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 `openai-curated` marketplace or plugin. Rerun migration
|
||||
against the target runtime or inspect Codex app-server plugin status.
|
||||
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`.
|
||||
|
||||
**`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"]
|
||||
"enum": ["openai-curated", "openai-bundled", "openai-primary-runtime"]
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
|
||||
@@ -653,6 +653,59 @@ 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,7 +60,30 @@ type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "
|
||||
export type CodexDynamicToolsLoading = "searchable" | "direct";
|
||||
export type CodexPluginDestructivePolicy = boolean;
|
||||
|
||||
export const CODEX_PLUGINS_MARKETPLACE_NAME = "openai-curated";
|
||||
// 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 type CodexComputerUseConfig = {
|
||||
enabled?: boolean;
|
||||
@@ -103,7 +126,7 @@ export type CodexAppServerExperimentalConfig = {
|
||||
|
||||
export type ResolvedCodexPluginPolicy = {
|
||||
configKey: string;
|
||||
marketplaceName: typeof CODEX_PLUGINS_MARKETPLACE_NAME;
|
||||
marketplaceName: CodexPluginsMarketplaceName;
|
||||
pluginName: string;
|
||||
enabled: boolean;
|
||||
allowDestructiveActions: CodexPluginDestructivePolicy;
|
||||
@@ -255,7 +278,7 @@ const codexAppServerExperimentalSchema = z
|
||||
const codexPluginEntryConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
marketplaceName: z.literal(CODEX_PLUGINS_MARKETPLACE_NAME).optional(),
|
||||
marketplaceName: z.enum(CODEX_PLUGINS_MARKETPLACE_NAMES).optional(),
|
||||
pluginName: z.string().trim().min(1).optional(),
|
||||
allow_destructive_actions: z.boolean().optional(),
|
||||
})
|
||||
@@ -365,13 +388,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 (entry.marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME || !entry.pluginName) {
|
||||
if (!isCodexPluginsMarketplaceName(entry.marketplaceName) || !entry.pluginName) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
configKey,
|
||||
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
marketplaceName: entry.marketplaceName,
|
||||
pluginName: entry.pluginName,
|
||||
enabled: enabled && entry.enabled !== false,
|
||||
allowDestructiveActions: entry.allow_destructive_actions ?? allowDestructiveActions,
|
||||
|
||||
@@ -22,6 +22,59 @@ 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({
|
||||
@@ -295,10 +348,13 @@ describe("Codex plugin activation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function identity(pluginName: string): ResolvedCodexPluginPolicy {
|
||||
function identity(
|
||||
pluginName: string,
|
||||
marketplaceName: ResolvedCodexPluginPolicy["marketplaceName"] = CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
): ResolvedCodexPluginPolicy {
|
||||
return {
|
||||
configKey: pluginName,
|
||||
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
marketplaceName,
|
||||
pluginName,
|
||||
enabled: true,
|
||||
allowDestructiveActions: false,
|
||||
@@ -320,6 +376,24 @@ 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,7 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { CodexAppInventoryCache, CodexAppInventoryRequest } from "./app-inventory-cache.js";
|
||||
import { CODEX_PLUGINS_MARKETPLACE_NAME, type ResolvedCodexPluginPolicy } from "./config.js";
|
||||
import {
|
||||
CODEX_PLUGINS_MARKETPLACE_NAMES,
|
||||
isCodexPluginsMarketplaceName,
|
||||
type ResolvedCodexPluginPolicy,
|
||||
} from "./config.js";
|
||||
import {
|
||||
findOpenAiCuratedPluginSummary,
|
||||
pluginReadParams,
|
||||
@@ -48,27 +52,32 @@ export type CodexPluginRuntimeRefreshResult = {
|
||||
export async function ensureCodexPluginActivation(
|
||||
params: EnsureCodexPluginActivationParams,
|
||||
): Promise<CodexPluginActivationResult> {
|
||||
if (params.identity.marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME) {
|
||||
if (!isCodexPluginsMarketplaceName(params.identity.marketplaceName)) {
|
||||
return activationFailure(params.identity, "marketplace_missing", {
|
||||
message: "Only " + CODEX_PLUGINS_MARKETPLACE_NAME + " plugins can be activated.",
|
||||
message:
|
||||
"Only " + CODEX_PLUGINS_MARKETPLACE_NAMES.join(" or ") + " 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);
|
||||
const resolved = findOpenAiCuratedPluginSummary(
|
||||
listed,
|
||||
params.identity.pluginName,
|
||||
params.identity.marketplaceName,
|
||||
);
|
||||
if (!resolved) {
|
||||
const hasCuratedMarketplace = listed.marketplaces.some(
|
||||
(marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
const hasMarketplace = listed.marketplaces.some(
|
||||
(marketplace) => marketplace.name === params.identity.marketplaceName,
|
||||
);
|
||||
if (!hasCuratedMarketplace) {
|
||||
if (!hasMarketplace) {
|
||||
return activationFailure(params.identity, "marketplace_missing", {
|
||||
message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.`,
|
||||
message: `Codex marketplace ${params.identity.marketplaceName} was not found.`,
|
||||
});
|
||||
}
|
||||
return activationFailure(params.identity, "plugin_missing", {
|
||||
message: `${params.identity.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.`,
|
||||
message: `${params.identity.pluginName} was not found in ${params.identity.marketplaceName}.`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ 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";
|
||||
@@ -15,7 +18,7 @@ import type { v2 } from "./protocol.js";
|
||||
export type CodexPluginRuntimeRequest = (method: string, params?: unknown) => Promise<unknown>;
|
||||
|
||||
export type CodexPluginMarketplaceRef = {
|
||||
name: typeof CODEX_PLUGINS_MARKETPLACE_NAME;
|
||||
name: CodexPluginsMarketplaceName;
|
||||
path?: string;
|
||||
remoteMarketplaceName?: string;
|
||||
};
|
||||
@@ -57,7 +60,6 @@ export type CodexPluginInventoryRecord = {
|
||||
|
||||
export type CodexPluginInventory = {
|
||||
policy: ResolvedCodexPluginsPolicy;
|
||||
marketplace?: CodexPluginMarketplaceRef;
|
||||
records: CodexPluginInventoryRecord[];
|
||||
diagnostics: CodexPluginInventoryDiagnostic[];
|
||||
appInventory?: CodexAppInventoryCacheRead;
|
||||
@@ -95,25 +97,14 @@ export async function readCodexPluginInventory(
|
||||
const listed = (await params.request("plugin/list", {
|
||||
cwds: [],
|
||||
} satisfies v2.PluginListParams)) as v2.PluginListResponse;
|
||||
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 } : {}),
|
||||
};
|
||||
// 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 marketplace = marketplaceRef(marketplaceEntry);
|
||||
const diagnostics: CodexPluginInventoryDiagnostic[] = [];
|
||||
const records: CodexPluginInventoryRecord[] = [];
|
||||
if (appInventory?.state === "missing") {
|
||||
@@ -132,12 +123,22 @@ 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 ${CODEX_PLUGINS_MARKETPLACE_NAME}.`,
|
||||
message: `${pluginPolicy.pluginName} was not found in ${pluginPolicy.marketplaceName}.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -187,7 +188,6 @@ export async function readCodexPluginInventory(
|
||||
|
||||
const inventory = {
|
||||
policy,
|
||||
marketplace,
|
||||
records,
|
||||
diagnostics,
|
||||
...(appInventory ? { appInventory } : {}),
|
||||
@@ -198,15 +198,32 @@ export async function readCodexPluginInventory(
|
||||
export function findOpenAiCuratedPluginSummary(
|
||||
listed: v2.PluginListResponse,
|
||||
pluginName: string,
|
||||
marketplaceName?: CodexPluginsMarketplaceName,
|
||||
): { marketplace: CodexPluginMarketplaceRef; summary: v2.PluginSummary } | undefined {
|
||||
const marketplaceEntry = listed.marketplaces.find(
|
||||
(marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
);
|
||||
if (!marketplaceEntry) {
|
||||
return 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 summary = findPluginSummary(marketplaceEntry, pluginName);
|
||||
return summary ? { marketplace: marketplaceRef(marketplaceEntry), summary } : 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;
|
||||
}
|
||||
|
||||
export function pluginReadParams(
|
||||
@@ -349,8 +366,12 @@ 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: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
name,
|
||||
...(marketplace.path ? { path: marketplace.path } : {}),
|
||||
...(!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}),
|
||||
};
|
||||
|
||||
@@ -107,6 +107,36 @@ 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 {
|
||||
CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
isCodexPluginsMarketplaceName,
|
||||
normalizeCodexServiceTier,
|
||||
type CodexAppServerApprovalPolicy,
|
||||
type CodexAppServerSandboxMode,
|
||||
@@ -251,7 +251,8 @@ function readPluginAppPolicyContext(value: unknown): PluginAppPolicyContext | un
|
||||
if (
|
||||
"appId" in entry ||
|
||||
typeof entry.configKey !== "string" ||
|
||||
entry.marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME ||
|
||||
typeof entry.marketplaceName !== "string" ||
|
||||
!isCodexPluginsMarketplaceName(entry.marketplaceName) ||
|
||||
typeof entry.pluginName !== "string" ||
|
||||
typeof entry.allowDestructiveActions !== "boolean" ||
|
||||
!Array.isArray(entry.mcpServerNames) ||
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "../app-server/auth-bridge.js";
|
||||
import {
|
||||
CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
isCodexPluginsMarketplaceName,
|
||||
readCodexPluginConfig,
|
||||
resolveCodexAppServerRuntimeOptions,
|
||||
type ResolvedCodexPluginPolicy,
|
||||
@@ -354,12 +355,13 @@ function hasOpenAiCuratedMarketplace(response: unknown): boolean {
|
||||
const marketplaces = (response as { marketplaces?: unknown }).marketplaces;
|
||||
return (
|
||||
Array.isArray(marketplaces) &&
|
||||
marketplaces.some(
|
||||
(marketplace) =>
|
||||
marketplace &&
|
||||
typeof marketplace === "object" &&
|
||||
(marketplace as { name?: unknown }).name === CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
)
|
||||
marketplaces.some((marketplace) => {
|
||||
if (!marketplace || typeof marketplace !== "object") {
|
||||
return false;
|
||||
}
|
||||
const name = (marketplace as { name?: unknown }).name;
|
||||
return name === CODEX_PLUGINS_MARKETPLACE_NAME;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -494,14 +496,15 @@ function readCodexPluginPolicy(item: MigrationItem): ResolvedCodexPluginPolicy |
|
||||
const pluginName = item.details?.pluginName;
|
||||
if (
|
||||
typeof configKey !== "string" ||
|
||||
marketplaceName !== CODEX_PLUGINS_MARKETPLACE_NAME ||
|
||||
typeof marketplaceName !== "string" ||
|
||||
!isCodexPluginsMarketplaceName(marketplaceName) ||
|
||||
typeof pluginName !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
configKey,
|
||||
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
marketplaceName,
|
||||
pluginName,
|
||||
enabled: true,
|
||||
allowDestructiveActions: true,
|
||||
|
||||
@@ -1462,7 +1462,7 @@ describe("buildCodexMigrationProvider", () => {
|
||||
if (method === "plugin/list" && isTarget) {
|
||||
targetPluginListCalls += 1;
|
||||
if (targetPluginListCalls === 1) {
|
||||
return { marketplaces: [], marketplaceLoadErrors: [], featuredPluginIds: [] };
|
||||
return pluginList([], "openai-bundled");
|
||||
}
|
||||
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
|
||||
}
|
||||
@@ -2225,12 +2225,15 @@ function createConfigRuntime(
|
||||
} as unknown as MigrationProviderContext["runtime"];
|
||||
}
|
||||
|
||||
function pluginList(plugins: v2.PluginSummary[]): v2.PluginListResponse {
|
||||
function pluginList(
|
||||
plugins: v2.PluginSummary[],
|
||||
marketplaceName = CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
): v2.PluginListResponse {
|
||||
return {
|
||||
marketplaces: [
|
||||
{
|
||||
name: CODEX_PLUGINS_MARKETPLACE_NAME,
|
||||
path: "/marketplaces/openai-curated",
|
||||
name: marketplaceName,
|
||||
path: `/marketplaces/${marketplaceName}`,
|
||||
interface: null,
|
||||
plugins,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ResolvedSmsAccount } from "./types.js";
|
||||
|
||||
type ChannelModule = typeof import("./channel.js");
|
||||
|
||||
@@ -48,13 +49,16 @@ describe("smsPlugin status", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
expect(snapshot).toMatchObject({
|
||||
accountId: "support",
|
||||
name: "+15557654321",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
statusState: "configured",
|
||||
running: false,
|
||||
webhookPath: "/webhooks/sms",
|
||||
});
|
||||
expect(snapshot).not.toHaveProperty("connected");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,4 +150,33 @@ 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,46 @@ 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,
|
||||
@@ -254,16 +294,15 @@ export const smsPlugin: ChannelPlugin<ResolvedSmsAccount> = createChatChannelPlu
|
||||
},
|
||||
},
|
||||
status: {
|
||||
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",
|
||||
};
|
||||
defaultRuntime: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
running: false,
|
||||
lastConnectedAt: null,
|
||||
lastError: null,
|
||||
lastInboundAt: null,
|
||||
lastOutboundAt: null,
|
||||
},
|
||||
buildAccountSnapshot: buildSmsAccountSnapshot,
|
||||
buildCapabilitiesDiagnostics: async ({ account }) => ({
|
||||
lines: collectSmsStartupWarnings(account).map((text) => ({ text, tone: "warn" })),
|
||||
}),
|
||||
|
||||
@@ -109,12 +109,15 @@ describe("dispatchSmsInboundEvent", () => {
|
||||
meta: undefined,
|
||||
});
|
||||
expect(sendSmsViaTwilio).toHaveBeenCalledOnce();
|
||||
expect(sendSmsViaTwilio).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "+15551234567",
|
||||
text: expect.stringContaining("PAIR123"),
|
||||
}),
|
||||
);
|
||||
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");
|
||||
});
|
||||
|
||||
it("uses the canonical routed session key for authorized SMS turns", async () => {
|
||||
|
||||
@@ -159,8 +159,17 @@ describe("Twilio SMS helpers", () => {
|
||||
status: "queued",
|
||||
});
|
||||
|
||||
const [url, init] = fetchImpl.mock.calls[0] ?? [];
|
||||
const firstFetchCall = fetchImpl.mock.calls[0];
|
||||
expect(firstFetchCall).toBeDefined();
|
||||
if (!firstFetchCall) {
|
||||
throw new Error("Expected Twilio fetch call");
|
||||
}
|
||||
const [url, init] = firstFetchCall;
|
||||
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")}`,
|
||||
|
||||
@@ -72,11 +72,11 @@ function parseTwilioSuccessPayload(text: string): TwilioMessagePayload {
|
||||
from: typeof record.from === "string" ? record.from : undefined,
|
||||
status: typeof record.status === "string" ? record.status : undefined,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message === "Twilio SMS send returned malformed JSON.") {
|
||||
throw err;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "Twilio SMS send returned malformed JSON.") {
|
||||
throw error;
|
||||
}
|
||||
throw new Error("Twilio SMS send returned malformed JSON.", { cause: err });
|
||||
throw new Error("Twilio SMS send returned malformed JSON.", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -382,6 +382,9 @@ 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 as object, "__proto__")).toBe(true);
|
||||
expect(entry.polluted).toBeUndefined();
|
||||
expect(Object.hasOwn(entry, "__proto__")).toBe(true);
|
||||
expect(Object.prototype).not.toHaveProperty("polluted");
|
||||
});
|
||||
|
||||
|
||||
@@ -365,11 +365,10 @@ 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: "+1555",
|
||||
lastTo: "webchat-room",
|
||||
});
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
|
||||
@@ -186,14 +186,9 @@ describe("production lint suppressions", () => {
|
||||
"extensions/slack/src/monitor/provider-support.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"extensions/telegram/src/telegram-ingress-worker.runtime.ts|unicorn/require-post-message-target-origin|1",
|
||||
"extensions/telegram/src/telegram-ingress-worker.ts|unicorn/require-post-message-target-origin|1",
|
||||
"extensions/whatsapp/src/document-filename.ts|no-control-regex|1",
|
||||
"scripts/e2e/mcp-channels-harness.ts|unicorn/prefer-add-event-listener|1",
|
||||
"scripts/lib/extension-package-boundary.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"scripts/lib/plugin-npm-release.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/agents/agent-scope.ts|no-control-regex|1",
|
||||
"src/agents/code-mode.worker.ts|unicorn/require-post-message-target-origin|1",
|
||||
"src/agents/embedded-agent-runner/run/images.ts|no-control-regex|1",
|
||||
"src/agents/subagent-spawn.ts|no-control-regex|1",
|
||||
"src/channels/plugins/channel-runtime-surface.types.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/channels/plugins/contracts/test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/channels/plugins/types.plugin.ts|typescript/no-explicit-any|1",
|
||||
@@ -201,7 +196,7 @@ describe("production lint suppressions", () => {
|
||||
"src/cli/command-options.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/cli/plugins-cli-test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/cli/test-runtime-capture.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/config/types.channels.ts|@typescript-eslint/no-explicit-any|1",
|
||||
"src/config/types.channels.ts|typescript/no-explicit-any|1",
|
||||
"src/gateway/test-helpers.server.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/hooks/module-loader.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
"src/infra/channel-runtime-context.ts|typescript/no-unnecessary-type-parameters|1",
|
||||
@@ -246,6 +241,10 @@ describe("production lint suppressions", () => {
|
||||
file: "src/channels/plugins/types.plugin.ts",
|
||||
rule: "typescript/no-explicit-any",
|
||||
},
|
||||
{
|
||||
file: "src/config/types.channels.ts",
|
||||
rule: "typescript/no-explicit-any",
|
||||
},
|
||||
{
|
||||
file: "src/test-utils/vitest-mock-fn.ts",
|
||||
rule: "typescript/no-explicit-any",
|
||||
|
||||
Reference in New Issue
Block a user