Onboard: store OpenAI auth in profiles instead of .env

This commit is contained in:
joshavant
2026-02-21 14:55:56 -08:00
committed by Peter Steinberger
parent 09c7cb5d34
commit 68b9d89ee7
6 changed files with 131 additions and 26 deletions

View File

@@ -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<string, { key?: string; keyRef?: unknown }>;
}>(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<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["openai:default"]?.key).toBe("sk-openai-token");
expect(parsed.profiles?.["openai:default"]?.keyRef).toBeUndefined();
});
});

View File

@@ -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();
}

View File

@@ -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<string, { key?: string; keyRef?: unknown }>;
}>(env.agentDir);
expect(parsed.profiles?.["openai:default"]).toMatchObject({
keyRef: { source: "env", id: "OPENAI_API_KEY" },
});
expect(parsed.profiles?.["openai:default"]?.key).toBeUndefined();
});
});

View File

@@ -206,6 +206,7 @@ export async function setAnthropicApiKey(
});
}
<<<<<<< HEAD
export async function setOpenaiApiKey(
key: SecretInput,
agentDir?: string,

View File

@@ -61,6 +61,7 @@ export {
KILOCODE_DEFAULT_MODEL_REF,
LITELLM_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
setOpenaiApiKey,
setAnthropicApiKey,
setCloudflareAiGatewayConfig,
setQianfanApiKey,

View File

@@ -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);
}