diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index 02a31650f9a..f39e9f00daf 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -12,6 +12,7 @@ import { bridgeCodexAppServerStartOptions, refreshCodexAppServerAuthTokens, resolveCodexAppServerAuthAccountCacheKey, + resolveCodexAppServerAuthProfileId, resolveCodexAppServerHomeDir, resolveCodexAppServerNativeHomeDir, } from "./auth-bridge.js"; @@ -651,6 +652,30 @@ describe("bridgeCodexAppServerStartOptions", () => { } }); + it("selects an oauthRef-backed Codex profile for app-server login", () => { + expect( + resolveCodexAppServerAuthProfileId({ + store: { + version: 1, + profiles: { + "openai-codex:default": { + type: "oauth", + provider: "openai-codex", + access: "", + refresh: "", + expires: Date.now() + 60_000, + oauthRef: { + source: "openclaw-credentials", + provider: "openai-codex", + id: "0123456789abcdef0123456789abcdef", + }, + }, + }, + }, + }), + ).toBe("openai-codex:default"); + }); + it("applies native Codex CLI OAuth when no OpenClaw auth profile exists", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); const agentDir = path.join(root, "agent"); diff --git a/src/agents/auth-profiles/credential-state.test.ts b/src/agents/auth-profiles/credential-state.test.ts index 936a7bbe64b..53656aabc24 100644 --- a/src/agents/auth-profiles/credential-state.test.ts +++ b/src/agents/auth-profiles/credential-state.test.ts @@ -103,4 +103,23 @@ describe("evaluateStoredCredentialEligibility", () => { }); expect(result).toEqual({ eligible: false, reasonCode: "invalid_expires" }); }); + + it("marks oauth with oauthRef as eligible", () => { + const result = evaluateStoredCredentialEligibility({ + credential: { + type: "oauth", + provider: "openai-codex", + access: "", + refresh: "", + expires: now + 60_000, + oauthRef: { + source: "openclaw-credentials", + provider: "openai-codex", + id: "0123456789abcdef0123456789abcdef", + }, + }, + now, + }); + expect(result).toEqual({ eligible: true, reasonCode: "ok" }); + }); }); diff --git a/src/agents/auth-profiles/credential-state.ts b/src/agents/auth-profiles/credential-state.ts index 4aeb8c7e6ed..e132b73826f 100644 --- a/src/agents/auth-profiles/credential-state.ts +++ b/src/agents/auth-profiles/credential-state.ts @@ -1,5 +1,5 @@ import { coerceSecretRef, normalizeSecretInputString } from "../../config/types.secrets.js"; -import type { AuthProfileCredential, OAuthCredential } from "./types.js"; +import type { AuthProfileCredential, OAuthCredential, OAuthCredentialRef } from "./types.js"; export type AuthCredentialReasonCode = | "ok" @@ -69,6 +69,15 @@ function hasConfiguredSecretString(value: unknown): boolean { return normalizeSecretInputString(value) !== undefined; } +function hasConfiguredOAuthRef(value: OAuthCredentialRef | undefined): boolean { + return ( + value?.source === "openclaw-credentials" && + value.provider === "openai-codex" && + typeof value.id === "string" && + /^[a-f0-9]{32}$/.test(value.id) + ); +} + export function evaluateStoredCredentialEligibility(params: { credential: AuthProfileCredential; now?: number; @@ -104,7 +113,8 @@ export function evaluateStoredCredentialEligibility(params: { if ( normalizeSecretInputString(credential.access) === undefined && - normalizeSecretInputString(credential.refresh) === undefined + normalizeSecretInputString(credential.refresh) === undefined && + !hasConfiguredOAuthRef(credential.oauthRef) ) { return { eligible: false, reasonCode: "missing_credential" }; } diff --git a/src/agents/auth-profiles/order.test.ts b/src/agents/auth-profiles/order.test.ts index 0e8b2d144d2..90e838982fb 100644 --- a/src/agents/auth-profiles/order.test.ts +++ b/src/agents/auth-profiles/order.test.ts @@ -276,6 +276,33 @@ describe("resolveAuthProfileOrder", () => { expect(order).toEqual(["openai-codex:personal", "openai:backup"]); }); + it("lets Codex auth discover oauthRef-backed OAuth profiles", async () => { + const store: AuthProfileStore = { + version: 1, + profiles: { + "openai-codex:personal": { + type: "oauth", + provider: "openai-codex", + access: "", + refresh: "", + expires: Date.now() + 60_000, + oauthRef: { + source: "openclaw-credentials", + provider: "openai-codex", + id: "0123456789abcdef0123456789abcdef", + }, + }, + }, + }; + + const order = resolveAuthProfileOrder({ + store, + provider: "openai-codex", + }); + + expect(order).toEqual(["openai-codex:personal"]); + }); + it("preserves native Codex profiles before OpenAI alias API-key order", async () => { const store: AuthProfileStore = { version: 1,