diff --git a/docs/plugins/codex-harness-reference.md b/docs/plugins/codex-harness-reference.md index a4cc3c627997..86ec057b9906 100644 --- a/docs/plugins/codex-harness-reference.md +++ b/docs/plugins/codex-harness-reference.md @@ -91,8 +91,8 @@ Supported `appServer` fields: | `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. | | `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. | | `url` | unset | WebSocket app-server URL. | -| `authToken` | unset | Bearer token for WebSocket transport. | -| `headers` | `{}` | Extra WebSocket headers. | +| `authToken` | unset | Bearer token for WebSocket transport. Accepts a literal string or SecretInput such as `${CODEX_APP_SERVER_TOKEN}`. | +| `headers` | `{}` | Extra WebSocket headers. Header values accept literal strings or SecretInput values, for example `x-codex-client-session-token: "${CODEX_CLIENT_SESSION_TOKEN}"`. | | `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. | | `remoteWorkspaceRoot` | unset | Remote Codex app-server workspace root. When set, OpenClaw infers the local workspace root from the resolved OpenClaw workspace, preserves the current cwd suffix under this remote root, and sends only the final app-server cwd to Codex. If the cwd is outside the resolved OpenClaw workspace root, OpenClaw fails closed instead of sending a gateway-local path to the remote app-server. | | `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | @@ -149,11 +149,15 @@ must report stable version `0.125.0` or newer. OpenClaw treats non-loopback WebSocket app-server URLs as remote and requires identity-bearing WebSocket auth through `appServer.authToken` or an -`Authorization` header. When native Codex plugins are configured, OpenClaw uses -the connected app-server's plugin control plane to install or refresh those -plugins and then refreshes app inventory so plugin-owned apps are visible to the -Codex thread. Only connect OpenClaw to remote app-servers that are trusted to -accept OpenClaw-managed plugin installs and app inventory refreshes. +`Authorization` header. `appServer.authToken` and each `appServer.headers.*` +value can be a SecretInput; the secrets runtime resolves SecretRefs and env +shorthand before OpenClaw builds app-server start options, and unresolved +structured SecretRefs fail before any token or header is sent. When native Codex +plugins are configured, OpenClaw uses the connected app-server's plugin control +plane to install or refresh those plugins and then refreshes app inventory so +plugin-owned apps are visible to the Codex thread. Only connect OpenClaw to +remote app-servers that are trusted to accept OpenClaw-managed plugin installs +and app inventory refreshes. ## Approval and sandbox modes diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index e44d396a12c4..edc34dcde231 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -552,8 +552,8 @@ Supported `appServer` fields: | `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. | | `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. | | `url` | unset | WebSocket app-server URL. | -| `authToken` | unset | Bearer token for WebSocket transport. | -| `headers` | `{}` | Extra WebSocket headers. | +| `authToken` | unset | Bearer token for WebSocket transport. Accepts a literal string or SecretInput such as `${CODEX_APP_SERVER_TOKEN}`. | +| `headers` | `{}` | Extra WebSocket headers. Header values accept literal strings or SecretInput values, for example `x-codex-client-session-token: "${CODEX_CLIENT_SESSION_TOKEN}"`. | | `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. | | `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. | | `remoteWorkspaceRoot` | unset | Remote Codex app-server workspace root. When set, OpenClaw infers the local workspace root from the resolved OpenClaw workspace, preserves the current cwd suffix under this remote root, and sends only the final app-server cwd to Codex. If the cwd is outside the resolved OpenClaw workspace root, OpenClaw fails closed instead of sending a gateway-local path to the remote app-server. | diff --git a/docs/reference/secretref-credential-surface.md b/docs/reference/secretref-credential-surface.md index d91d6b18ed70..6ad13880b751 100644 --- a/docs/reference/secretref-credential-surface.md +++ b/docs/reference/secretref-credential-surface.md @@ -43,6 +43,8 @@ Scope intent: - `tools.web.fetch.firecrawl.apiKey` - `plugins.entries.acpx.config.mcpServers.*.env.*` - `plugins.entries.brave.config.webSearch.apiKey` +- `plugins.entries.codex.config.appServer.authToken` +- `plugins.entries.codex.config.appServer.headers.*` - `plugins.entries.exa.config.webSearch.apiKey` - `plugins.entries.google-meet.config.realtime.providers.*.apiKey` - `plugins.entries.google.config.webSearch.apiKey` diff --git a/docs/reference/secretref-user-supplied-credentials-matrix.json b/docs/reference/secretref-user-supplied-credentials-matrix.json index 26945a5e0530..b75bde53e7f1 100644 --- a/docs/reference/secretref-user-supplied-credentials-matrix.json +++ b/docs/reference/secretref-user-supplied-credentials-matrix.json @@ -554,6 +554,20 @@ "secretShape": "secret_input", "optIn": true }, + { + "id": "plugins.entries.codex.config.appServer.authToken", + "configFile": "openclaw.json", + "path": "plugins.entries.codex.config.appServer.authToken", + "secretShape": "secret_input", + "optIn": true + }, + { + "id": "plugins.entries.codex.config.appServer.headers.*", + "configFile": "openclaw.json", + "path": "plugins.entries.codex.config.appServer.headers.*", + "secretShape": "secret_input", + "optIn": true + }, { "id": "plugins.entries.exa.config.webSearch.apiKey", "configFile": "openclaw.json", diff --git a/extensions/codex/openclaw.plugin.json b/extensions/codex/openclaw.plugin.json index 175731e34638..d0d2e2c4894b 100644 --- a/extensions/codex/openclaw.plugin.json +++ b/extensions/codex/openclaw.plugin.json @@ -152,10 +152,10 @@ ] }, "url": { "type": "string" }, - "authToken": { "type": "string" }, + "authToken": { "type": ["string", "object"] }, "headers": { "type": "object", - "additionalProperties": { "type": "string" } + "additionalProperties": { "type": ["string", "object"] } }, "clearEnv": { "type": "array", @@ -254,6 +254,14 @@ } } }, + "configContracts": { + "secretInputs": { + "paths": [ + { "path": "appServer.authToken", "expected": "string" }, + { "path": "appServer.headers.*", "expected": "string" } + ] + } + }, "uiHints": { "codexDynamicToolsLoading": { "label": "Dynamic Tools Loading", @@ -382,6 +390,7 @@ "appServer.headers": { "label": "Headers", "help": "Additional headers sent to the WebSocket app-server.", + "sensitive": true, "advanced": true }, "appServer.clearEnv": { diff --git a/extensions/codex/src/app-server/config.test.ts b/extensions/codex/src/app-server/config.test.ts index a08a140bccbc..1004308010ed 100644 --- a/extensions/codex/src/app-server/config.test.ts +++ b/extensions/codex/src/app-server/config.test.ts @@ -28,6 +28,10 @@ function resolveRuntimeForTest(params: RuntimeOptionsParams = {}) { return resolveCodexAppServerRuntimeOptions({ env: {}, requirementsToml: null, ...params }); } +function envRef(id: string) { + return { source: "env" as const, provider: "default", id }; +} + function requireRecord(value: unknown, label: string): Record { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`Expected ${label}`); @@ -413,6 +417,65 @@ describe("Codex app-server config", () => { }); }); + it("passes resolved app-server SecretInput strings through to auth token and headers", () => { + const runtime = resolveRuntimeForTest({ + pluginConfig: { + appServer: { + transport: "websocket", + url: "wss://codex-app-server.example.internal/ws", + authToken: " resolved-capability-token ", + headers: { + " x-codex-client-session-token ": " resolved-session-token ", + Authorization: " Bearer explicit-token ", + }, + }, + }, + }); + + expectFields(runtime.start, "runtime start", { + authToken: "resolved-capability-token", + headers: { + "x-codex-client-session-token": "resolved-session-token", + Authorization: "Bearer explicit-token", + }, + }); + }); + + it("rejects unresolved app-server auth token SecretRefs at runtime option resolution", () => { + expect(() => + resolveRuntimeForTest({ + pluginConfig: { + appServer: { + transport: "websocket", + url: "wss://codex-app-server.example.internal/ws", + authToken: envRef("CODEX_APP_SERVER_TOKEN"), + }, + }, + }), + ).toThrow( + 'plugins.entries.codex.config.appServer.authToken: unresolved SecretRef "env:default:CODEX_APP_SERVER_TOKEN"', + ); + }); + + it("rejects unresolved app-server header SecretRefs at runtime option resolution", () => { + expect(() => + resolveRuntimeForTest({ + pluginConfig: { + appServer: { + transport: "websocket", + url: "wss://codex-app-server.example.internal/ws", + authToken: "capability-token", + headers: { + "x-codex-client-session-token": envRef("CODEX_CLIENT_SESSION_TOKEN"), + }, + }, + }, + }), + ).toThrow( + 'plugins.entries.codex.config.appServer.headers.x-codex-client-session-token: unresolved SecretRef "env:default:CODEX_CLIENT_SESSION_TOKEN"', + ); + }); + it("treats IPv6 loopback websocket app-servers as local loopback", () => { const runtime = resolveRuntimeForTest({ pluginConfig: { @@ -2314,6 +2377,47 @@ allowed_sandbox_modes = ["read-only", "workspace-write"] expect(second).not.toContain("sk-second"); }); + it("derives distinct shared-client keys for distinct headers without exposing them", () => { + const first = codexAppServerStartOptionsKey({ + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + headers: { + Authorization: "Bearer first", + "x-codex-client-session-token": "session-first", + }, + }); + const second = codexAppServerStartOptionsKey({ + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + headers: { + Authorization: "Bearer second", + "x-codex-client-session-token": "session-second", + }, + }); + + expect(first).not.toEqual(second); + expect( + codexAppServerStartOptionsKey({ + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + headers: { + Authorization: "Bearer first", + "x-codex-client-session-token": "session-first", + }, + }), + ).toEqual(first); + expect(first).not.toContain("Bearer first"); + expect(first).not.toContain("session-first"); + expect(second).not.toContain("Bearer second"); + expect(second).not.toContain("session-second"); + }); + it("keeps secret-derived shared-client keys stable across module reloads", async () => { const startOptions = { transport: "websocket" as const, diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index 6ffb54f41ab4..5856cc5e85d9 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -13,6 +13,11 @@ import { } from "openclaw/plugin-sdk/exec-approvals-runtime"; import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; +import { + buildSecretInputSchema, + normalizeResolvedSecretInputString, + type SecretInput, +} from "openclaw/plugin-sdk/secret-input"; import { normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime"; import { detectWindowsSpawnCommandInlineArgs } from "openclaw/plugin-sdk/windows-spawn"; import { z } from "zod"; @@ -211,8 +216,8 @@ export type CodexPluginConfig = { command?: string; args?: string[] | string; url?: string; - authToken?: string; - headers?: Record; + authToken?: SecretInput; + headers?: Record; clearEnv?: string[]; remoteWorkspaceRoot?: string; codeModeOnly?: boolean; @@ -294,6 +299,7 @@ const DEFAULT_CODEX_COMPUTER_USE_MARKETPLACE_DISCOVERY_TIMEOUT_MS = 60_000; const DEFAULT_CODEX_APP_SERVER_NETWORK_PROXY_PROFILE_PREFIX = "openclaw-network"; const codexAppServerTransportSchema = z.enum(["stdio", "websocket"]); +const SecretInputSchema = buildSecretInputSchema(); const codexAppServerPolicyModeSchema = z.enum(["yolo", "guardian"]); const codexAppServerApprovalPolicySchema = z.enum([ "never", @@ -387,8 +393,8 @@ const codexPluginConfigSchema = z command: z.string().optional(), args: z.union([z.array(z.string()), z.string()]).optional(), url: z.string().optional(), - authToken: z.string().optional(), - headers: z.record(z.string(), z.string()).optional(), + authToken: SecretInputSchema.optional(), + headers: z.record(z.string(), SecretInputSchema).optional(), clearEnv: z.array(z.string()).optional(), remoteWorkspaceRoot: codexAppServerRemoteWorkspaceRootSchema.optional(), codeModeOnly: z.boolean().optional(), @@ -531,7 +537,10 @@ export function resolveCodexAppServerRuntimeOptions( const args = resolveArgs(config.args, env.OPENCLAW_CODEX_APP_SERVER_ARGS); const headers = normalizeHeaders(config.headers); const clearEnv = normalizeStringList(config.clearEnv); - const authToken = readNonEmptyString(config.authToken); + const authToken = normalizeCodexAppServerSecretInput({ + value: config.authToken, + path: "plugins.entries.codex.config.appServer.authToken", + }); const url = readNonEmptyString(config.url); const connectionClass = inferCodexAppServerConnectionClass({ transport, url }); const remoteAppsSubstrate: CodexAppServerRemoteAppsSubstrate = "preconfigured"; @@ -868,9 +877,9 @@ export function codexAppServerStartOptionsKey( args: options.args, url: options.url ?? null, authToken: hashSecretForKey(options.authToken, "authToken"), - headers: Object.entries(options.headers).toSorted(([left], [right]) => - left.localeCompare(right), - ), + headers: Object.entries(options.headers) + .toSorted(([left], [right]) => left.localeCompare(right)) + .map(([key, value]) => [key, hashSecretForKey(value, `header:${key}`)]), env: Object.entries(options.env ?? {}) .toSorted(([left], [right]) => left.localeCompare(right)) .map(([key, value]) => [key, hashSecretForKey(value, `env:${key}`)]), @@ -2037,11 +2046,27 @@ function normalizeHeaders(value: unknown): Record { } return Object.fromEntries( Object.entries(value) - .map(([key, child]) => [key.trim(), readNonEmptyString(child)] as const) + .map( + ([key, child]) => + [ + key.trim(), + normalizeCodexAppServerSecretInput({ + value: child, + path: `plugins.entries.codex.config.appServer.headers.${key}`, + }), + ] as const, + ) .filter((entry): entry is readonly [string, string] => Boolean(entry[0] && entry[1])), ); } +function normalizeCodexAppServerSecretInput(params: { + value: unknown; + path: string; +}): string | undefined { + return normalizeResolvedSecretInputString(params); +} + function normalizeStringList(value: unknown): string[] { return normalizeTrimmedStringList(value); } diff --git a/src/plugins/config-contracts.ts b/src/plugins/config-contracts.ts index ae711c4d5ac7..e5e06aee4c03 100644 --- a/src/plugins/config-contracts.ts +++ b/src/plugins/config-contracts.ts @@ -1,6 +1,7 @@ /** Resolves plugin config contract metadata for scanners and secret/config policy checks. */ import { normalizeSortedUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { findBundledPluginMetadataById } from "./bundled-plugin-metadata.js"; import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { PluginManifestConfigContracts } from "./manifest.js"; @@ -60,6 +61,15 @@ export function resolvePluginConfigContractsById(params: { for (const plugin of registry.plugins) { bundledContractFallbacks.set(plugin.id, plugin.configContracts); } + if (bundledContractFallbacks.get(pluginId) === undefined) { + const bundledMetadata = findBundledPluginMetadataById(pluginId, { + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }); + if (bundledMetadata?.manifest.configContracts) { + bundledContractFallbacks.set(pluginId, bundledMetadata.manifest.configContracts); + } + } if (!bundledContractFallbacks.has(pluginId)) { bundledContractFallbacks.set(pluginId, undefined); } diff --git a/src/secrets/runtime-config-collectors-plugins.bundled.test.ts b/src/secrets/runtime-config-collectors-plugins.bundled.test.ts index de67cca64cbe..9d1d40809494 100644 --- a/src/secrets/runtime-config-collectors-plugins.bundled.test.ts +++ b/src/secrets/runtime-config-collectors-plugins.bundled.test.ts @@ -13,6 +13,86 @@ function envRef(id: string) { } describe("collectPluginConfigAssignments bundled plugin manifests", () => { + it("collects Codex app-server SecretRefs from bundled manifest contracts", () => { + expect( + findBundledPluginMetadataById("codex", { + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + })?.manifest.configContracts?.secretInputs?.paths, + ).toEqual([ + { path: "appServer.authToken", expected: "string" }, + { path: "appServer.headers.*", expected: "string" }, + ]); + const config = { + plugins: { + entries: { + codex: { + enabled: true, + config: { + appServer: { + transport: "websocket", + url: "wss://codex-app-server.example.internal/ws", + authToken: "$CODEX_APP_SERVER_TOKEN", + headers: { + Authorization: "Bearer literal-token", + "x-codex-client-session-token": envRef("CODEX_CLIENT_SESSION_TOKEN"), + }, + }, + }, + }, + }, + }, + } as OpenClawConfig; + expect( + resolvePluginConfigContractsById({ + config, + workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)), + env: {}, + fallbackToBundledMetadata: true, + fallbackToBundledMetadataForResolvedBundled: true, + pluginIds: ["codex"], + fallbackBundledPluginIds: ["codex"], + }).get("codex")?.configContracts.secretInputs?.paths, + ).toEqual([ + { path: "appServer.authToken", expected: "string" }, + { path: "appServer.headers.*", expected: "string" }, + ]); + const context = createResolverContext({ + sourceConfig: config, + env: {}, + }); + + collectPluginConfigAssignments({ + config, + defaults: undefined, + context, + loadablePluginOrigins: new Map([["codex", "bundled"]]), + }); + + expect({ + assignments: context.assignments.map((assignment) => assignment.path).toSorted(), + warnings: context.warnings, + }).toEqual({ + assignments: [ + "plugins.entries.codex.config.appServer.authToken", + "plugins.entries.codex.config.appServer.headers.x-codex-client-session-token", + ], + warnings: [], + }); + + context.assignments[0]?.apply("resolved-app-server-token"); + context.assignments[1]?.apply("resolved-session-token"); + expect(config.plugins?.entries?.codex?.config).toMatchObject({ + appServer: { + authToken: "resolved-app-server-token", + headers: { + Authorization: "Bearer literal-token", + "x-codex-client-session-token": "resolved-session-token", + }, + }, + }); + }); + it("collects voice-call SecretRef assignments from bundled manifest contracts", () => { expect( findBundledPluginMetadataById("voice-call", { diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 4c8da2c45045..86ea52ccd675 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -3,8 +3,15 @@ import { describe, expect, it } from "vitest"; import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts"; const EMPTY_LOADABLE_PLUGIN_ORIGINS = new Map(); +const BUNDLED_CODEX_PLUGIN_ORIGINS = new Map([["codex", "bundled" as const]]); const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks(); +const CODEX_APP_SERVER_TOKEN_REF = { + source: "env", + provider: "default", + id: "CODEX_APP_SERVER_TOKEN", +} as const; + function expectWarning( snapshot: Awaited>, expected: { code: string; path: string }, @@ -90,6 +97,78 @@ describe("secrets runtime snapshot", () => { }); }); + it("resolves active bundled Codex app-server plugin SecretRefs", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + plugins: { + entries: { + codex: { + enabled: true, + config: { + appServer: { + transport: "websocket", + url: "wss://codex-app-server.example.internal/ws", + authToken: CODEX_APP_SERVER_TOKEN_REF, + headers: { + Authorization: "Bearer literal-token", + "x-codex-client-session-token": "${CODEX_CLIENT_SESSION_TOKEN}", + }, + }, + }, + }, + }, + }, + }), + env: { + CODEX_APP_SERVER_TOKEN: "resolved-app-server-token", + CODEX_CLIENT_SESSION_TOKEN: "resolved-session-token", + }, + includeAuthStoreRefs: false, + loadablePluginOrigins: BUNDLED_CODEX_PLUGIN_ORIGINS, + }); + + expect(snapshot.config.plugins?.entries?.codex?.config).toMatchObject({ + appServer: { + authToken: "resolved-app-server-token", + headers: { + Authorization: "Bearer literal-token", + "x-codex-client-session-token": "resolved-session-token", + }, + }, + }); + }); + + it("fails active bundled Codex app-server plugin SecretRefs when env is missing", async () => { + await expect( + prepareSecretsRuntimeSnapshot({ + config: asConfig({ + plugins: { + entries: { + codex: { + enabled: true, + config: { + appServer: { + transport: "websocket", + url: "wss://codex-app-server.example.internal/ws", + authToken: CODEX_APP_SERVER_TOKEN_REF, + headers: { + "x-codex-client-session-token": "${CODEX_CLIENT_SESSION_TOKEN}", + }, + }, + }, + }, + }, + }, + }), + env: { + CODEX_CLIENT_SESSION_TOKEN: "resolved-session-token", + }, + includeAuthStoreRefs: false, + loadablePluginOrigins: BUNDLED_CODEX_PLUGIN_ORIGINS, + }), + ).rejects.toThrow('Environment variable "CODEX_APP_SERVER_TOKEN" is missing or empty.'); + }); + it("fails when an active exec ref id contains traversal segments", async () => { await expect( prepareSecretsRuntimeSnapshot({ diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 123a9c399430..7b04dc27a094 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -1,5 +1,6 @@ /** Builds the static and plugin-derived registry of secret migration targets. */ import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; +import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; import { resolvePluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { loadChannelSecretContractApiForRecord } from "./channel-contract-api.js"; import type { SecretTargetRegistryEntry } from "./target-registry-types.js"; @@ -66,7 +67,7 @@ function listBundledWebProviderSecretTargetRegistryEntries( } function listBundledPluginConfigSecretTargetRegistryEntries( - bundledPlugins: readonly PluginManifestRecord[], + bundledPlugins: readonly Pick[], ): SecretTargetRegistryEntry[] { const entries: SecretTargetRegistryEntry[] = []; const seen = new Set(); @@ -85,6 +86,24 @@ function listBundledPluginConfigSecretTargetRegistryEntries( return entries.toSorted((left, right) => left.id.localeCompare(right.id)); } +function listSourceBundledPluginConfigContractRecords(): Array< + Pick +> { + return listBundledPluginMetadata({ + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }).flatMap((metadata) => + metadata.manifest.configContracts + ? [ + { + id: metadata.manifest.id, + configContracts: metadata.manifest.configContracts, + }, + ] + : [], + ); +} + function listChannelSecretTargetRegistryEntries( channelPlugins: readonly PluginManifestRecord[], ): SecretTargetRegistryEntry[] { @@ -491,7 +510,10 @@ function loadSecretTargetRegistryFromPluginMetadata(params: { return [ ...CORE_SECRET_TARGET_REGISTRY, ...listBundledWebProviderSecretTargetRegistryEntries(bundledPlugins), - ...listBundledPluginConfigSecretTargetRegistryEntries(bundledPlugins), + ...listBundledPluginConfigSecretTargetRegistryEntries([ + ...bundledPlugins, + ...listSourceBundledPluginConfigContractRecords(), + ]), ...listChannelSecretTargetRegistryEntries(channelPlugins), ]; } diff --git a/src/secrets/target-registry.test.ts b/src/secrets/target-registry.test.ts index 1ada14813879..30cbeae1baca 100644 --- a/src/secrets/target-registry.test.ts +++ b/src/secrets/target-registry.test.ts @@ -87,6 +87,7 @@ describe("secret target registry", () => { it("derives bundled plugin SecretInput contract target paths from plugin manifests", () => { const coreTargetIds = new Set(getCoreSecretTargetRegistry().map((entry) => entry.id)); expect(coreTargetIds.has("plugins.entries.voice-call.config.twilio.authToken")).toBe(false); + expect(coreTargetIds.has("plugins.entries.codex.config.appServer.authToken")).toBe(false); const target = resolveConfigSecretTargetByPath([ "plugins", @@ -100,5 +101,30 @@ describe("secret target registry", () => { ]); expect(target?.entry?.id).toBe("plugins.entries.voice-call.config.tts.providers.*.apiKey"); + + const codexAuthTarget = resolveConfigSecretTargetByPath([ + "plugins", + "entries", + "codex", + "config", + "appServer", + "authToken", + ]); + expect(codexAuthTarget?.entry?.id).toBe( + "plugins.entries.codex.config.appServer.authToken", + ); + + const codexHeaderTarget = resolveConfigSecretTargetByPath([ + "plugins", + "entries", + "codex", + "config", + "appServer", + "headers", + "x-codex-client-session-token", + ]); + expect(codexHeaderTarget?.entry?.id).toBe( + "plugins.entries.codex.config.appServer.headers.*", + ); }); });