Skip to content

Commit eb1a0aa

Browse files
committed
fix(codex): honor app-server auth order
1 parent 3a8ea14 commit eb1a0aa

13 files changed

Lines changed: 244 additions & 43 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
5151
- 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.
5252
- 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.
5353
- 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.
54+
- 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.
5455
- 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.
5556
- 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.
5657
- 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.

extensions/codex/src/app-server/auth-bridge.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,58 @@ describe("bridgeCodexAppServerStartOptions", () => {
421421
}
422422
});
423423

424+
it("honors config auth order when selecting an implicit Codex profile", async () => {
425+
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
426+
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
427+
try {
428+
upsertAuthProfile({
429+
agentDir,
430+
profileId: "openai-codex:default",
431+
credential: {
432+
type: "oauth",
433+
provider: "openai-codex",
434+
access: "default-access-token",
435+
refresh: "default-refresh-token",
436+
expires: Date.now() + 24 * 60 * 60_000,
437+
accountId: "account-default",
438+
},
439+
});
440+
upsertAuthProfile({
441+
agentDir,
442+
profileId: "openai-codex:work",
443+
credential: {
444+
type: "oauth",
445+
provider: "openai-codex",
446+
access: "work-access-token",
447+
refresh: "work-refresh-token",
448+
expires: Date.now() + 24 * 60 * 60_000,
449+
accountId: "account-work",
450+
},
451+
});
452+
453+
await applyCodexAppServerAuthProfile({
454+
client: { request } as never,
455+
agentDir,
456+
config: {
457+
auth: {
458+
order: {
459+
"openai-codex": ["openai-codex:work", "openai-codex:default"],
460+
},
461+
},
462+
},
463+
});
464+
465+
expect(request).toHaveBeenCalledWith("account/login/start", {
466+
type: "chatgptAuthTokens",
467+
accessToken: "work-access-token",
468+
chatgptAccountId: "account-work",
469+
chatgptPlanType: null,
470+
});
471+
} finally {
472+
await fs.rm(agentDir, { recursive: true, force: true });
473+
}
474+
});
475+
424476
it("refreshes an expired OpenAI Codex OAuth profile before app-server login", async () => {
425477
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
426478
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));

