fix: keep codex oauth bridge extension-owned (#68284) (thanks @vincentkoc)

This commit is contained in:
Peter Steinberger
2026-04-18 20:56:38 +01:00
parent f1cc8f0cfc
commit 2fc429dfbf
27 changed files with 363 additions and 198 deletions

View File

@@ -41,10 +41,12 @@ describe("bridgeCodexAppServerStartOptions", () => {
refresh: "refresh-token",
expires: Date.now() + 60_000,
accountId: "acct-123",
idToken: "id-token",
},
},
},
agentDir,
{ filterExternalAuthProfiles: false },
);
const result = await bridgeCodexAppServerStartOptions({
@@ -73,6 +75,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
expect(authFile).toEqual({
auth_mode: "chatgpt",
tokens: {
id_token: "id-token",
access_token: "access-token",
refresh_token: "refresh-token",
account_id: "acct-123",
@@ -93,7 +96,9 @@ describe("bridgeCodexAppServerStartOptions", () => {
args: ["app-server"],
headers: { authorization: "Bearer dev-token" },
};
saveAuthProfileStore({ version: 1, profiles: {} }, agentDir);
saveAuthProfileStore({ version: 1, profiles: {} }, agentDir, {
filterExternalAuthProfiles: false,
});
await expect(
bridgeCodexAppServerStartOptions({
@@ -121,6 +126,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
},
},
agentDir,
{ filterExternalAuthProfiles: false },
);
const codexHome = resolveHashedCodexHome(agentDir, "openai-codex:default");

View File

@@ -1,7 +1,54 @@
import { prepareCodexAuthBridgeFromProfile } from "openclaw/plugin-sdk/codex-auth-bridge-runtime";
import crypto from "node:crypto";
import path from "node:path";
import {
ensureAuthProfileStoreForLocalUpdate,
type OAuthCredential,
} from "openclaw/plugin-sdk/provider-auth";
import { writePrivateSecretFileAtomic } from "openclaw/plugin-sdk/secret-file-runtime";
import type { CodexAppServerStartOptions } from "./config.js";
const DEFAULT_CODEX_AUTH_PROFILE_ID = "openai-codex:default";
const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const;
function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential {
return Boolean(
value &&
typeof value === "object" &&
value !== null &&
"type" in value &&
"provider" in value &&
"access" in value &&
"refresh" in value &&
value.type === "oauth" &&
value.provider === OPENAI_CODEX_PROVIDER_ID &&
typeof value.access === "string" &&
value.access.trim().length > 0 &&
typeof value.refresh === "string" &&
value.refresh.trim().length > 0,
);
}
function resolveCodexBridgeHome(agentDir: string, profileId: string): string {
const digest = crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16);
return path.join(agentDir, "harness-auth", "codex", digest);
}
function buildCodexAuthFile(credential: OAuthCredential): string {
return `${JSON.stringify(
{
auth_mode: "chatgpt",
tokens: {
...(credential.idToken ? { id_token: credential.idToken } : {}),
access_token: credential.access,
refresh_token: credential.refresh,
...(credential.accountId ? { account_id: credential.accountId } : {}),
},
},
null,
2,
)}\n`;
}
export async function bridgeCodexAppServerStartOptions(params: {
startOptions: CodexAppServerStartOptions;
@@ -9,21 +56,27 @@ export async function bridgeCodexAppServerStartOptions(params: {
authProfileId?: string;
}): Promise<CodexAppServerStartOptions> {
const profileId = params.authProfileId?.trim() || DEFAULT_CODEX_AUTH_PROFILE_ID;
const bridge = await prepareCodexAuthBridgeFromProfile({
agentDir: params.agentDir,
authProfileId: profileId,
bridgeRoot: "harness-auth",
});
if (!bridge) {
const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir);
const credential = store.profiles[profileId];
if (!isCodexBridgeableOAuthCredential(credential)) {
return params.startOptions;
}
const codexHome = resolveCodexBridgeHome(params.agentDir, profileId);
await writePrivateSecretFileAtomic({
rootDir: params.agentDir,
filePath: path.join(codexHome, "auth.json"),
content: buildCodexAuthFile(credential),
});
return {
...params.startOptions,
env: {
...params.startOptions.env,
CODEX_HOME: bridge.codexHome,
CODEX_HOME: codexHome,
},
clearEnv: Array.from(new Set([...(params.startOptions.clearEnv ?? []), ...bridge.clearEnv])),
clearEnv: Array.from(
new Set([...(params.startOptions.clearEnv ?? []), ...CODEX_AUTH_ENV_CLEAR_KEYS]),
),
};
}

View File

@@ -532,7 +532,7 @@ describe("runCodexAppServerAttempt", () => {
const binding = await startOrResumeThread({
client: {
request: async (method) => {
request: async (method: string) => {
if (method === "thread/resume") {
return { thread: { id: "thread-existing" }, modelProvider: "openai" };
}