diff --git a/src/commands/onboard-non-interactive/api-keys.test.ts b/src/commands/onboard-non-interactive/api-keys.test.ts index e276aa66d2f1..5ab88d5d89b8 100644 --- a/src/commands/onboard-non-interactive/api-keys.test.ts +++ b/src/commands/onboard-non-interactive/api-keys.test.ts @@ -91,30 +91,79 @@ describe("resolveNonInteractiveApiKey", () => { expect(runtime.exit).not.toHaveBeenCalled(); }); - it("rejects command-shaped flag keys before returning them", async () => { + it.each([ + { source: "flag", flagValue: "malformed" }, + { source: "environment", resolvedEnv: true }, + { source: "secret-ref environment", resolvedEnv: true, secretInputMode: "ref" as const }, + ])("rejects command-shaped $source keys before returning them", async (testCase) => { const runtime = createRuntime(); - resolveEnvApiKey.mockImplementation(() => { - throw new Error("env lookup should not run for a malformed explicit flag"); - }); + const malformedKey = + "openclaw onboard --non-interactive --auth-choice=zai-coding-global --zai-api-key $ZAI_API_KEY"; + if (testCase.resolvedEnv) { + resolveEnvApiKey.mockReturnValue({ + apiKey: malformedKey, + source: "env: ZAI_API_KEY", + }); + } else { + resolveEnvApiKey.mockImplementation(() => { + throw new Error("env lookup should not run for a malformed explicit flag"); + }); + } const result = await resolveNonInteractiveApiKey({ provider: "zai", cfg: {}, - flagValue: - "openclaw onboard --non-interactive --auth-choice=zai-coding-global --zai-api-key $ZAI_API_KEY", + flagValue: testCase.flagValue === "malformed" ? malformedKey : undefined, flagName: "--zai-api-key", envVar: "ZAI_API_KEY", runtime: runtime as never, + secretInputMode: testCase.secretInputMode, }); expect(result).toBeNull(); - expect(resolveEnvApiKey).not.toHaveBeenCalled(); + expect(resolveEnvApiKey).toHaveBeenCalledTimes(testCase.resolvedEnv ? 1 : 0); expect(runtime.error).toHaveBeenCalledWith( - "Paste the API key value, not an OpenClaw onboarding command.", + testCase.resolvedEnv + ? "Paste the API key value, not an OpenClaw onboarding command. Check ZAI_API_KEY." + : "Paste the API key value, not an OpenClaw onboarding command.", ); expect(runtime.exit).toHaveBeenCalledWith(1); }); + it("rejects a command-shaped explicit env key before a secret-ref flag", async () => { + const runtime = createRuntime(); + const previousZaiApiKey = process.env.ZAI_API_KEY; + process.env.ZAI_API_KEY = "openclaw onboard --non-interactive --auth-choice zai-api-key"; // pragma: allowlist secret + resolveEnvApiKey.mockImplementation(() => { + throw new Error("broad env lookup should not run for an explicit ref-mode flag"); + }); + + try { + const result = await resolveNonInteractiveApiKey({ + provider: "zai", + cfg: {}, + flagValue: "zai-flag-key", + flagName: "--zai-api-key", + envVar: "ZAI_API_KEY", + runtime: runtime as never, + secretInputMode: "ref", + }); + + expect(result).toBeNull(); + expect(resolveEnvApiKey).not.toHaveBeenCalled(); + expect(runtime.error).toHaveBeenCalledWith( + "Paste the API key value, not an OpenClaw onboarding command. Check ZAI_API_KEY.", + ); + expect(runtime.exit).toHaveBeenCalledWith(1); + } finally { + if (previousZaiApiKey === undefined) { + delete process.env.ZAI_API_KEY; + } else { + process.env.ZAI_API_KEY = previousZaiApiKey; + } + } + }); + it.each([ { provider: "xai", diff --git a/src/commands/onboard-non-interactive/api-keys.ts b/src/commands/onboard-non-interactive/api-keys.ts index ae25e0eb9871..90d23c899562 100644 --- a/src/commands/onboard-non-interactive/api-keys.ts +++ b/src/commands/onboard-non-interactive/api-keys.ts @@ -90,12 +90,21 @@ export async function resolveNonInteractiveApiKey(params: { envVarName: parseEnvVarNameFromSourceLabel(envResolved?.source) ?? explicitEnvVar, }; }; + const returnOperatorKey = (key: string, source: "flag" | "env", envVarName?: string) => { + if (!isMalformedApiKeyInput(key)) { + return envVarName ? { key, source, envVarName } : { key, source }; + } + const envHint = source === "env" ? ` Check ${envVarName ?? params.envVar}.` : ""; + params.runtime.error(`Paste the API key value, not an OpenClaw onboarding command.${envHint}`); + params.runtime.exit(1); + return null; + }; const useSecretRefMode = params.secretInputMode === "ref"; // pragma: allowlist secret if (useSecretRefMode && flagKey) { const explicitEnvKey = resolveExplicitEnvKey(); if (explicitEnvKey) { - return { key: explicitEnvKey, source: "env", envVarName: explicitEnvVar }; + return returnOperatorKey(explicitEnvKey, "env", explicitEnvVar); } // A literal flag value cannot be converted into a durable secret reference; // require an env var so the stored config can reference a stable name. @@ -124,24 +133,21 @@ export async function resolveNonInteractiveApiKey(params: { params.runtime.exit(1); return null; } - return { key: resolvedEnv.key, source: "env", envVarName: resolvedEnv.envVarName }; + return returnOperatorKey(resolvedEnv.key, "env", resolvedEnv.envVarName); } } if (flagKey) { - if (isMalformedApiKeyInput(flagKey)) { - params.runtime.error("Paste the API key value, not an OpenClaw onboarding command."); - params.runtime.exit(1); - return null; - } - return { key: flagKey, source: "flag" }; + return returnOperatorKey(flagKey, "flag"); } const resolvedEnv = resolveEnvKey(); if (resolvedEnv.key) { - return { key: resolvedEnv.key, source: "env", envVarName: resolvedEnv.envVarName }; + return returnOperatorKey(resolvedEnv.key, "env", resolvedEnv.envVarName); } + // Stored profiles are pre-existing state: doctor diagnoses them, while a new + // flag or env value must remain able to replace them during onboarding. if (params.allowProfile ?? true) { const profileKey = await resolveApiKeyFromProfiles({ provider: params.provider,