extensions/codex/src/app-server/auth-bridge.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export async function bridgeCodexAppServerStartOptions(params: {
3636
startOptions: CodexAppServerStartOptions;
3737
agentDir: string;
3838
authProfileId?: string;
39+
config?: AuthProfileOrderConfig;
3940
}): Promise<CodexAppServerStartOptions> {
4041
if (params.startOptions.transport !== "stdio") {
4142
return params.startOptions;
@@ -48,10 +49,12 @@ export async function bridgeCodexAppServerStartOptions(params: {
4849
const authProfileId = resolveCodexAppServerAuthProfileId({
4950
authProfileId: params.authProfileId,
5051
store,
52+
config: params.config,
5153
});
5254
const shouldClearInheritedOpenAiApiKey = shouldClearOpenAiApiKeyForCodexAuthProfile({
5355
store,
5456
authProfileId,
57+
config: params.config,
5558
});
5659
return shouldClearInheritedOpenAiApiKey
5760
? withClearedEnvironmentVariables(isolatedStartOptions, CODEX_APP_SERVER_API_KEY_ENV_VARS)
@@ -139,10 +142,12 @@ export async function applyCodexAppServerAuthProfile(params: {
139142
agentDir: string;
140143
authProfileId?: string;
141144
startOptions?: CodexAppServerStartOptions;
145+
config?: AuthProfileOrderConfig;
142146
}): Promise<void> {
143147
const loginParams = await resolveCodexAppServerAuthProfileLoginParams({
144148
agentDir: params.agentDir,
145149
authProfileId: params.authProfileId,
150+
config: params.config,
146151
});
147152
if (!loginParams) {
148153
if (params.startOptions?.transport !== "stdio") {
@@ -164,13 +169,15 @@ export async function applyCodexAppServerAuthProfile(params: {
164169
function resolveCodexAppServerAuthProfileLoginParams(params: {
165170
agentDir: string;
166171
authProfileId?: string;
172+
config?: AuthProfileOrderConfig;
167173
}): Promise<LoginAccountParams | undefined> {
168174
return resolveCodexAppServerAuthProfileLoginParamsInternal(params);
169175
}
170176

171177
export async function refreshCodexAppServerAuthTokens(params: {
172178
agentDir: string;
173179
authProfileId?: string;
180+
config?: AuthProfileOrderConfig;
174181
}): Promise<ChatgptAuthTokensRefreshResponse> {
175182
const loginParams = await resolveCodexAppServerAuthProfileLoginParamsInternal({
176183
...params,
@@ -190,11 +197,13 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
190197
agentDir: string;
191198
authProfileId?: string;
192199
forceOAuthRefresh?: boolean;
200+
config?: AuthProfileOrderConfig;
193201
}): Promise<LoginAccountParams | undefined> {
194202
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
195203
const profileId = resolveCodexAppServerAuthProfileId({
196204
authProfileId: params.authProfileId,
197205
store,
206+
config: params.config,
198207
});
199208
if (!profileId) {
200209
return undefined;
@@ -203,14 +212,15 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
203212
if (!credential) {
204213
throw new Error(`Codex app-server auth profile "${profileId}" was not found.`);
205214
}
206-
if (!isCodexAppServerAuthProvider(credential.provider)) {
215+
if (!isCodexAppServerAuthProvider(credential.provider, params.config)) {
207216
throw new Error(
208217
`Codex app-server auth profile "${profileId}" must belong to provider "openai-codex" or a supported alias.`,
209218
);
210219
}
211220
const loginParams = await resolveLoginParamsForCredential(profileId, credential, {
212221
agentDir: params.agentDir,
213222
forceOAuthRefresh: params.forceOAuthRefresh === true,
223+
config: params.config,
214224
});
215225
if (!loginParams) {
216226
throw new Error(
@@ -240,7 +250,7 @@ async function resolveCodexAppServerEnvApiKeyLoginParams(params: {
240250
async function resolveLoginParamsForCredential(
241251
profileId: string,
242252
credential: AuthProfileCredential,
243-
params: { agentDir: string; forceOAuthRefresh: boolean },
253+
params: { agentDir: string; forceOAuthRefresh: boolean; config?: AuthProfileOrderConfig },
244254
): Promise<LoginAccountParams | undefined> {
245255
if (credential.type === "api_key") {
246256
const resolved = await resolveApiKeyForProfile({
@@ -265,6 +275,7 @@ async function resolveLoginParamsForCredential(
265275
const resolvedCredential = await resolveOAuthCredentialForCodexAppServer(profileId, credential, {
266276
agentDir: params.agentDir,
267277
forceRefresh: params.forceOAuthRefresh,
278+
config: params.config,
268279
});
269280
const accessToken = resolvedCredential.access?.trim();
270281
return accessToken
@@ -275,7 +286,7 @@ async function resolveLoginParamsForCredential(
275286
async function resolveOAuthCredentialForCodexAppServer(
276287
profileId: string,
277288
credential: OAuthCredential,
278-
params: { agentDir: string; forceRefresh: boolean },
289+
params: { agentDir: string; forceRefresh: boolean; config?: AuthProfileOrderConfig },
279290
): Promise<OAuthCredential> {
280291
const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir({
281292
agentDir: params.agentDir,
@@ -284,7 +295,8 @@ async function resolveOAuthCredentialForCodexAppServer(
284295
const store = ensureAuthProfileStore(ownerAgentDir, { allowKeychainPrompt: false });
285296
const ownerCredential = store.profiles[profileId];
286297
const credentialForOwner =
287-
ownerCredential?.type === "oauth" && isCodexAppServerAuthProvider(ownerCredential.provider)
298+
ownerCredential?.type === "oauth" &&
299+
isCodexAppServerAuthProvider(ownerCredential.provider, params.config)
288300
? ownerCredential
289301
: credential;
290302
if (params.forceRefresh) {
@@ -299,32 +311,36 @@ async function resolveOAuthCredentialForCodexAppServer(
299311
const refreshed = loadAuthProfileStoreForSecretsRuntime(ownerAgentDir).profiles[profileId];
300312
const storedCredential = store.profiles[profileId];
301313
const candidate =
302-
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider)
314+
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider, params.config)
303315
? refreshed
304316
: storedCredential?.type === "oauth" &&
305-
isCodexAppServerAuthProvider(storedCredential.provider)
317+
isCodexAppServerAuthProvider(storedCredential.provider, params.config)
306318
? storedCredential
307319
: credential;
308320
return resolved?.apiKey ? { ...candidate, access: resolved.apiKey } : candidate;
309321
}
310322

311-
function isCodexAppServerAuthProvider(provider: string): boolean {
312-
return resolveProviderIdForAuth(provider) === CODEX_APP_SERVER_AUTH_PROVIDER;
323+
function isCodexAppServerAuthProvider(provider: string, config?: AuthProfileOrderConfig): boolean {
324+
return resolveProviderIdForAuth(provider, { config }) === CODEX_APP_SERVER_AUTH_PROVIDER;
313325
}
314326

315327
function shouldClearOpenAiApiKeyForCodexAuthProfile(params: {
316328
store: ReturnType<typeof ensureAuthProfileStore>;
317329
authProfileId?: string;
330+
config?: AuthProfileOrderConfig;
318331
}): boolean {
319332
const profileId = params.authProfileId?.trim();
320333
const credential = profileId
321334
? params.store.profiles[profileId]
322335
: params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID];
323-
return isCodexSubscriptionCredential(credential);
336+
return isCodexSubscriptionCredential(credential, params.config);
324337
}
325338

326-
function isCodexSubscriptionCredential(credential: AuthProfileCredential | undefined): boolean {
327-
if (!credential || !isCodexAppServerAuthProvider(credential.provider)) {
339+
function isCodexSubscriptionCredential(
340+
credential: AuthProfileCredential | undefined,
341+
config?: AuthProfileOrderConfig,
342+
): boolean {
343+
if (!credential || !isCodexAppServerAuthProvider(credential.provider, config)) {
328344
return false;
329345
}
330346
return credential.type === "oauth" || credential.type === "token";

extensions/codex/src/app-server/client-factory.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1+
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
12
import type { CodexAppServerClient } from "./client.js";
23
import type { CodexAppServerStartOptions } from "./config.js";
34

5+
type AuthProfileOrderConfig = Parameters<
6+
typeof resolveCodexAppServerAuthProfileIdForAgent
7+
>[0]["config"];
8+
49
export type CodexAppServerClientFactory = (
510
startOptions?: CodexAppServerStartOptions,
611
authProfileId?: string,
712
agentDir?: string,
13+
config?: AuthProfileOrderConfig,
814
) => Promise<CodexAppServerClient>;
915

1016
export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
1117
startOptions,
1218
authProfileId,
1319
agentDir,
20+
config,
1421
) =>
1522
import("./shared-client.js").then(({ getSharedCodexAppServerClient }) =>
16-
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir }),
23+
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
1724
);
1825

1926
export function createCodexAppServerClientFactoryTestHooks(

extensions/codex/src/app-server/compact.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async function compactCodexNativeThread(
110110
options: { pluginConfig?: unknown } = {},
111111
): Promise<EmbeddedPiCompactResult | undefined> {
112112
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: options.pluginConfig });
113-
const binding = await readCodexAppServerBinding(params.sessionFile);
113+
const binding = await readCodexAppServerBinding(params.sessionFile, { config: params.config });
114114
if (!binding?.threadId) {
115115
return { ok: false, compacted: false, reason: "no codex app-server thread binding" };
116116
}
@@ -127,6 +127,7 @@ async function compactCodexNativeThread(
127127
appServer.start,
128128
requestedAuthProfileId ?? binding.authProfileId,
129129
params.agentDir,
130+
params.config,
130131
);
131132
const waiter = createCodexNativeCompactionWaiter(client, binding.threadId);
132133
let completion: CodexNativeCompactionCompletion;

extensions/codex/src/app-server/models.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
12
import type { CodexAppServerClient } from "./client.js";
23
import type { CodexAppServerStartOptions } from "./config.js";
34
import type { v2 } from "./protocol-generated/typescript/index.js";
@@ -29,6 +30,7 @@ export type CodexAppServerListModelsOptions = {
2930
startOptions?: CodexAppServerStartOptions;
3031
authProfileId?: string;
3132
agentDir?: string;
33+
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
3234
sharedClient?: boolean;
3335
};
3436

@@ -79,12 +81,14 @@ async function withCodexAppServerModelClient<T>(
7981
timeoutMs,
8082
authProfileId: options.authProfileId,
8183
agentDir: options.agentDir,
84+
config: options.config,
8285
})
8386
: await createIsolatedCodexAppServerClient({
8487
startOptions: options.startOptions,
8588
timeoutMs,
8689
authProfileId: options.authProfileId,
8790
agentDir: options.agentDir,
91+
config: options.config,
8892
});
8993
try {
9094
return await run({ client, timeoutMs });

extensions/codex/src/app-server/request.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
12
import type { CodexAppServerStartOptions } from "./config.js";
23
import type {
34
CodexAppServerRequestMethod,
@@ -14,20 +15,23 @@ export async function requestCodexAppServerJson<M extends CodexAppServerRequestM
1415
timeoutMs?: number;
1516
startOptions?: CodexAppServerStartOptions;
1617
authProfileId?: string;
18+
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
1719
}): Promise<CodexAppServerRequestResult<M>>;
1820
export async function requestCodexAppServerJson<T = JsonValue | undefined>(params: {
1921
method: string;
2022
requestParams?: unknown;
2123
timeoutMs?: number;
2224
startOptions?: CodexAppServerStartOptions;
2325
authProfileId?: string;
26+
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
2427
}): Promise<T>;
2528
export async function requestCodexAppServerJson<T = JsonValue | undefined>(params: {
2629
method: string;
2730
requestParams?: unknown;
2831
timeoutMs?: number;
2932
startOptions?: CodexAppServerStartOptions;
3033
authProfileId?: string;
34+
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
3135
}): Promise<T> {
3236
const timeoutMs = params.timeoutMs ?? 60_000;
3337
return await withTimeout(
@@ -36,6 +40,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
3640
startOptions: params.startOptions,
3741
timeoutMs,
3842
authProfileId: params.authProfileId,
43+
config: params.config,
3944
});
4045
return await client.request<T>(params.method, params.requestParams, { timeoutMs });
4146
})(),

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ export async function runCodexAppServerAttempt(
561561
appServer.start,
562562
startupAuthProfileId,
563563
agentDir,
564+
params.config,
564565
);
565566
attemptedClient = startupClient;
566567
startupClientForCleanup = startupClient;

0 commit comments

Comments
 (0)