diff --git a/src/commands/auth-choice.apply.byteplus.ts b/src/commands/auth-choice.apply.byteplus.ts index de62f6bd0..816f82edf 100644 --- a/src/commands/auth-choice.apply.byteplus.ts +++ b/src/commands/auth-choice.apply.byteplus.ts @@ -1,5 +1,4 @@ import { resolveEnvApiKey } from "../agents/model-auth.js"; -import { upsertSharedEnvVar } from "../infra/env-file.js"; import { formatApiKeyPreview, normalizeApiKeyInput, @@ -7,6 +6,7 @@ import { } from "./auth-choice.api-key.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import { applyPrimaryModel } from "./model-picker.js"; +import { applyAuthProfileConfig, setByteplusApiKey } from "./onboard-auth.js"; /** Default model for BytePlus auth onboarding. */ export const BYTEPLUS_DEFAULT_MODEL = "byteplus-plan/ark-code-latest"; @@ -25,18 +25,13 @@ export async function applyAuthChoiceBytePlus( initialValue: true, }); if (useExisting) { - const result = upsertSharedEnvVar({ - key: "BYTEPLUS_API_KEY", - value: envKey.apiKey, + await setByteplusApiKey(envKey.apiKey, params.agentDir); + const configWithAuth = applyAuthProfileConfig(params.config, { + profileId: "byteplus:default", + provider: "byteplus", + mode: "api_key", }); - if (!process.env.BYTEPLUS_API_KEY) { - process.env.BYTEPLUS_API_KEY = envKey.apiKey; - } - await params.prompter.note( - `Copied BYTEPLUS_API_KEY to ${result.path} for launchd compatibility.`, - "BytePlus API key", - ); - const configWithModel = applyPrimaryModel(params.config, BYTEPLUS_DEFAULT_MODEL); + const configWithModel = applyPrimaryModel(configWithAuth, BYTEPLUS_DEFAULT_MODEL); return { config: configWithModel, agentModelOverride: BYTEPLUS_DEFAULT_MODEL, @@ -55,17 +50,13 @@ export async function applyAuthChoiceBytePlus( } const trimmed = normalizeApiKeyInput(String(key)); - const result = upsertSharedEnvVar({ - key: "BYTEPLUS_API_KEY", - value: trimmed, + await setByteplusApiKey(trimmed, params.agentDir); + const configWithAuth = applyAuthProfileConfig(params.config, { + profileId: "byteplus:default", + provider: "byteplus", + mode: "api_key", }); - process.env.BYTEPLUS_API_KEY = trimmed; - await params.prompter.note( - `Saved BYTEPLUS_API_KEY to ${result.path} for launchd compatibility.`, - "BytePlus API key", - ); - - const configWithModel = applyPrimaryModel(params.config, BYTEPLUS_DEFAULT_MODEL); + const configWithModel = applyPrimaryModel(configWithAuth, BYTEPLUS_DEFAULT_MODEL); return { config: configWithModel, agentModelOverride: BYTEPLUS_DEFAULT_MODEL, diff --git a/src/commands/auth-choice.apply.volcengine-byteplus.test.ts b/src/commands/auth-choice.apply.volcengine-byteplus.test.ts new file mode 100644 index 000000000..fd61fb74a --- /dev/null +++ b/src/commands/auth-choice.apply.volcengine-byteplus.test.ts @@ -0,0 +1,129 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { applyAuthChoiceBytePlus } from "./auth-choice.apply.byteplus.js"; +import { applyAuthChoiceVolcengine } from "./auth-choice.apply.volcengine.js"; +import { + createAuthTestLifecycle, + createExitThrowingRuntime, + createWizardPrompter, + readAuthProfilesForAgent, + setupAuthTestEnv, +} from "./test-wizard-helpers.js"; + +describe("volcengine/byteplus auth choice", () => { + const lifecycle = createAuthTestLifecycle([ + "OPENCLAW_STATE_DIR", + "OPENCLAW_AGENT_DIR", + "PI_CODING_AGENT_DIR", + "VOLCANO_ENGINE_API_KEY", + "BYTEPLUS_API_KEY", + ]); + + async function setupTempState() { + const env = await setupAuthTestEnv("openclaw-volc-byte-"); + lifecycle.setStateDir(env.stateDir); + return env.agentDir; + } + + afterEach(async () => { + await lifecycle.cleanup(); + }); + + it("stores volcengine env key as keyRef and configures auth profile", async () => { + const agentDir = await setupTempState(); + process.env.VOLCANO_ENGINE_API_KEY = "volc-env-key"; + + const prompter = createWizardPrompter( + { + confirm: vi.fn(async () => true), + text: vi.fn(async () => "unused"), + }, + { defaultSelect: "" }, + ); + const runtime = createExitThrowingRuntime(); + + const result = await applyAuthChoiceVolcengine({ + authChoice: "volcengine-api-key", + config: {}, + prompter, + runtime, + setDefaultModel: true, + }); + + expect(result).not.toBeNull(); + expect(result?.config.auth?.profiles?.["volcengine:default"]).toMatchObject({ + provider: "volcengine", + mode: "api_key", + }); + + const parsed = await readAuthProfilesForAgent<{ + profiles?: Record; + }>(agentDir); + expect(parsed.profiles?.["volcengine:default"]).toMatchObject({ + keyRef: { source: "env", id: "VOLCANO_ENGINE_API_KEY" }, + }); + expect(parsed.profiles?.["volcengine:default"]?.key).toBeUndefined(); + }); + + it("stores byteplus env key as keyRef and configures auth profile", async () => { + const agentDir = await setupTempState(); + process.env.BYTEPLUS_API_KEY = "byte-env-key"; + + const prompter = createWizardPrompter( + { + confirm: vi.fn(async () => true), + text: vi.fn(async () => "unused"), + }, + { defaultSelect: "" }, + ); + const runtime = createExitThrowingRuntime(); + + const result = await applyAuthChoiceBytePlus({ + authChoice: "byteplus-api-key", + config: {}, + prompter, + runtime, + setDefaultModel: true, + }); + + expect(result).not.toBeNull(); + expect(result?.config.auth?.profiles?.["byteplus:default"]).toMatchObject({ + provider: "byteplus", + mode: "api_key", + }); + + const parsed = await readAuthProfilesForAgent<{ + profiles?: Record; + }>(agentDir); + expect(parsed.profiles?.["byteplus:default"]).toMatchObject({ + keyRef: { source: "env", id: "BYTEPLUS_API_KEY" }, + }); + expect(parsed.profiles?.["byteplus:default"]?.key).toBeUndefined(); + }); + + it("stores explicit volcengine key when env is not used", async () => { + const agentDir = await setupTempState(); + const prompter = createWizardPrompter( + { + confirm: vi.fn(async () => false), + text: vi.fn(async () => "volc-manual-key"), + }, + { defaultSelect: "" }, + ); + const runtime = createExitThrowingRuntime(); + + const result = await applyAuthChoiceVolcengine({ + authChoice: "volcengine-api-key", + config: {}, + prompter, + runtime, + setDefaultModel: true, + }); + + expect(result).not.toBeNull(); + const parsed = await readAuthProfilesForAgent<{ + profiles?: Record; + }>(agentDir); + expect(parsed.profiles?.["volcengine:default"]?.key).toBe("volc-manual-key"); + expect(parsed.profiles?.["volcengine:default"]?.keyRef).toBeUndefined(); + }); +}); diff --git a/src/commands/auth-choice.apply.volcengine.ts b/src/commands/auth-choice.apply.volcengine.ts index 0616dc177..599f9efdc 100644 --- a/src/commands/auth-choice.apply.volcengine.ts +++ b/src/commands/auth-choice.apply.volcengine.ts @@ -1,5 +1,4 @@ import { resolveEnvApiKey } from "../agents/model-auth.js"; -import { upsertSharedEnvVar } from "../infra/env-file.js"; import { formatApiKeyPreview, normalizeApiKeyInput, @@ -7,6 +6,7 @@ import { } from "./auth-choice.api-key.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import { applyPrimaryModel } from "./model-picker.js"; +import { applyAuthProfileConfig, setVolcengineApiKey } from "./onboard-auth.js"; /** Default model for Volcano Engine auth onboarding. */ export const VOLCENGINE_DEFAULT_MODEL = "volcengine-plan/ark-code-latest"; @@ -25,18 +25,13 @@ export async function applyAuthChoiceVolcengine( initialValue: true, }); if (useExisting) { - const result = upsertSharedEnvVar({ - key: "VOLCANO_ENGINE_API_KEY", - value: envKey.apiKey, + await setVolcengineApiKey(envKey.apiKey, params.agentDir); + const configWithAuth = applyAuthProfileConfig(params.config, { + profileId: "volcengine:default", + provider: "volcengine", + mode: "api_key", }); - if (!process.env.VOLCANO_ENGINE_API_KEY) { - process.env.VOLCANO_ENGINE_API_KEY = envKey.apiKey; - } - await params.prompter.note( - `Copied VOLCANO_ENGINE_API_KEY to ${result.path} for launchd compatibility.`, - "Volcano Engine API Key", - ); - const configWithModel = applyPrimaryModel(params.config, VOLCENGINE_DEFAULT_MODEL); + const configWithModel = applyPrimaryModel(configWithAuth, VOLCENGINE_DEFAULT_MODEL); return { config: configWithModel, agentModelOverride: VOLCENGINE_DEFAULT_MODEL, @@ -55,17 +50,13 @@ export async function applyAuthChoiceVolcengine( } const trimmed = normalizeApiKeyInput(String(key)); - const result = upsertSharedEnvVar({ - key: "VOLCANO_ENGINE_API_KEY", - value: trimmed, + await setVolcengineApiKey(trimmed, params.agentDir); + const configWithAuth = applyAuthProfileConfig(params.config, { + profileId: "volcengine:default", + provider: "volcengine", + mode: "api_key", }); - process.env.VOLCANO_ENGINE_API_KEY = trimmed; - await params.prompter.note( - `Saved VOLCANO_ENGINE_API_KEY to ${result.path} for launchd compatibility.`, - "Volcano Engine API Key", - ); - - const configWithModel = applyPrimaryModel(params.config, VOLCENGINE_DEFAULT_MODEL); + const configWithModel = applyPrimaryModel(configWithAuth, VOLCENGINE_DEFAULT_MODEL); return { config: configWithModel, agentModelOverride: VOLCENGINE_DEFAULT_MODEL, diff --git a/src/commands/onboard-auth.credentials.test.ts b/src/commands/onboard-auth.credentials.test.ts index 340b33a03..16cdea4d6 100644 --- a/src/commands/onboard-auth.credentials.test.ts +++ b/src/commands/onboard-auth.credentials.test.ts @@ -1,8 +1,10 @@ import { afterEach, describe, expect, it } from "vitest"; import { + setByteplusApiKey, setCloudflareAiGatewayConfig, setMoonshotApiKey, setOpenaiApiKey, + setVolcengineApiKey, } from "./onboard-auth.js"; import { createAuthTestLifecycle, @@ -18,6 +20,8 @@ describe("onboard auth credentials secret refs", () => { "MOONSHOT_API_KEY", "OPENAI_API_KEY", "CLOUDFLARE_AI_GATEWAY_API_KEY", + "VOLCANO_ENGINE_API_KEY", + "BYTEPLUS_API_KEY", ]); afterEach(async () => { @@ -137,4 +141,28 @@ describe("onboard auth credentials secret refs", () => { }); expect(parsed.profiles?.["openai:default"]?.key).toBeUndefined(); }); + + it("stores env-backed volcengine and byteplus keys as keyRef", async () => { + const env = await setupAuthTestEnv("openclaw-onboard-auth-credentials-volc-byte-"); + lifecycle.setStateDir(env.stateDir); + process.env.VOLCANO_ENGINE_API_KEY = "volcengine-secret"; + process.env.BYTEPLUS_API_KEY = "byteplus-secret"; + + await setVolcengineApiKey("volcengine-secret"); + await setByteplusApiKey("byteplus-secret"); + + const parsed = await readAuthProfilesForAgent<{ + profiles?: Record; + }>(env.agentDir); + + expect(parsed.profiles?.["volcengine:default"]).toMatchObject({ + keyRef: { source: "env", id: "VOLCANO_ENGINE_API_KEY" }, + }); + expect(parsed.profiles?.["volcengine:default"]?.key).toBeUndefined(); + + expect(parsed.profiles?.["byteplus:default"]).toMatchObject({ + keyRef: { source: "env", id: "BYTEPLUS_API_KEY" }, + }); + expect(parsed.profiles?.["byteplus:default"]?.key).toBeUndefined(); + }); }); diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index fdc4cc8a1..13d2cf75b 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -64,6 +64,7 @@ export { setOpenaiApiKey, setAnthropicApiKey, setCloudflareAiGatewayConfig, + setByteplusApiKey, setQianfanApiKey, setGeminiApiKey, setKilocodeApiKey, @@ -80,6 +81,7 @@ export { setVeniceApiKey, setVercelAiGatewayApiKey, setXiaomiApiKey, + setVolcengineApiKey, setZaiApiKey, setXaiApiKey, writeOAuthCredentials, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 47e8b347d..68399ba28 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -2,9 +2,7 @@ import { upsertAuthProfile } from "../../../agents/auth-profiles.js"; import { normalizeProviderId } from "../../../agents/model-selection.js"; import { parseDurationMs } from "../../../cli/parse-duration.js"; import type { OpenClawConfig } from "../../../config/config.js"; -import { upsertSharedEnvVar } from "../../../infra/env-file.js"; import type { RuntimeEnv } from "../../../runtime.js"; -import { shortenHomePath } from "../../../utils.js"; import { normalizeSecretInput } from "../../../utils/normalize-secret-input.js"; import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js"; import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js"; @@ -34,6 +32,7 @@ import { applyZaiConfig, setAnthropicApiKey, setCloudflareAiGatewayConfig, + setByteplusApiKey, setQianfanApiKey, setGeminiApiKey, setKilocodeApiKey, @@ -46,6 +45,7 @@ import { setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, + setVolcengineApiKey, setXaiApiKey, setVeniceApiKey, setTogetherApiKey, @@ -344,14 +344,12 @@ export async function applyNonInteractiveAuthChoice(params: { if (!resolved) { return null; } - if (resolved.source !== "profile") { - const result = upsertSharedEnvVar({ - key: "VOLCANO_ENGINE_API_KEY", - value: resolved.key, - }); - process.env.VOLCANO_ENGINE_API_KEY = resolved.key; - runtime.log(`Saved VOLCANO_ENGINE_API_KEY to ${shortenHomePath(result.path)}`); - } + await setVolcengineApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "volcengine:default", + provider: "volcengine", + mode: "api_key", + }); return applyPrimaryModel(nextConfig, "volcengine-plan/ark-code-latest"); } @@ -367,14 +365,12 @@ export async function applyNonInteractiveAuthChoice(params: { if (!resolved) { return null; } - if (resolved.source !== "profile") { - const result = upsertSharedEnvVar({ - key: "BYTEPLUS_API_KEY", - value: resolved.key, - }); - process.env.BYTEPLUS_API_KEY = resolved.key; - runtime.log(`Saved BYTEPLUS_API_KEY to ${shortenHomePath(result.path)}`); - } + await setByteplusApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "byteplus:default", + provider: "byteplus", + mode: "api_key", + }); return applyPrimaryModel(nextConfig, "byteplus-plan/ark-code-latest"); }