mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:00:44 +00:00
fix(codex): auto-clear api key for subscription auth
This commit is contained in:
committed by
Peter Steinberger
parent
aeb007e4e5
commit
20ff49f7c8
@@ -8,6 +8,7 @@ import {
|
||||
bridgeCodexAppServerStartOptions,
|
||||
refreshCodexAppServerAuthTokens,
|
||||
} from "./auth-bridge.js";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
|
||||
const oauthMocks = vi.hoisted(() => ({
|
||||
refreshOpenAICodexToken: vi.fn(),
|
||||
@@ -96,25 +97,54 @@ afterEach(() => {
|
||||
providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin.mockClear();
|
||||
});
|
||||
|
||||
function createStartOptions(
|
||||
overrides: Partial<CodexAppServerStartOptions> = {},
|
||||
): CodexAppServerStartOptions {
|
||||
return {
|
||||
transport: "stdio",
|
||||
command: "codex",
|
||||
args: ["app-server"],
|
||||
headers: { authorization: "Bearer dev-token" },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeCodexCliAuthFile(codexHome: string): Promise<void> {
|
||||
await fs.mkdir(codexHome, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(codexHome, "auth.json"),
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: "cli-access-token",
|
||||
refresh_token: "cli-refresh-token",
|
||||
account_id: "cli-account-123",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe("bridgeCodexAppServerStartOptions", () => {
|
||||
it("leaves Codex app-server start options unchanged", async () => {
|
||||
it("clears an inherited OpenAI API key when local Codex CLI OAuth is available", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const startOptions = {
|
||||
transport: "stdio" as const,
|
||||
command: "codex",
|
||||
args: ["app-server"],
|
||||
headers: { authorization: "Bearer dev-token" },
|
||||
const codexHome = path.join(agentDir, "codex-home");
|
||||
const startOptions = createStartOptions({
|
||||
env: { CODEX_HOME: "/tmp/source-codex-home", EXISTING: "1" },
|
||||
clearEnv: ["FOO"],
|
||||
};
|
||||
});
|
||||
vi.stubEnv("CODEX_HOME", codexHome);
|
||||
try {
|
||||
await writeCodexCliAuthFile(codexHome);
|
||||
|
||||
await expect(
|
||||
bridgeCodexAppServerStartOptions({
|
||||
startOptions,
|
||||
agentDir,
|
||||
authProfileId: "openai-codex:default",
|
||||
}),
|
||||
).resolves.toBe(startOptions);
|
||||
).resolves.toEqual({
|
||||
...startOptions,
|
||||
clearEnv: ["FOO", "OPENAI_API_KEY"],
|
||||
});
|
||||
expect(startOptions.clearEnv).toEqual(["FOO"]);
|
||||
await expect(fs.access(path.join(agentDir, "harness-auth"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
@@ -123,6 +153,126 @@ describe("bridgeCodexAppServerStartOptions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("clears an inherited OpenAI API key for an explicit Codex OAuth profile", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const startOptions = createStartOptions({ clearEnv: ["FOO"] });
|
||||
try {
|
||||
upsertAuthProfile({
|
||||
agentDir,
|
||||
profileId: "openai-codex:work",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 24 * 60 * 60_000,
|
||||
accountId: "account-123",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
bridgeCodexAppServerStartOptions({
|
||||
startOptions,
|
||||
agentDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
...startOptions,
|
||||
clearEnv: ["FOO", "OPENAI_API_KEY"],
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("clears an inherited OpenAI API key for an explicit Codex token profile", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const startOptions = createStartOptions({ clearEnv: ["FOO"] });
|
||||
try {
|
||||
upsertAuthProfile({
|
||||
agentDir,
|
||||
profileId: "openai-codex:work",
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "openai-codex",
|
||||
token: "access-token",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
bridgeCodexAppServerStartOptions({
|
||||
startOptions,
|
||||
agentDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
...startOptions,
|
||||
clearEnv: ["FOO", "OPENAI_API_KEY"],
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an inherited OpenAI API key for an explicit Codex api-key profile", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const startOptions = createStartOptions({ clearEnv: ["FOO"] });
|
||||
try {
|
||||
upsertAuthProfile({
|
||||
agentDir,
|
||||
profileId: "openai-codex:work",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "openai-codex",
|
||||
key: "explicit-api-key",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
bridgeCodexAppServerStartOptions({
|
||||
startOptions,
|
||||
agentDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
).resolves.toBe(startOptions);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not clear process environment for websocket app-server connections", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const startOptions = createStartOptions({
|
||||
transport: "websocket",
|
||||
url: "ws://127.0.0.1:1455",
|
||||
clearEnv: ["FOO"],
|
||||
});
|
||||
try {
|
||||
upsertAuthProfile({
|
||||
agentDir,
|
||||
profileId: "openai-codex:work",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 24 * 60 * 60_000,
|
||||
accountId: "account-123",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
bridgeCodexAppServerStartOptions({
|
||||
startOptions,
|
||||
agentDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
).resolves.toBe(startOptions);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("applies an OpenAI Codex OAuth profile through app-server login", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
||||
|
||||
@@ -13,15 +13,25 @@ import type { ChatgptAuthTokensRefreshResponse } from "./protocol-generated/type
|
||||
import type { LoginAccountParams } from "./protocol-generated/typescript/v2/LoginAccountParams.js";
|
||||
|
||||
const CODEX_APP_SERVER_AUTH_PROVIDER = "openai-codex";
|
||||
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
|
||||
const OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY";
|
||||
|
||||
export async function bridgeCodexAppServerStartOptions(params: {
|
||||
startOptions: CodexAppServerStartOptions;
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
}): Promise<CodexAppServerStartOptions> {
|
||||
void params.agentDir;
|
||||
void params.authProfileId;
|
||||
return params.startOptions;
|
||||
if (params.startOptions.transport !== "stdio") {
|
||||
return params.startOptions;
|
||||
}
|
||||
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
|
||||
const shouldClearInheritedOpenAiApiKey = shouldClearOpenAiApiKeyForCodexAuthProfile({
|
||||
store,
|
||||
authProfileId: params.authProfileId,
|
||||
});
|
||||
return shouldClearInheritedOpenAiApiKey
|
||||
? withClearedEnvironmentVariable(params.startOptions, OPENAI_API_KEY_ENV_VAR)
|
||||
: params.startOptions;
|
||||
}
|
||||
|
||||
export async function applyCodexAppServerAuthProfile(params: {
|
||||
@@ -161,6 +171,38 @@ function isCodexAppServerAuthProvider(provider: string): boolean {
|
||||
return resolveProviderIdForAuth(provider) === CODEX_APP_SERVER_AUTH_PROVIDER;
|
||||
}
|
||||
|
||||
function shouldClearOpenAiApiKeyForCodexAuthProfile(params: {
|
||||
store: ReturnType<typeof ensureAuthProfileStore>;
|
||||
authProfileId?: string;
|
||||
}): boolean {
|
||||
const profileId = params.authProfileId?.trim();
|
||||
const credential = profileId
|
||||
? params.store.profiles[profileId]
|
||||
: params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID];
|
||||
return isCodexSubscriptionCredential(credential);
|
||||
}
|
||||
|
||||
function isCodexSubscriptionCredential(credential: AuthProfileCredential | undefined): boolean {
|
||||
if (!credential || !isCodexAppServerAuthProvider(credential.provider)) {
|
||||
return false;
|
||||
}
|
||||
return credential.type === "oauth" || credential.type === "token";
|
||||
}
|
||||
|
||||
function withClearedEnvironmentVariable(
|
||||
startOptions: CodexAppServerStartOptions,
|
||||
envVar: string,
|
||||
): CodexAppServerStartOptions {
|
||||
const clearEnv = startOptions.clearEnv ?? [];
|
||||
if (clearEnv.includes(envVar)) {
|
||||
return startOptions;
|
||||
}
|
||||
return {
|
||||
...startOptions,
|
||||
clearEnv: [...clearEnv, envVar],
|
||||
};
|
||||
}
|
||||
|
||||
function buildChatgptAuthTokensParams(
|
||||
profileId: string,
|
||||
credential: AuthProfileCredential,
|
||||
|
||||
Reference in New Issue
Block a user