fix(onboard): infer interactive provider auth from credential flags (#126946)

This commit is contained in:
Peter Steinberger
2026-08-20 19:17:50 -07:00
committed by GitHub
parent 94f042ba86
commit eb07eecd40
5 changed files with 162 additions and 6 deletions
+4 -3
View File
@@ -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
+6
View File
@@ -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", "");
+7 -2
View File
@@ -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;
+131
View File
@@ -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
View File
@@ -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,