Onboard: require explicit mode for env secret refs

This commit is contained in:
joshavant
2026-02-24 15:01:53 -06:00
committed by Peter Steinberger
parent 4d94b05ac5
commit cb119874dc
5 changed files with 131 additions and 35 deletions

View File

@@ -4,6 +4,10 @@ import {
normalizeApiKeyInput,
validateApiKeyInput,
} from "./auth-choice.api-key.js";
import {
normalizeSecretInputModeInput,
resolveSecretInputModeForEnvSelection,
} from "./auth-choice.apply-helpers.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyPrimaryModel } from "./model-picker.js";
import { applyAuthProfileConfig, setByteplusApiKey } from "./onboard-auth.js";
@@ -18,6 +22,7 @@ export async function applyAuthChoiceBytePlus(
return null;
}
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
const envKey = resolveEnvApiKey("byteplus");
if (envKey) {
const useExisting = await params.prompter.confirm({
@@ -25,7 +30,11 @@ export async function applyAuthChoiceBytePlus(
initialValue: true,
});
if (useExisting) {
await setByteplusApiKey(envKey.apiKey, params.agentDir);
const mode = await resolveSecretInputModeForEnvSelection({
prompter: params.prompter,
explicitMode: requestedSecretInputMode,
});
await setByteplusApiKey(envKey.apiKey, params.agentDir, { secretInputMode: mode });
const configWithAuth = applyAuthProfileConfig(params.config, {
profileId: "byteplus:default",
provider: "byteplus",
@@ -50,7 +59,9 @@ export async function applyAuthChoiceBytePlus(
}
const trimmed = normalizeApiKeyInput(String(key));
await setByteplusApiKey(trimmed, params.agentDir);
await setByteplusApiKey(trimmed, params.agentDir, {
secretInputMode: requestedSecretInputMode,
});
const configWithAuth = applyAuthProfileConfig(params.config, {
profileId: "byteplus:default",
provider: "byteplus",

View File

@@ -28,7 +28,7 @@ describe("volcengine/byteplus auth choice", () => {
await lifecycle.cleanup();
});
it("stores volcengine env key as keyRef and configures auth profile", async () => {
it("stores volcengine env key as plaintext by default", async () => {
const agentDir = await setupTempState();
process.env.VOLCANO_ENGINE_API_KEY = "volc-env-key";
@@ -37,7 +37,7 @@ describe("volcengine/byteplus auth choice", () => {
confirm: vi.fn(async () => true),
text: vi.fn(async () => "unused"),
},
{ defaultSelect: "" },
{ defaultSelect: "plaintext" },
);
const runtime = createExitThrowingRuntime();
@@ -55,6 +55,35 @@ describe("volcengine/byteplus auth choice", () => {
mode: "api_key",
});
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["volcengine:default"]?.key).toBe("volc-env-key");
expect(parsed.profiles?.["volcengine:default"]?.keyRef).toBeUndefined();
});
it("stores volcengine env key as keyRef in ref mode", 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: "ref" },
);
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<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
@@ -64,7 +93,7 @@ describe("volcengine/byteplus auth choice", () => {
expect(parsed.profiles?.["volcengine:default"]?.key).toBeUndefined();
});
it("stores byteplus env key as keyRef and configures auth profile", async () => {
it("stores byteplus env key as plaintext by default", async () => {
const agentDir = await setupTempState();
process.env.BYTEPLUS_API_KEY = "byte-env-key";
@@ -73,7 +102,7 @@ describe("volcengine/byteplus auth choice", () => {
confirm: vi.fn(async () => true),
text: vi.fn(async () => "unused"),
},
{ defaultSelect: "" },
{ defaultSelect: "plaintext" },
);
const runtime = createExitThrowingRuntime();
@@ -91,6 +120,35 @@ describe("volcengine/byteplus auth choice", () => {
mode: "api_key",
});
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["byteplus:default"]?.key).toBe("byte-env-key");
expect(parsed.profiles?.["byteplus:default"]?.keyRef).toBeUndefined();
});
it("stores byteplus env key as keyRef in ref mode", 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: "ref" },
);
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceBytePlus({
authChoice: "byteplus-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(result).not.toBeNull();
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);

View File

@@ -4,6 +4,10 @@ import {
normalizeApiKeyInput,
validateApiKeyInput,
} from "./auth-choice.api-key.js";
import {
normalizeSecretInputModeInput,
resolveSecretInputModeForEnvSelection,
} from "./auth-choice.apply-helpers.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyPrimaryModel } from "./model-picker.js";
import { applyAuthProfileConfig, setVolcengineApiKey } from "./onboard-auth.js";
@@ -18,6 +22,7 @@ export async function applyAuthChoiceVolcengine(
return null;
}
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
const envKey = resolveEnvApiKey("volcengine");
if (envKey) {
const useExisting = await params.prompter.confirm({
@@ -25,7 +30,11 @@ export async function applyAuthChoiceVolcengine(
initialValue: true,
});
if (useExisting) {
await setVolcengineApiKey(envKey.apiKey, params.agentDir);
const mode = await resolveSecretInputModeForEnvSelection({
prompter: params.prompter,
explicitMode: requestedSecretInputMode,
});
await setVolcengineApiKey(envKey.apiKey, params.agentDir, { secretInputMode: mode });
const configWithAuth = applyAuthProfileConfig(params.config, {
profileId: "volcengine:default",
provider: "volcengine",
@@ -50,7 +59,9 @@ export async function applyAuthChoiceVolcengine(
}
const trimmed = normalizeApiKeyInput(String(key));
await setVolcengineApiKey(trimmed, params.agentDir);
await setVolcengineApiKey(trimmed, params.agentDir, {
secretInputMode: requestedSecretInputMode,
});
const configWithAuth = applyAuthProfileConfig(params.config, {
profileId: "volcengine:default",
provider: "volcengine",

View File

@@ -44,7 +44,7 @@ describe("onboard auth credentials secret refs", () => {
expect(parsed.profiles?.["moonshot:default"]?.keyRef).toBeUndefined();
});
it("stores env-backed moonshot key as keyRef in ref mode", async () => {
it("stores env-backed moonshot key as keyRef when secret-input-mode=ref", async () => {
const env = await setupAuthTestEnv("openclaw-onboard-auth-credentials-ref-");
lifecycle.setStateDir(env.stateDir);
process.env.MOONSHOT_API_KEY = "sk-moonshot-env";
@@ -142,14 +142,14 @@ describe("onboard auth credentials secret refs", () => {
expect(parsed.profiles?.["openai:default"]?.key).toBeUndefined();
});
it("stores env-backed volcengine and byteplus keys as keyRef", async () => {
it("stores env-backed volcengine and byteplus keys as keyRef in ref mode", 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");
await setVolcengineApiKey("volcengine-secret", env.agentDir, { secretInputMode: "ref" });
await setByteplusApiKey("byteplus-secret", env.agentDir, { secretInputMode: "ref" });
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;

View File

@@ -4,6 +4,7 @@ import { parseDurationMs } from "../../../cli/parse-duration.js";
import type { OpenClawConfig } from "../../../config/config.js";
import type { RuntimeEnv } from "../../../runtime.js";
import { normalizeSecretInput } from "../../../utils/normalize-secret-input.js";
import { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js";
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js";
import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
import { applyPrimaryModel } from "../../model-picker.js";
@@ -74,6 +75,15 @@ export async function applyNonInteractiveAuthChoice(params: {
}): Promise<OpenClawConfig | null> {
const { authChoice, opts, runtime, baseConfig } = params;
let nextConfig = params.nextConfig;
const requestedSecretInputMode = normalizeSecretInputModeInput(opts.secretInputMode);
if (opts.secretInputMode && !requestedSecretInputMode) {
runtime.error('Invalid --secret-input-mode. Use "plaintext" or "ref".');
runtime.exit(1);
return null;
}
const apiKeyStorageOptions = requestedSecretInputMode
? { secretInputMode: requestedSecretInputMode }
: undefined;
if (authChoice === "claude-cli" || authChoice === "codex-cli") {
runtime.error(
@@ -121,7 +131,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setAnthropicApiKey(resolved.key);
await setAnthropicApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
return applyAuthProfileConfig(nextConfig, {
profileId: "anthropic:default",
@@ -198,7 +208,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setGeminiApiKey(resolved.key);
await setGeminiApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "google:default",
@@ -227,7 +237,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setZaiApiKey(resolved.key);
await setZaiApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "zai:default",
@@ -276,7 +286,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setXiaomiApiKey(resolved.key);
await setXiaomiApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "xiaomi:default",
@@ -299,7 +309,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
setXaiApiKey(resolved.key);
setXaiApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "xai:default",
@@ -322,7 +332,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setMistralApiKey(resolved.key);
await setMistralApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "mistral:default",
@@ -345,7 +355,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setVolcengineApiKey(resolved.key);
await setVolcengineApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "volcengine:default",
@@ -368,7 +378,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setByteplusApiKey(resolved.key);
await setByteplusApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "byteplus:default",
@@ -391,7 +401,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
setQianfanApiKey(resolved.key);
setQianfanApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "qianfan:default",
@@ -414,7 +424,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setOpenaiApiKey(resolved.key);
await setOpenaiApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "openai:default",
@@ -437,7 +447,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setOpenrouterApiKey(resolved.key);
await setOpenrouterApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "openrouter:default",
@@ -460,7 +470,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setKilocodeApiKey(resolved.key);
await setKilocodeApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "kilocode:default",
@@ -483,7 +493,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setLitellmApiKey(resolved.key);
await setLitellmApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "litellm:default",
@@ -506,7 +516,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setVercelAiGatewayApiKey(resolved.key);
await setVercelAiGatewayApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "vercel-ai-gateway:default",
@@ -541,7 +551,13 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setCloudflareAiGatewayConfig(accountId, gatewayId, resolved.key);
await setCloudflareAiGatewayConfig(
accountId,
gatewayId,
resolved.key,
undefined,
apiKeyStorageOptions,
);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "cloudflare-ai-gateway:default",
@@ -569,7 +585,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setMoonshotApiKey(resolved.key);
await setMoonshotApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "moonshot:default",
@@ -600,7 +616,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setKimiCodingApiKey(resolved.key);
await setKimiCodingApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "kimi-coding:default",
@@ -623,7 +639,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setSyntheticApiKey(resolved.key);
await setSyntheticApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "synthetic:default",
@@ -646,7 +662,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setVeniceApiKey(resolved.key);
await setVeniceApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "venice:default",
@@ -677,7 +693,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setMinimaxApiKey(resolved.key, undefined, profileId);
await setMinimaxApiKey(resolved.key, undefined, profileId, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
@@ -708,7 +724,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setOpencodeZenApiKey(resolved.key);
await setOpencodeZenApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "opencode:default",
@@ -731,7 +747,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setTogetherApiKey(resolved.key);
await setTogetherApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "together:default",
@@ -754,7 +770,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return null;
}
if (resolved.source !== "profile") {
await setHuggingfaceApiKey(resolved.key);
await setHuggingfaceApiKey(resolved.key, undefined, apiKeyStorageOptions);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "huggingface:default",