mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(onboard): infer interactive provider auth from credential flags (#126946)
This commit is contained in:
committed by
GitHub
parent
94f042ba86
commit
eb07eecd40
@@ -38,9 +38,10 @@ not install or modify anything on the remote host.
|
||||
- With a configured default model, **Keep existing model config** appears
|
||||
first and becomes the default, followed by **QuickStart (recommended)**
|
||||
and **Manual setup**.
|
||||
An explicit non-`skip` `--auth-choice` still configures that provider
|
||||
without changing the existing default model, unless the provider requires
|
||||
you to select a model.
|
||||
An explicit non-`skip` `--auth-choice` or a single provider credential
|
||||
flag still configures that provider without changing the existing default
|
||||
model, unless the provider requires you to select a model. Multiple
|
||||
provider flags require an explicit `--auth-choice`.
|
||||
- When a migration provider is available, **Import from another agent**
|
||||
appears after those setup choices. Selecting it opens a provider list
|
||||
with entries such as **Import from Claude**, **Import from Codex**, and
|
||||
|
||||
@@ -997,6 +997,12 @@ describe("setupWizardCommand", () => {
|
||||
expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects ambiguous interactive provider flags before reset", async () => {
|
||||
const runtime = makeRuntime();
|
||||
await setupWizardCommand({ reset: true, nvidiaApiKey: "n", openaiApiKey: "o" }, runtime);
|
||||
expect(mocks.handleReset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates custom credential storage before reset", async () => {
|
||||
const runtime = makeRuntime();
|
||||
vi.stubEnv("CUSTOM_API_KEY", "");
|
||||
|
||||
@@ -214,7 +214,9 @@ async function validateResetAuthChoice(params: {
|
||||
resetScope: ResetScope;
|
||||
}): Promise<boolean> {
|
||||
const inferredAuthChoice =
|
||||
params.opts.authChoice || !params.opts.nonInteractive
|
||||
params.opts.authChoice ||
|
||||
params.opts.mode === "remote" ||
|
||||
(!params.opts.nonInteractive && !wantsClassicInteractiveSetup(params.opts))
|
||||
? undefined
|
||||
: inferAuthChoiceFromFlags(params.opts, {
|
||||
config: params.baseConfig,
|
||||
@@ -225,12 +227,15 @@ async function validateResetAuthChoice(params: {
|
||||
return rejectOption(
|
||||
params.runtime,
|
||||
[
|
||||
"Multiple API key flags were provided for non-interactive setup.",
|
||||
`Multiple ${params.opts.nonInteractive ? "API key" : "provider credential"} flags were provided for ${params.opts.nonInteractive ? "non-interactive" : "interactive"} setup.`,
|
||||
"Use a single provider flag or pass --auth-choice explicitly.",
|
||||
`Flags: ${inferredAuthChoice.matches.map((match) => match.label).join(", ")}`,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
if (!params.opts.nonInteractive && inferredAuthChoice) {
|
||||
return true;
|
||||
}
|
||||
const authChoice = params.opts.authChoice ?? inferredAuthChoice?.choice;
|
||||
if (!authChoice) {
|
||||
return true;
|
||||
|
||||
@@ -32,6 +32,8 @@ type ResolvePluginSetupProvider =
|
||||
typeof import("../plugins/provider-auth-choice.runtime.js").resolvePluginSetupProvider;
|
||||
type ResolveManifestProviderAuthChoice =
|
||||
typeof import("../plugins/provider-auth-choices.js").resolveManifestProviderAuthChoice;
|
||||
type ResolveProviderOnboardAuthFlags =
|
||||
typeof import("../plugins/provider-auth-choices.js").resolveProviderOnboardAuthFlags;
|
||||
type PromptDefaultModel = typeof import("../commands/model-picker.js").promptDefaultModel;
|
||||
type ApplyAuthChoice = typeof import("../commands/auth-choice.js").applyAuthChoice;
|
||||
type PrepareAuthChoice = typeof import("../commands/auth-choice.js").prepareAuthChoice;
|
||||
@@ -59,6 +61,9 @@ const resolvePreferredProviderForAuthChoice = vi.hoisted(() => vi.fn(async () =>
|
||||
const resolveManifestProviderAuthChoice = vi.hoisted(() =>
|
||||
vi.fn<ResolveManifestProviderAuthChoice>(() => undefined),
|
||||
);
|
||||
const resolveProviderOnboardAuthFlags = vi.hoisted(() =>
|
||||
vi.fn<ResolveProviderOnboardAuthFlags>(() => []),
|
||||
);
|
||||
const resolvePluginSetupProvider = vi.hoisted(() =>
|
||||
vi.fn<ResolvePluginSetupProvider>(() => undefined),
|
||||
);
|
||||
@@ -383,6 +388,7 @@ vi.mock("../commands/auth-choice.js", () => ({
|
||||
vi.mock("../plugins/provider-auth-choices.js", () => ({
|
||||
resolveManifestProviderAuthChoice,
|
||||
resolveManifestProviderAuthChoices: () => [],
|
||||
resolveProviderOnboardAuthFlags,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/setup-registry.js", () => ({
|
||||
@@ -672,6 +678,8 @@ describe("runSetupWizard", () => {
|
||||
resolvePluginProvidersRuntime.mockReturnValue([]);
|
||||
resolveManifestProviderAuthChoice.mockReset();
|
||||
resolveManifestProviderAuthChoice.mockReturnValue(undefined);
|
||||
resolveProviderOnboardAuthFlags.mockReset();
|
||||
resolveProviderOnboardAuthFlags.mockReturnValue([]);
|
||||
resolvePluginSetupProvider.mockReset();
|
||||
resolvePluginSetupProvider.mockReturnValue(undefined);
|
||||
resolveProviderPluginChoice.mockReset();
|
||||
@@ -2553,6 +2561,129 @@ describe("runSetupWizard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "an API-key flag",
|
||||
optionKey: "nvidiaApiKey",
|
||||
authChoice: "nvidia-api-key",
|
||||
cliFlag: "--nvidia-api-key",
|
||||
},
|
||||
{
|
||||
name: "a provider token flag",
|
||||
optionKey: "githubCopilotToken",
|
||||
authChoice: "github-copilot",
|
||||
cliFlag: "--github-copilot-token",
|
||||
},
|
||||
] as const)(
|
||||
"infers $name while preserving an existing default model",
|
||||
async ({ optionKey, authChoice, cliFlag }) => {
|
||||
resolveProviderOnboardAuthFlags.mockReturnValue([
|
||||
{
|
||||
optionKey,
|
||||
authChoice,
|
||||
cliFlag,
|
||||
cliOption: `${cliFlag} <key>`,
|
||||
description: "Provider credential",
|
||||
},
|
||||
]);
|
||||
const existingConfig: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: { model: { primary: "anthropic/sonnet-4.6" } },
|
||||
entries: { main: { default: true } },
|
||||
},
|
||||
};
|
||||
readConfigFileSnapshot.mockImplementation(async () =>
|
||||
configSnapshot(persistedWizardConfigs().at(-1) ?? existingConfig),
|
||||
);
|
||||
|
||||
await runSetupWizard(
|
||||
{
|
||||
acceptRisk: true,
|
||||
[optionKey]: "provider-credential-fixture",
|
||||
installDaemon: false,
|
||||
skipChannels: true,
|
||||
skipSkills: true,
|
||||
skipSearch: true,
|
||||
skipHealth: true,
|
||||
skipUi: true,
|
||||
},
|
||||
createRuntime(),
|
||||
buildWizardPrompter({}, { defaultSelect: "keep-model" }),
|
||||
);
|
||||
|
||||
expect(prepareAuthChoice).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authChoice,
|
||||
opts: expect.objectContaining({ [optionKey]: "provider-credential-fixture" }),
|
||||
}),
|
||||
);
|
||||
expect(persistedWizardConfigs().at(-1)?.agents?.defaults?.model).toEqual({
|
||||
primary: "anthropic/sonnet-4.6",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects ambiguous provider credential flags before writing local setup state", async () => {
|
||||
resolveProviderOnboardAuthFlags.mockReturnValue([
|
||||
{
|
||||
optionKey: "nvidiaApiKey",
|
||||
authChoice: "nvidia-api-key",
|
||||
cliFlag: "--nvidia-api-key",
|
||||
cliOption: "--nvidia-api-key <key>",
|
||||
description: "NVIDIA API key",
|
||||
},
|
||||
{
|
||||
optionKey: "githubCopilotToken",
|
||||
authChoice: "github-copilot",
|
||||
cliFlag: "--github-copilot-token",
|
||||
cliOption: "--github-copilot-token <token>",
|
||||
description: "GitHub Copilot token",
|
||||
},
|
||||
]);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await runSetupWizard(
|
||||
{
|
||||
acceptRisk: true,
|
||||
flow: "quickstart",
|
||||
nvidiaApiKey: "nvidia-credential-fixture",
|
||||
githubCopilotToken: "copilot-credential-fixture",
|
||||
},
|
||||
runtime,
|
||||
buildWizardPrompter({}),
|
||||
);
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Multiple provider credential flags"),
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(prepareAuthChoice).not.toHaveBeenCalled();
|
||||
expect(ensureOnboardingConfig).not.toHaveBeenCalled();
|
||||
expect(replaceConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps an explicit auth skip cold when a provider credential flag is supplied", async () => {
|
||||
await runSetupWizard(
|
||||
{
|
||||
acceptRisk: true,
|
||||
flow: "quickstart",
|
||||
authChoice: "skip",
|
||||
nvidiaApiKey: "nvidia-credential-fixture",
|
||||
installDaemon: false,
|
||||
skipChannels: true,
|
||||
skipSkills: true,
|
||||
skipSearch: true,
|
||||
skipHealth: true,
|
||||
skipUi: true,
|
||||
},
|
||||
createRuntime(),
|
||||
buildWizardPrompter({}),
|
||||
);
|
||||
|
||||
expect(resolveProviderOnboardAuthFlags).not.toHaveBeenCalled();
|
||||
expect(prepareAuthChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prompts for a model during explicit interactive Ollama setup", async () => {
|
||||
promptDefaultModel.mockClear();
|
||||
warnIfModelConfigLooksOff.mockClear();
|
||||
|
||||
+14
-1
@@ -67,10 +67,11 @@ export async function runSetupWizard(
|
||||
}
|
||||
|
||||
async function runSetupWizardOnce(
|
||||
opts: OnboardOptions,
|
||||
initialOpts: OnboardOptions,
|
||||
runtimeInput: RuntimeEnv | undefined,
|
||||
prompter: WizardPrompter,
|
||||
) {
|
||||
let opts = initialOpts;
|
||||
const runtime = runtimeInput ?? defaultRuntime;
|
||||
const onboardHelpers = await import("../commands/onboard-helpers.js");
|
||||
await onboardHelpers.printWizardHeader(runtime);
|
||||
@@ -498,6 +499,18 @@ async function runSetupWizardOnce(
|
||||
prompter,
|
||||
hasAuthoredRoster,
|
||||
});
|
||||
if (opts.authChoice === undefined) {
|
||||
const { inferAuthChoiceFromFlags } =
|
||||
await import("../commands/onboard-non-interactive/local/auth-choice-inference.js");
|
||||
const inferred = inferAuthChoiceFromFlags(opts, { config: baseConfig, workspaceDir });
|
||||
if (inferred.matches.length > 1) {
|
||||
runtime.error(
|
||||
`Multiple provider credential flags (${inferred.matches.map((match) => match.label).join(", ")}). Use one flag or pass --auth-choice explicitly.`,
|
||||
);
|
||||
return runtime.exit(1);
|
||||
}
|
||||
opts = inferred.choice ? { ...opts, authChoice: inferred.choice } : opts;
|
||||
}
|
||||
const firstAgent = await firstAgentOnboarding.promptFirstOnboardingAgent(
|
||||
hasAuthoredRoster,
|
||||
opts.agentName,
|
||||
|
||||
Reference in New Issue
Block a user