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 { resolveEnvApiKey } from "../agents/model-auth.js";
import { upsertSharedEnvVar } from "../infra/env-file.js";
import { import {
formatApiKeyPreview, formatApiKeyPreview,
normalizeApiKeyInput, normalizeApiKeyInput,
@@ -9,7 +8,7 @@ import { createAuthChoiceAgentModelNoter } from "./auth-choice.apply-helpers.js"
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js"; import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
import { isRemoteEnvironment } from "./oauth-env.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 { openUrl } from "./onboard-helpers.js";
import { import {
applyOpenAICodexModelDefault, applyOpenAICodexModelDefault,
@@ -58,17 +57,12 @@ export async function applyAuthChoiceOpenAI(
initialValue: true, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
const result = upsertSharedEnvVar({ await setOpenaiApiKey(envKey.apiKey, params.agentDir);
key: "OPENAI_API_KEY", nextConfig = applyAuthProfileConfig(nextConfig, {
value: envKey.apiKey, 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(); return await applyOpenAiDefaultModelChoice();
} }
} }
@@ -84,15 +78,12 @@ export async function applyAuthChoiceOpenAI(
} }
const trimmed = normalizeApiKeyInput(String(key)); const trimmed = normalizeApiKeyInput(String(key));
const result = upsertSharedEnvVar({ await setOpenaiApiKey(trimmed, params.agentDir);
key: "OPENAI_API_KEY", nextConfig = applyAuthProfileConfig(nextConfig, {
value: trimmed, 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(); return await applyOpenAiDefaultModelChoice();
} }

View File

@@ -1,5 +1,9 @@
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
import { setCloudflareAiGatewayConfig, setMoonshotApiKey } from "./onboard-auth.js"; import {
setCloudflareAiGatewayConfig,
setMoonshotApiKey,
setOpenaiApiKey,
} from "./onboard-auth.js";
import { import {
createAuthTestLifecycle, createAuthTestLifecycle,
readAuthProfilesForAgent, readAuthProfilesForAgent,
@@ -12,6 +16,7 @@ describe("onboard auth credentials secret refs", () => {
"OPENCLAW_AGENT_DIR", "OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR", "PI_CODING_AGENT_DIR",
"MOONSHOT_API_KEY", "MOONSHOT_API_KEY",
"OPENAI_API_KEY",
"CLOUDFLARE_AI_GATEWAY_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(); 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( export async function setOpenaiApiKey(
key: SecretInput, key: SecretInput,
agentDir?: string, agentDir?: string,

View File

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

View File

@@ -42,6 +42,7 @@ import {
setMistralApiKey, setMistralApiKey,
setMinimaxApiKey, setMinimaxApiKey,
setMoonshotApiKey, setMoonshotApiKey,
setOpenaiApiKey,
setOpencodeZenApiKey, setOpencodeZenApiKey,
setOpenrouterApiKey, setOpenrouterApiKey,
setSyntheticApiKey, setSyntheticApiKey,
@@ -408,15 +409,16 @@ export async function applyNonInteractiveAuthChoice(params: {
flagName: "--openai-api-key", flagName: "--openai-api-key",
envVar: "OPENAI_API_KEY", envVar: "OPENAI_API_KEY",
runtime, runtime,
allowProfile: false,
}); });
if (!resolved) { if (!resolved) {
return null; return null;
} }
const key = resolved.key; await setOpenaiApiKey(resolved.key);
const result = upsertSharedEnvVar({ key: "OPENAI_API_KEY", value: key }); nextConfig = applyAuthProfileConfig(nextConfig, {
process.env.OPENAI_API_KEY = key; profileId: "openai:default",
runtime.log(`Saved OPENAI_API_KEY to ${shortenHomePath(result.path)}`); provider: "openai",
mode: "api_key",
});
return applyOpenAIConfig(nextConfig); return applyOpenAIConfig(nextConfig);
} }