Fix Codex CLI auth profile sync (#45353)

Merged via squash.

Prepared head SHA: e5432ec4e1
Co-authored-by: Gugu-sugar <201366873+Gugu-sugar@users.noreply.github.com>
Co-authored-by: grp06 <1573959+grp06@users.noreply.github.com>
Reviewed-by: @grp06
This commit is contained in:
Gugu-sugar 2026-03-15 07:36:09 +08:00 committed by GitHub
parent b202ac2ad1
commit c1a0196826
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 88 additions and 4 deletions

View File

@ -100,6 +100,7 @@ Docs: https://docs.openclaw.ai
- Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement when `gateway.auth.mode=none` so Control UI connections behind reverse proxies no longer get stuck on `pairing required` (code 1008) despite auth being explicitly disabled. (#42931) - Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement when `gateway.auth.mode=none` so Control UI connections behind reverse proxies no longer get stuck on `pairing required` (code 1008) despite auth being explicitly disabled. (#42931)
- Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057) - Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057)
- Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy. - Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy.
- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar.
## 2026.3.12 ## 2026.3.12

View File

@ -0,0 +1,54 @@
import { describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "./auth-profiles/types.js";
const mocks = vi.hoisted(() => ({
readCodexCliCredentialsCached: vi.fn(),
readQwenCliCredentialsCached: vi.fn(() => null),
readMiniMaxCliCredentialsCached: vi.fn(() => null),
}));
vi.mock("./cli-credentials.js", () => ({
readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached,
readQwenCliCredentialsCached: mocks.readQwenCliCredentialsCached,
readMiniMaxCliCredentialsCached: mocks.readMiniMaxCliCredentialsCached,
}));
const { syncExternalCliCredentials } = await import("./auth-profiles/external-cli-sync.js");
const { CODEX_CLI_PROFILE_ID } = await import("./auth-profiles/constants.js");
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
describe("syncExternalCliCredentials", () => {
it("syncs Codex CLI credentials into the supported default auth profile", () => {
const expires = Date.now() + 60_000;
mocks.readCodexCliCredentialsCached.mockReturnValue({
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires,
accountId: "acct_123",
});
const store: AuthProfileStore = {
version: 1,
profiles: {},
};
const mutated = syncExternalCliCredentials(store);
expect(mutated).toBe(true);
expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith(
expect.objectContaining({ ttlMs: expect.any(Number) }),
);
expect(store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]).toMatchObject({
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires,
accountId: "acct_123",
});
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined();
});
});

View File

@ -1,4 +1,5 @@
import { import {
readCodexCliCredentialsCached,
readQwenCliCredentialsCached, readQwenCliCredentialsCached,
readMiniMaxCliCredentialsCached, readMiniMaxCliCredentialsCached,
} from "../cli-credentials.js"; } from "../cli-credentials.js";
@ -11,6 +12,8 @@ import {
} from "./constants.js"; } from "./constants.js";
import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js"; import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js";
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean { function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
if (!a) { if (!a) {
return false; return false;
@ -37,7 +40,11 @@ function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: nu
if (cred.type !== "oauth" && cred.type !== "token") { if (cred.type !== "oauth" && cred.type !== "token") {
return false; return false;
} }
if (cred.provider !== "qwen-portal" && cred.provider !== "minimax-portal") { if (
cred.provider !== "qwen-portal" &&
cred.provider !== "minimax-portal" &&
cred.provider !== "openai-codex"
) {
return false; return false;
} }
if (typeof cred.expires !== "number") { if (typeof cred.expires !== "number") {
@ -82,7 +89,8 @@ function syncExternalCliCredentialsForProvider(
} }
/** /**
* Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI) into the store. * Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI, Codex CLI)
* into the store.
* *
* Returns true if any credentials were updated. * Returns true if any credentials were updated.
*/ */
@ -130,6 +138,17 @@ export function syncExternalCliCredentials(store: AuthProfileStore): boolean {
) { ) {
mutated = true; mutated = true;
} }
if (
syncExternalCliCredentialsForProvider(
store,
OPENAI_CODEX_DEFAULT_PROFILE_ID,
"openai-codex",
() => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
now,
)
) {
mutated = true;
}
return mutated; return mutated;
} }

View File

@ -63,6 +63,13 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => {
refresh: "token-r2", refresh: "token-r2",
expires: Date.now() + 60_000, expires: Date.now() + 60_000,
}, },
"openai-codex:default": {
type: "oauth",
provider: "openai-codex",
access: "token-c",
refresh: "token-r3",
expires: Date.now() + 60_000,
},
}, },
}, },
null, null,
@ -76,10 +83,11 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => {
profiles: { profiles: {
"anthropic:claude-cli": { provider: "anthropic", mode: "oauth" }, "anthropic:claude-cli": { provider: "anthropic", mode: "oauth" },
"openai-codex:codex-cli": { provider: "openai-codex", mode: "oauth" }, "openai-codex:codex-cli": { provider: "openai-codex", mode: "oauth" },
"openai-codex:default": { provider: "openai-codex", mode: "oauth" },
}, },
order: { order: {
anthropic: ["anthropic:claude-cli"], anthropic: ["anthropic:claude-cli"],
"openai-codex": ["openai-codex:codex-cli"], "openai-codex": ["openai-codex:codex-cli", "openai-codex:default"],
}, },
}, },
} as const; } as const;
@ -94,10 +102,12 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => {
}; };
expect(raw.profiles?.["anthropic:claude-cli"]).toBeUndefined(); expect(raw.profiles?.["anthropic:claude-cli"]).toBeUndefined();
expect(raw.profiles?.["openai-codex:codex-cli"]).toBeUndefined(); expect(raw.profiles?.["openai-codex:codex-cli"]).toBeUndefined();
expect(raw.profiles?.["openai-codex:default"]).toBeDefined();
expect(next.auth?.profiles?.["anthropic:claude-cli"]).toBeUndefined(); expect(next.auth?.profiles?.["anthropic:claude-cli"]).toBeUndefined();
expect(next.auth?.profiles?.["openai-codex:codex-cli"]).toBeUndefined(); expect(next.auth?.profiles?.["openai-codex:codex-cli"]).toBeUndefined();
expect(next.auth?.profiles?.["openai-codex:default"]).toBeDefined();
expect(next.auth?.order?.anthropic).toBeUndefined(); expect(next.auth?.order?.anthropic).toBeUndefined();
expect(next.auth?.order?.["openai-codex"]).toBeUndefined(); expect(next.auth?.order?.["openai-codex"]).toEqual(["openai-codex:default"]);
}); });
}); });