From 68b9d89ee7446f0ea5bbf20a6b1b079d30d9866c Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:55:56 -0800 Subject: [PATCH] Onboard: store OpenAI auth in profiles instead of .env --- src/commands/auth-choice.apply.openai.test.ts | 89 +++++++++++++++++++ src/commands/auth-choice.apply.openai.ts | 31 +++---- src/commands/onboard-auth.credentials.test.ts | 23 ++++- src/commands/onboard-auth.credentials.ts | 1 + src/commands/onboard-auth.ts | 1 + .../local/auth-choice.ts | 12 +-- 6 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 src/commands/auth-choice.apply.openai.test.ts diff --git a/src/commands/auth-choice.apply.openai.test.ts b/src/commands/auth-choice.apply.openai.test.ts new file mode 100644 index 000000000..a53f4986f --- /dev/null +++ b/src/commands/auth-choice.apply.openai.test.ts @@ -0,0 +1,89 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js"; +import { + createAuthTestLifecycle, + createExitThrowingRuntime, + createWizardPrompter, + readAuthProfilesForAgent, + setupAuthTestEnv, +} from "./test-wizard-helpers.js"; + +describe("applyAuthChoiceOpenAI", () => { + const lifecycle = createAuthTestLifecycle([ + "OPENCLAW_STATE_DIR", + "OPENCLAW_AGENT_DIR", + "PI_CODING_AGENT_DIR", + "OPENAI_API_KEY", + ]); + + async function setupTempState() { + const env = await setupAuthTestEnv("openclaw-openai-"); + lifecycle.setStateDir(env.stateDir); + return env.agentDir; + } + + afterEach(async () => { + await lifecycle.cleanup(); + }); + + it("writes env-backed OpenAI key as keyRef in auth profiles", async () => { + const agentDir = await setupTempState(); + process.env.OPENAI_API_KEY = "sk-openai-env"; + + const confirm = vi.fn(async () => true); + const text = vi.fn(async () => "unused"); + const prompter = createWizardPrompter({ confirm, text }, { defaultSelect: "" }); + const runtime = createExitThrowingRuntime(); + + const result = await applyAuthChoiceOpenAI({ + authChoice: "openai-api-key", + config: {}, + prompter, + runtime, + setDefaultModel: true, + }); + + expect(result).not.toBeNull(); + expect(result?.config.auth?.profiles?.["openai:default"]).toMatchObject({ + provider: "openai", + mode: "api_key", + }); + expect(result?.config.agents?.defaults?.model?.primary).toBe("openai/gpt-5.1-codex"); + expect(text).not.toHaveBeenCalled(); + + const parsed = await readAuthProfilesForAgent<{ + profiles?: Record; + }>(agentDir); + expect(parsed.profiles?.["openai:default"]).toMatchObject({ + keyRef: { source: "env", id: "OPENAI_API_KEY" }, + }); + expect(parsed.profiles?.["openai:default"]?.key).toBeUndefined(); + }); + + it("writes explicit token input into openai auth profile", async () => { + const agentDir = await setupTempState(); + + const prompter = createWizardPrompter({}, { defaultSelect: "" }); + const runtime = createExitThrowingRuntime(); + + const result = await applyAuthChoiceOpenAI({ + authChoice: "apiKey", + config: {}, + prompter, + runtime, + setDefaultModel: true, + opts: { + tokenProvider: "openai", + token: "sk-openai-token", + }, + }); + + expect(result).not.toBeNull(); + + const parsed = await readAuthProfilesForAgent<{ + profiles?: Record; + }>(agentDir); + expect(parsed.profiles?.["openai:default"]?.key).toBe("sk-openai-token"); + expect(parsed.profiles?.["openai:default"]?.keyRef).toBeUndefined(); + }); +}); diff --git a/src/commands/auth-choice.apply.openai.ts b/src/commands/auth-choice.apply.openai.ts index 2d1beaf04..cf616b312 100644 --- a/src/commands/auth-choice.apply.openai.ts +++ b/src/commands/auth-choice.apply.openai.ts @@ -1,5 +1,4 @@ import { resolveEnvApiKey } from "../agents/model-auth.js"; -import { upsertSharedEnvVar } from "../infra/env-file.js"; import { formatApiKeyPreview, normalizeApiKeyInput, @@ -9,7 +8,7 @@ import { createAuthChoiceAgentModelNoter } from "./auth-choice.apply-helpers.js" import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import { applyDefaultModelChoice } from "./auth-choice.default-model.js"; import { isRemoteEnvironment } from "./oauth-env.js"; -import { applyAuthProfileConfig, writeOAuthCredentials } from "./onboard-auth.js"; +import { applyAuthProfileConfig, setOpenaiApiKey, writeOAuthCredentials } from "./onboard-auth.js"; import { openUrl } from "./onboard-helpers.js"; import { applyOpenAICodexModelDefault, @@ -58,17 +57,12 @@ export async function applyAuthChoiceOpenAI( initialValue: true, }); if (useExisting) { - const result = upsertSharedEnvVar({ - key: "OPENAI_API_KEY", - value: envKey.apiKey, + await setOpenaiApiKey(envKey.apiKey, params.agentDir); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "openai:default", + provider: "openai", + mode: "api_key", }); - if (!process.env.OPENAI_API_KEY) { - process.env.OPENAI_API_KEY = envKey.apiKey; - } - await params.prompter.note( - `Copied OPENAI_API_KEY to ${result.path} for launchd compatibility.`, - "OpenAI API key", - ); return await applyOpenAiDefaultModelChoice(); } } @@ -84,15 +78,12 @@ export async function applyAuthChoiceOpenAI( } const trimmed = normalizeApiKeyInput(String(key)); - const result = upsertSharedEnvVar({ - key: "OPENAI_API_KEY", - value: trimmed, + await setOpenaiApiKey(trimmed, params.agentDir); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "openai:default", + provider: "openai", + mode: "api_key", }); - process.env.OPENAI_API_KEY = trimmed; - await params.prompter.note( - `Saved OPENAI_API_KEY to ${result.path} for launchd compatibility.`, - "OpenAI API key", - ); return await applyOpenAiDefaultModelChoice(); } diff --git a/src/commands/onboard-auth.credentials.test.ts b/src/commands/onboard-auth.credentials.test.ts index 902a44bf5..5825409f8 100644 --- a/src/commands/onboard-auth.credentials.test.ts +++ b/src/commands/onboard-auth.credentials.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { setCloudflareAiGatewayConfig, setMoonshotApiKey } from "./onboard-auth.js"; +import { + setCloudflareAiGatewayConfig, + setMoonshotApiKey, + setOpenaiApiKey, +} from "./onboard-auth.js"; import { createAuthTestLifecycle, readAuthProfilesForAgent, @@ -12,6 +16,7 @@ describe("onboard auth credentials secret refs", () => { "OPENCLAW_AGENT_DIR", "PI_CODING_AGENT_DIR", "MOONSHOT_API_KEY", + "OPENAI_API_KEY", "CLOUDFLARE_AI_GATEWAY_API_KEY", ]); @@ -100,4 +105,20 @@ describe("onboard auth credentials secret refs", () => { }); expect(parsed.profiles?.["cloudflare-ai-gateway:default"]?.key).toBeUndefined(); }); + + it("stores env-backed openai key as keyRef", async () => { + const env = await setupAuthTestEnv("openclaw-onboard-auth-credentials-openai-"); + lifecycle.setStateDir(env.stateDir); + process.env.OPENAI_API_KEY = "sk-openai-env"; + + await setOpenaiApiKey("sk-openai-env"); + + const parsed = await readAuthProfilesForAgent<{ + profiles?: Record; + }>(env.agentDir); + expect(parsed.profiles?.["openai:default"]).toMatchObject({ + keyRef: { source: "env", id: "OPENAI_API_KEY" }, + }); + expect(parsed.profiles?.["openai:default"]?.key).toBeUndefined(); + }); }); diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 8e512cd21..944816997 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -206,6 +206,7 @@ export async function setAnthropicApiKey( }); } +<<<<<<< HEAD export async function setOpenaiApiKey( key: SecretInput, agentDir?: string, diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index de506df0b..fdc4cc8a1 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -61,6 +61,7 @@ export { KILOCODE_DEFAULT_MODEL_REF, LITELLM_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, + setOpenaiApiKey, setAnthropicApiKey, setCloudflareAiGatewayConfig, setQianfanApiKey, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 9f9ce49a5..47e8b347d 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -42,6 +42,7 @@ import { setMistralApiKey, setMinimaxApiKey, setMoonshotApiKey, + setOpenaiApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, @@ -408,15 +409,16 @@ export async function applyNonInteractiveAuthChoice(params: { flagName: "--openai-api-key", envVar: "OPENAI_API_KEY", runtime, - allowProfile: false, }); if (!resolved) { return null; } - const key = resolved.key; - const result = upsertSharedEnvVar({ key: "OPENAI_API_KEY", value: key }); - process.env.OPENAI_API_KEY = key; - runtime.log(`Saved OPENAI_API_KEY to ${shortenHomePath(result.path)}`); + await setOpenaiApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "openai:default", + provider: "openai", + mode: "api_key", + }); return applyOpenAIConfig(nextConfig); }