diff --git a/CHANGELOG.md b/CHANGELOG.md index 00394e0802e..e0de02e019f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai - Channels/CLI: keep `openclaw channels list --json` usable when provider usage fetching fails, and report per-provider usage errors without aborting the channel list. Refs #67595. - Agents/messaging: deliver distinct final commentary after same-target `message` tool sends while still deduping text/media already sent by the tool, so short closing remarks are no longer silently dropped. Fixes #76915. Thanks @hclsys. - Agents/messaging: preserve string thread IDs when matching message-tool reply dedupe routes, avoiding precision loss on numeric-looking topic IDs before channel plugin comparison. Thanks @vincentkoc. +- OpenAI Codex: honor `auth.order.openai-codex` when starting app-server clients without an explicit auth profile, so status/model probes and implicit startup use the configured Codex account instead of falling back to the default profile. Thanks @vincentkoc. - OpenAI Codex: let SSRF-guarded provider requests inherit OpenClaw's undici IPv4/IPv6 fallback policy, so ChatGPT-backed Codex runs recover on IPv4-working hosts when DNS still returns unreachable IPv6 addresses. Fixes #76857. Thanks @jplavoiemtl and @SymbolStar. - Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys. - Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc. diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index 5a6175a6b92..8093dcfc05d 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -421,6 +421,58 @@ describe("bridgeCodexAppServerStartOptions", () => { } }); + it("honors config auth order when selecting an implicit Codex profile", async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); + const request = vi.fn(async () => ({ type: "chatgptAuthTokens" })); + try { + upsertAuthProfile({ + agentDir, + profileId: "openai-codex:default", + credential: { + type: "oauth", + provider: "openai-codex", + access: "default-access-token", + refresh: "default-refresh-token", + expires: Date.now() + 24 * 60 * 60_000, + accountId: "account-default", + }, + }); + upsertAuthProfile({ + agentDir, + profileId: "openai-codex:work", + credential: { + type: "oauth", + provider: "openai-codex", + access: "work-access-token", + refresh: "work-refresh-token", + expires: Date.now() + 24 * 60 * 60_000, + accountId: "account-work", + }, + }); + + await applyCodexAppServerAuthProfile({ + client: { request } as never, + agentDir, + config: { + auth: { + order: { + "openai-codex": ["openai-codex:work", "openai-codex:default"], + }, + }, + }, + }); + + expect(request).toHaveBeenCalledWith("account/login/start", { + type: "chatgptAuthTokens", + accessToken: "work-access-token", + chatgptAccountId: "account-work", + chatgptPlanType: null, + }); + } finally { + await fs.rm(agentDir, { recursive: true, force: true }); + } + }); + it("refreshes an expired OpenAI Codex OAuth profile before app-server login", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); const request = vi.fn(async () => ({ type: "chatgptAuthTokens" })); diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index 41363a256f9..2d67b60b0c0 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -36,6 +36,7 @@ export async function bridgeCodexAppServerStartOptions(params: { startOptions: CodexAppServerStartOptions; agentDir: string; authProfileId?: string; + config?: AuthProfileOrderConfig; }): Promise { if (params.startOptions.transport !== "stdio") { return params.startOptions; @@ -48,10 +49,12 @@ export async function bridgeCodexAppServerStartOptions(params: { const authProfileId = resolveCodexAppServerAuthProfileId({ authProfileId: params.authProfileId, store, + config: params.config, }); const shouldClearInheritedOpenAiApiKey = shouldClearOpenAiApiKeyForCodexAuthProfile({ store, authProfileId, + config: params.config, }); return shouldClearInheritedOpenAiApiKey ? withClearedEnvironmentVariables(isolatedStartOptions, CODEX_APP_SERVER_API_KEY_ENV_VARS) @@ -139,10 +142,12 @@ export async function applyCodexAppServerAuthProfile(params: { agentDir: string; authProfileId?: string; startOptions?: CodexAppServerStartOptions; + config?: AuthProfileOrderConfig; }): Promise { const loginParams = await resolveCodexAppServerAuthProfileLoginParams({ agentDir: params.agentDir, authProfileId: params.authProfileId, + config: params.config, }); if (!loginParams) { if (params.startOptions?.transport !== "stdio") { @@ -164,6 +169,7 @@ export async function applyCodexAppServerAuthProfile(params: { function resolveCodexAppServerAuthProfileLoginParams(params: { agentDir: string; authProfileId?: string; + config?: AuthProfileOrderConfig; }): Promise { return resolveCodexAppServerAuthProfileLoginParamsInternal(params); } @@ -171,6 +177,7 @@ function resolveCodexAppServerAuthProfileLoginParams(params: { export async function refreshCodexAppServerAuthTokens(params: { agentDir: string; authProfileId?: string; + config?: AuthProfileOrderConfig; }): Promise { const loginParams = await resolveCodexAppServerAuthProfileLoginParamsInternal({ ...params, @@ -190,11 +197,13 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: { agentDir: string; authProfileId?: string; forceOAuthRefresh?: boolean; + config?: AuthProfileOrderConfig; }): Promise { const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }); const profileId = resolveCodexAppServerAuthProfileId({ authProfileId: params.authProfileId, store, + config: params.config, }); if (!profileId) { return undefined; @@ -203,7 +212,7 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: { if (!credential) { throw new Error(`Codex app-server auth profile "${profileId}" was not found.`); } - if (!isCodexAppServerAuthProvider(credential.provider)) { + if (!isCodexAppServerAuthProvider(credential.provider, params.config)) { throw new Error( `Codex app-server auth profile "${profileId}" must belong to provider "openai-codex" or a supported alias.`, ); @@ -211,6 +220,7 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: { const loginParams = await resolveLoginParamsForCredential(profileId, credential, { agentDir: params.agentDir, forceOAuthRefresh: params.forceOAuthRefresh === true, + config: params.config, }); if (!loginParams) { throw new Error( @@ -240,7 +250,7 @@ async function resolveCodexAppServerEnvApiKeyLoginParams(params: { async function resolveLoginParamsForCredential( profileId: string, credential: AuthProfileCredential, - params: { agentDir: string; forceOAuthRefresh: boolean }, + params: { agentDir: string; forceOAuthRefresh: boolean; config?: AuthProfileOrderConfig }, ): Promise { if (credential.type === "api_key") { const resolved = await resolveApiKeyForProfile({ @@ -265,6 +275,7 @@ async function resolveLoginParamsForCredential( const resolvedCredential = await resolveOAuthCredentialForCodexAppServer(profileId, credential, { agentDir: params.agentDir, forceRefresh: params.forceOAuthRefresh, + config: params.config, }); const accessToken = resolvedCredential.access?.trim(); return accessToken @@ -275,7 +286,7 @@ async function resolveLoginParamsForCredential( async function resolveOAuthCredentialForCodexAppServer( profileId: string, credential: OAuthCredential, - params: { agentDir: string; forceRefresh: boolean }, + params: { agentDir: string; forceRefresh: boolean; config?: AuthProfileOrderConfig }, ): Promise { const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir({ agentDir: params.agentDir, @@ -284,7 +295,8 @@ async function resolveOAuthCredentialForCodexAppServer( const store = ensureAuthProfileStore(ownerAgentDir, { allowKeychainPrompt: false }); const ownerCredential = store.profiles[profileId]; const credentialForOwner = - ownerCredential?.type === "oauth" && isCodexAppServerAuthProvider(ownerCredential.provider) + ownerCredential?.type === "oauth" && + isCodexAppServerAuthProvider(ownerCredential.provider, params.config) ? ownerCredential : credential; if (params.forceRefresh) { @@ -299,32 +311,36 @@ async function resolveOAuthCredentialForCodexAppServer( const refreshed = loadAuthProfileStoreForSecretsRuntime(ownerAgentDir).profiles[profileId]; const storedCredential = store.profiles[profileId]; const candidate = - refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider) + refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider, params.config) ? refreshed : storedCredential?.type === "oauth" && - isCodexAppServerAuthProvider(storedCredential.provider) + isCodexAppServerAuthProvider(storedCredential.provider, params.config) ? storedCredential : credential; return resolved?.apiKey ? { ...candidate, access: resolved.apiKey } : candidate; } -function isCodexAppServerAuthProvider(provider: string): boolean { - return resolveProviderIdForAuth(provider) === CODEX_APP_SERVER_AUTH_PROVIDER; +function isCodexAppServerAuthProvider(provider: string, config?: AuthProfileOrderConfig): boolean { + return resolveProviderIdForAuth(provider, { config }) === CODEX_APP_SERVER_AUTH_PROVIDER; } function shouldClearOpenAiApiKeyForCodexAuthProfile(params: { store: ReturnType; authProfileId?: string; + config?: AuthProfileOrderConfig; }): boolean { const profileId = params.authProfileId?.trim(); const credential = profileId ? params.store.profiles[profileId] : params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]; - return isCodexSubscriptionCredential(credential); + return isCodexSubscriptionCredential(credential, params.config); } -function isCodexSubscriptionCredential(credential: AuthProfileCredential | undefined): boolean { - if (!credential || !isCodexAppServerAuthProvider(credential.provider)) { +function isCodexSubscriptionCredential( + credential: AuthProfileCredential | undefined, + config?: AuthProfileOrderConfig, +): boolean { + if (!credential || !isCodexAppServerAuthProvider(credential.provider, config)) { return false; } return credential.type === "oauth" || credential.type === "token"; diff --git a/extensions/codex/src/app-server/client-factory.ts b/extensions/codex/src/app-server/client-factory.ts index 67731dd7c2a..7398ed75454 100644 --- a/extensions/codex/src/app-server/client-factory.ts +++ b/extensions/codex/src/app-server/client-factory.ts @@ -1,19 +1,26 @@ +import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js"; import type { CodexAppServerClient } from "./client.js"; import type { CodexAppServerStartOptions } from "./config.js"; +type AuthProfileOrderConfig = Parameters< + typeof resolveCodexAppServerAuthProfileIdForAgent +>[0]["config"]; + export type CodexAppServerClientFactory = ( startOptions?: CodexAppServerStartOptions, authProfileId?: string, agentDir?: string, + config?: AuthProfileOrderConfig, ) => Promise; export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = ( startOptions, authProfileId, agentDir, + config, ) => import("./shared-client.js").then(({ getSharedCodexAppServerClient }) => - getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir }), + getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }), ); export function createCodexAppServerClientFactoryTestHooks( diff --git a/extensions/codex/src/app-server/compact.ts b/extensions/codex/src/app-server/compact.ts index 7b222ea565d..45a5cc90767 100644 --- a/extensions/codex/src/app-server/compact.ts +++ b/extensions/codex/src/app-server/compact.ts @@ -110,7 +110,7 @@ async function compactCodexNativeThread( options: { pluginConfig?: unknown } = {}, ): Promise { const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: options.pluginConfig }); - const binding = await readCodexAppServerBinding(params.sessionFile); + const binding = await readCodexAppServerBinding(params.sessionFile, { config: params.config }); if (!binding?.threadId) { return { ok: false, compacted: false, reason: "no codex app-server thread binding" }; } @@ -127,6 +127,7 @@ async function compactCodexNativeThread( appServer.start, requestedAuthProfileId ?? binding.authProfileId, params.agentDir, + params.config, ); const waiter = createCodexNativeCompactionWaiter(client, binding.threadId); let completion: CodexNativeCompactionCompletion; diff --git a/extensions/codex/src/app-server/models.ts b/extensions/codex/src/app-server/models.ts index 4a63cb8cd93..4cd84436914 100644 --- a/extensions/codex/src/app-server/models.ts +++ b/extensions/codex/src/app-server/models.ts @@ -1,3 +1,4 @@ +import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js"; import type { CodexAppServerClient } from "./client.js"; import type { CodexAppServerStartOptions } from "./config.js"; import type { v2 } from "./protocol-generated/typescript/index.js"; @@ -29,6 +30,7 @@ export type CodexAppServerListModelsOptions = { startOptions?: CodexAppServerStartOptions; authProfileId?: string; agentDir?: string; + config?: Parameters[0]["config"]; sharedClient?: boolean; }; @@ -79,12 +81,14 @@ async function withCodexAppServerModelClient( timeoutMs, authProfileId: options.authProfileId, agentDir: options.agentDir, + config: options.config, }) : await createIsolatedCodexAppServerClient({ startOptions: options.startOptions, timeoutMs, authProfileId: options.authProfileId, agentDir: options.agentDir, + config: options.config, }); try { return await run({ client, timeoutMs }); diff --git a/extensions/codex/src/app-server/request.ts b/extensions/codex/src/app-server/request.ts index 1a6f0dd2714..4afa1d07cd2 100644 --- a/extensions/codex/src/app-server/request.ts +++ b/extensions/codex/src/app-server/request.ts @@ -1,3 +1,4 @@ +import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js"; import type { CodexAppServerStartOptions } from "./config.js"; import type { CodexAppServerRequestMethod, @@ -14,6 +15,7 @@ export async function requestCodexAppServerJson[0]["config"]; }): Promise>; export async function requestCodexAppServerJson(params: { method: string; @@ -21,6 +23,7 @@ export async function requestCodexAppServerJson(param timeoutMs?: number; startOptions?: CodexAppServerStartOptions; authProfileId?: string; + config?: Parameters[0]["config"]; }): Promise; export async function requestCodexAppServerJson(params: { method: string; @@ -28,6 +31,7 @@ export async function requestCodexAppServerJson(param timeoutMs?: number; startOptions?: CodexAppServerStartOptions; authProfileId?: string; + config?: Parameters[0]["config"]; }): Promise { const timeoutMs = params.timeoutMs ?? 60_000; return await withTimeout( @@ -36,6 +40,7 @@ export async function requestCodexAppServerJson(param startOptions: params.startOptions, timeoutMs, authProfileId: params.authProfileId, + config: params.config, }); return await client.request(params.method, params.requestParams, { timeoutMs }); })(), diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 50783c861a8..a8f391a8ff8 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -561,6 +561,7 @@ export async function runCodexAppServerAttempt( appServer.start, startupAuthProfileId, agentDir, + params.config, ); attemptedClient = startupClient; startupClientForCleanup = startupClient; diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index dd5318c3b1c..8e035c44d71 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -6,6 +6,9 @@ import { createClientHarness } from "./test-support.js"; const mocks = vi.hoisted(() => ({ bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions), applyCodexAppServerAuthProfile: vi.fn(async () => undefined), + resolveCodexAppServerAuthProfileIdForAgent: vi.fn( + (params?: { authProfileId?: string }) => params?.authProfileId, + ), resolveManagedCodexAppServerStartOptions: vi.fn(async (startOptions) => startOptions), embeddedAgentLog: { debug: vi.fn(), warn: vi.fn() }, resolveOpenClawAgentDir: vi.fn(() => "/tmp/openclaw-agent"), @@ -14,6 +17,7 @@ const mocks = vi.hoisted(() => ({ vi.mock("./auth-bridge.js", () => ({ applyCodexAppServerAuthProfile: mocks.applyCodexAppServerAuthProfile, bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions, + resolveCodexAppServerAuthProfileIdForAgent: mocks.resolveCodexAppServerAuthProfileIdForAgent, })); vi.mock("./managed-binary.js", () => ({ @@ -67,6 +71,10 @@ describe("shared Codex app-server client", () => { vi.restoreAllMocks(); mocks.bridgeCodexAppServerStartOptions.mockClear(); mocks.applyCodexAppServerAuthProfile.mockClear(); + mocks.resolveCodexAppServerAuthProfileIdForAgent.mockClear(); + mocks.resolveCodexAppServerAuthProfileIdForAgent.mockImplementation( + (params?: { authProfileId?: string }) => params?.authProfileId, + ); mocks.resolveManagedCodexAppServerStartOptions.mockClear(); mocks.resolveManagedCodexAppServerStartOptions.mockImplementation( async (startOptions) => startOptions, @@ -147,6 +155,37 @@ describe("shared Codex app-server client", () => { ); }); + it("resolves the configured implicit auth profile before sharing a client", async () => { + const harness = createClientHarness(); + vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client); + const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } }; + mocks.resolveCodexAppServerAuthProfileIdForAgent.mockReturnValue("openai-codex:work"); + + const listPromise = listCodexAppServerModels({ + timeoutMs: 1000, + config, + }); + await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)"); + await sendEmptyModelList(harness); + + await expect(listPromise).resolves.toEqual({ models: [] }); + expect(mocks.resolveCodexAppServerAuthProfileIdForAgent).toHaveBeenCalledWith( + expect.objectContaining({ config }), + ); + expect(mocks.bridgeCodexAppServerStartOptions).toHaveBeenCalledWith( + expect.objectContaining({ + authProfileId: "openai-codex:work", + config, + }), + ); + expect(mocks.applyCodexAppServerAuthProfile).toHaveBeenCalledWith( + expect.objectContaining({ + authProfileId: "openai-codex:work", + config, + }), + ); + }); + it("uses the selected agent dir for shared app-server auth bridging", async () => { const harness = createClientHarness(); vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client); diff --git a/extensions/codex/src/app-server/shared-client.ts b/extensions/codex/src/app-server/shared-client.ts index 1487b7bd84f..1f5f4c23bb5 100644 --- a/extensions/codex/src/app-server/shared-client.ts +++ b/extensions/codex/src/app-server/shared-client.ts @@ -1,5 +1,9 @@ import { resolveOpenClawAgentDir } from "openclaw/plugin-sdk/provider-auth"; -import { applyCodexAppServerAuthProfile, bridgeCodexAppServerStartOptions } from "./auth-bridge.js"; +import { + applyCodexAppServerAuthProfile, + bridgeCodexAppServerStartOptions, + resolveCodexAppServerAuthProfileIdForAgent, +} from "./auth-bridge.js"; import { CodexAppServerClient } from "./client.js"; import { codexAppServerStartOptionsKey, @@ -30,19 +34,26 @@ export async function getSharedCodexAppServerClient(options?: { timeoutMs?: number; authProfileId?: string; agentDir?: string; + config?: Parameters[0]["config"]; }): Promise { const state = getSharedCodexAppServerClientState(); const agentDir = options?.agentDir ?? resolveOpenClawAgentDir(); + const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({ + authProfileId: options?.authProfileId, + agentDir, + config: options?.config, + }); const requestedStartOptions = options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start; const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions); const startOptions = await bridgeCodexAppServerStartOptions({ startOptions: managedStartOptions, agentDir, - authProfileId: options?.authProfileId, + authProfileId, + config: options?.config, }); const key = codexAppServerStartOptionsKey(startOptions, { - authProfileId: options?.authProfileId, + authProfileId, agentDir, }); if (state.key && state.key !== key) { @@ -60,8 +71,9 @@ export async function getSharedCodexAppServerClient(options?: { await applyCodexAppServerAuthProfile({ client, agentDir, - authProfileId: options?.authProfileId, + authProfileId, startOptions, + config: options?.config, }); return client; } catch (error) { @@ -90,15 +102,22 @@ export async function createIsolatedCodexAppServerClient(options?: { timeoutMs?: number; authProfileId?: string; agentDir?: string; + config?: Parameters[0]["config"]; }): Promise { const agentDir = options?.agentDir ?? resolveOpenClawAgentDir(); + const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({ + authProfileId: options?.authProfileId, + agentDir, + config: options?.config, + }); const requestedStartOptions = options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start; const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions); const startOptions = await bridgeCodexAppServerStartOptions({ startOptions: managedStartOptions, agentDir, - authProfileId: options?.authProfileId, + authProfileId, + config: options?.config, }); const client = CodexAppServerClient.start(startOptions); const initialize = client.initialize(); @@ -107,8 +126,9 @@ export async function createIsolatedCodexAppServerClient(options?: { await applyCodexAppServerAuthProfile({ client, agentDir, - authProfileId: options?.authProfileId, + authProfileId, startOptions, + config: options?.config, }); return client; } catch (error) { diff --git a/extensions/codex/src/command-handlers.ts b/extensions/codex/src/command-handlers.ts index c33ae755176..761b6ac1813 100644 --- a/extensions/codex/src/command-handlers.ts +++ b/extensions/codex/src/command-handlers.ts @@ -185,12 +185,16 @@ export async function handleCodexSubcommand( return { text: buildHelp() }; } if (normalized === "status") { - return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig)) }; + return { + text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config)), + }; } if (normalized === "models") { return { text: formatModels( - await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100)), + await deps.listCodexAppServerModels( + deps.requestOptions(options.pluginConfig, 100, ctx.config), + ), ), }; } diff --git a/extensions/codex/src/command-rpc.ts b/extensions/codex/src/command-rpc.ts index ee6734bd62f..1de193734cc 100644 --- a/extensions/codex/src/command-rpc.ts +++ b/extensions/codex/src/command-rpc.ts @@ -1,3 +1,4 @@ +import type { resolveCodexAppServerAuthProfileIdForAgent } from "./app-server/auth-bridge.js"; import { CODEX_CONTROL_METHODS, describeControlFailure, @@ -15,12 +16,21 @@ import { requestCodexAppServerJson } from "./app-server/request.js"; export type SafeValue = { ok: true; value: T } | { ok: false; error: string }; -export function requestOptions(pluginConfig: unknown, limit: number) { +type AuthProfileOrderConfig = Parameters< + typeof resolveCodexAppServerAuthProfileIdForAgent +>[0]["config"]; + +export function requestOptions( + pluginConfig: unknown, + limit: number, + config?: AuthProfileOrderConfig, +) { const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig }); return { limit, timeoutMs: runtime.requestTimeoutMs, startOptions: runtime.start, + config, }; } @@ -30,16 +40,19 @@ export function codexControlRequest( pluginConfig: unknown, method: M, requestParams: CodexAppServerRequestParams, + options?: { config?: AuthProfileOrderConfig }, ): Promise>; export function codexControlRequest( pluginConfig: unknown, method: CodexControlMethod, requestParams?: JsonValue, + options?: { config?: AuthProfileOrderConfig }, ): Promise; export async function codexControlRequest( pluginConfig: unknown, method: CodexControlMethod, requestParams?: unknown, + options: { config?: AuthProfileOrderConfig } = {}, ) { const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig }); return await requestCodexAppServerJson({ @@ -47,6 +60,7 @@ export async function codexControlRequest( requestParams, timeoutMs: runtime.requestTimeoutMs, startOptions: runtime.start, + config: options.config, }); } @@ -54,35 +68,56 @@ export function safeCodexControlRequest( pluginConfig: unknown, method: M, requestParams: CodexAppServerRequestParams, + options?: { config?: AuthProfileOrderConfig }, ): Promise>>; export function safeCodexControlRequest( pluginConfig: unknown, method: CodexControlMethod, requestParams?: JsonValue, + options?: { config?: AuthProfileOrderConfig }, ): Promise>; export async function safeCodexControlRequest( pluginConfig: unknown, method: CodexControlMethod, requestParams?: unknown, + options: { config?: AuthProfileOrderConfig } = {}, ) { return await safeValue( - async () => await codexControlRequest(pluginConfig, method, requestParams as JsonValue), + async () => + await codexControlRequest(pluginConfig, method, requestParams as JsonValue, options), ); } -async function safeCodexModelList(pluginConfig: unknown, limit: number) { +async function safeCodexModelList( + pluginConfig: unknown, + limit: number, + config?: AuthProfileOrderConfig, +) { return await safeValue( - async () => await listCodexAppServerModels(requestOptions(pluginConfig, limit)), + async () => await listCodexAppServerModels(requestOptions(pluginConfig, limit, config)), ); } -export async function readCodexStatusProbes(pluginConfig: unknown) { +export async function readCodexStatusProbes( + pluginConfig: unknown, + config?: AuthProfileOrderConfig, +) { const [models, account, limits, mcps, skills] = await Promise.all([ - safeCodexModelList(pluginConfig, 20), - safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }), - safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.rateLimits, undefined), - safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }), - safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}), + safeCodexModelList(pluginConfig, 20, config), + safeCodexControlRequest( + pluginConfig, + CODEX_CONTROL_METHODS.account, + { refreshToken: false }, + { config }, + ), + safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.rateLimits, undefined, { config }), + safeCodexControlRequest( + pluginConfig, + CODEX_CONTROL_METHODS.listMcpServers, + { limit: 100 }, + { config }, + ), + safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, { config }), ]); return { models, account, limits, mcps, skills }; diff --git a/extensions/codex/src/commands.test.ts b/extensions/codex/src/commands.test.ts index 9b453a5052e..ab09902df6b 100644 --- a/extensions/codex/src/commands.test.ts +++ b/extensions/codex/src/commands.test.ts @@ -41,16 +41,23 @@ function createDeps(overrides: Partial = {}): Partial ({ - limit, - timeoutMs: 1000, - startOptions: { - transport: "stdio", - command: "codex", - args: ["app-server", "--listen", "stdio://"], - headers: {}, - } satisfies CodexAppServerStartOptions, - })), + requestOptions: vi.fn( + ( + _pluginConfig: unknown, + limit: number, + config?: Parameters>[2], + ) => ({ + limit, + timeoutMs: 1000, + startOptions: { + transport: "stdio", + command: "codex", + args: ["app-server", "--listen", "stdio://"], + headers: {}, + } satisfies CodexAppServerStartOptions, + config, + }), + ), safeCodexControlRequest: vi.fn(), ...overrides, }; @@ -132,6 +139,7 @@ describe("codex command", () => { }); it("shows model ids from Codex app-server", async () => { + const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } }; const deps = createDeps({ listCodexAppServerModels: vi.fn(async () => ({ models: [ @@ -145,9 +153,13 @@ describe("codex command", () => { })), }); - await expect(handleCodexCommand(createContext("models"), { deps })).resolves.toEqual({ + await expect( + handleCodexCommand(createContext("models", undefined, { config }), { deps }), + ).resolves.toEqual({ text: "Codex models:\n- gpt-5.4", }); + expect(deps.requestOptions).toHaveBeenCalledWith(undefined, 100, config); + expect(deps.listCodexAppServerModels).toHaveBeenCalledWith(expect.objectContaining({ config })); }); it("shows when Codex app-server model output is truncated", async () => { @@ -172,6 +184,7 @@ describe("codex command", () => { }); it("reports status unavailable when every Codex probe fails", async () => { + const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } }; const offline = { ok: false as const, error: "offline" }; const deps = createDeps({ readCodexStatusProbes: vi.fn(async () => ({ @@ -183,7 +196,9 @@ describe("codex command", () => { })), }); - await expect(handleCodexCommand(createContext("status"), { deps })).resolves.toEqual({ + await expect( + handleCodexCommand(createContext("status", undefined, { config }), { deps }), + ).resolves.toEqual({ text: [ "Codex app-server: unavailable", "Models: offline", @@ -193,6 +208,7 @@ describe("codex command", () => { "Skills: offline", ].join("\n"), }); + expect(deps.readCodexStatusProbes).toHaveBeenCalledWith(undefined, config); }); it("formats generated account/read responses", async () => {