mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 19:03:58 +00:00
Fixes rough edges in the standalone install flow (install.sh -> openclaw onboard), found and verified by running the flow in a clean container and on a clean macOS Tahoe VM: - Provider auth setup failures (e.g. the preselected "Anthropic Claude CLI" option on a host without a Claude CLI login) no longer kill the whole wizard. The interactive wizard notes the error and returns to the provider picker; explicit --auth-choice automation still fails fast. - Onboarding config now persists before the channel/search/skills steps, so a crash or cancel during channel pairing no longer loses auth + gateway decisions. - With model auth skipped, finalize no longer auto-sends the "Wake up, my friend!" message (which always failed with a provider auth error). The hatch seed is gated on usable model credentials and a "Model auth missing" note explains the next step. - Search provider picker no longer labels non-key credentials (e.g. SearXNG base URL) as "API key required". - install.sh no longer warns "PATH missing npm global bin dir" with manual fix steps after it already persisted the export line; it reports the PATH was updated and how to reload the current shell. - Removed the dead interactive hooks onboarding step (setupInternalHooks); quickstart enables default hooks silently. Verified live per fix in a clean Debian/Node 24 container and on a clean macOS 26.5 Parallels VM (wizard re-prompt, SearXNG label), plus wizard/onboard test suites and tsgo:core. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
276 lines
9.8 KiB
TypeScript
276 lines
9.8 KiB
TypeScript
// Model/auth provider selection step shared by the classic wizard and bootstrap onboarding.
|
|
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
|
import type { AuthChoice, OnboardOptions } from "../commands/onboard-types.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
|
|
import { t } from "./i18n/index.js";
|
|
import { WizardCancelledError, type WizardPrompter } from "./prompts.js";
|
|
|
|
type KeepCurrentAuthChoice =
|
|
typeof import("../commands/auth-choice-prompt.js").KEEP_CURRENT_AUTH_CHOICE;
|
|
|
|
const loadAuthChoiceModule = createLazyRuntimeModule(() => import("../commands/auth-choice.js"));
|
|
|
|
const loadModelPickerModule = createLazyRuntimeModule(() => import("../commands/model-picker.js"));
|
|
|
|
function isAuthChoiceSelected(
|
|
value: AuthChoice | KeepCurrentAuthChoice,
|
|
keepCurrentAuthChoice: KeepCurrentAuthChoice | undefined,
|
|
): value is AuthChoice {
|
|
return keepCurrentAuthChoice === undefined || value !== keepCurrentAuthChoice;
|
|
}
|
|
|
|
async function resolveAuthChoiceModelSelectionPolicy(params: {
|
|
authChoice: string;
|
|
config: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
resolvePreferredProviderForAuthChoice: (params: {
|
|
choice: string;
|
|
config?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
}) => Promise<string | undefined>;
|
|
}): Promise<{
|
|
preferredProvider?: string;
|
|
promptWhenAuthChoiceProvided: boolean;
|
|
allowKeepCurrent: boolean;
|
|
}> {
|
|
const preferredProvider = await params.resolvePreferredProviderForAuthChoice({
|
|
choice: params.authChoice,
|
|
config: params.config,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
});
|
|
|
|
const [{ resolveManifestProviderAuthChoice }, { resolvePluginSetupProvider }] = await Promise.all(
|
|
[import("../plugins/provider-auth-choices.js"), import("../plugins/setup-registry.js")],
|
|
);
|
|
const manifestChoice = resolveManifestProviderAuthChoice(params.authChoice, {
|
|
config: params.config,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
includeUntrustedWorkspacePlugins: false,
|
|
});
|
|
if (manifestChoice) {
|
|
const setupProvider = resolvePluginSetupProvider({
|
|
provider: manifestChoice.providerId,
|
|
config: params.config,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
pluginIds: [manifestChoice.pluginId],
|
|
});
|
|
const setupMethod = setupProvider?.auth.find(
|
|
(method) => normalizeProviderId(method.id) === normalizeProviderId(manifestChoice.methodId),
|
|
);
|
|
const setupPolicy =
|
|
setupMethod?.wizard?.modelSelection ?? setupProvider?.wizard?.setup?.modelSelection;
|
|
return {
|
|
preferredProvider,
|
|
promptWhenAuthChoiceProvided: setupPolicy?.promptWhenAuthChoiceProvided === true,
|
|
allowKeepCurrent: setupPolicy?.allowKeepCurrent ?? true,
|
|
};
|
|
}
|
|
|
|
const { resolvePluginProviders, resolveProviderPluginChoice } =
|
|
await import("../plugins/provider-auth-choice.runtime.js");
|
|
const providers = resolvePluginProviders({
|
|
config: params.config,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
mode: "setup",
|
|
});
|
|
const resolvedChoice = resolveProviderPluginChoice({
|
|
providers,
|
|
choice: params.authChoice,
|
|
});
|
|
const matchedProvider =
|
|
resolvedChoice?.provider ??
|
|
(() => {
|
|
const preferredId = preferredProvider?.trim();
|
|
if (!preferredId) {
|
|
return undefined;
|
|
}
|
|
return providers.find(
|
|
(provider) => typeof provider.id === "string" && provider.id.trim() === preferredId,
|
|
);
|
|
})();
|
|
const setupPolicy =
|
|
resolvedChoice?.wizard?.modelSelection ?? matchedProvider?.wizard?.setup?.modelSelection;
|
|
|
|
return {
|
|
preferredProvider,
|
|
promptWhenAuthChoiceProvided: setupPolicy?.promptWhenAuthChoiceProvided === true,
|
|
allowKeepCurrent: setupPolicy?.allowKeepCurrent ?? true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Run the provider auth-choice + default-model selection loop. When
|
|
* `opts.authChoice` is set the prompt is skipped and the flag drives the flow
|
|
* (public onboarding automation contract).
|
|
*/
|
|
export async function runSetupModelAuthStep(params: {
|
|
config: OpenClawConfig;
|
|
opts: OnboardOptions;
|
|
prompter: WizardPrompter;
|
|
runtime: RuntimeEnv;
|
|
workspaceDir: string;
|
|
}): Promise<OpenClawConfig> {
|
|
const { opts, prompter, runtime, workspaceDir } = params;
|
|
let nextConfig = params.config;
|
|
const authChoiceFromPrompt = opts.authChoice === undefined;
|
|
let authChoice: AuthChoice | KeepCurrentAuthChoice | undefined = opts.authChoice;
|
|
let authStore:
|
|
| ReturnType<(typeof import("../agents/auth-profiles.runtime.js"))["ensureAuthProfileStore"]>
|
|
| undefined;
|
|
let promptAuthChoiceGrouped:
|
|
| (typeof import("../commands/auth-choice-prompt.js"))["promptAuthChoiceGrouped"]
|
|
| undefined;
|
|
let keepCurrentAuthChoice: KeepCurrentAuthChoice | undefined;
|
|
if (authChoiceFromPrompt) {
|
|
const { ensureAuthProfileStore } = await import("../agents/auth-profiles.runtime.js");
|
|
const authChoicePromptModule = await import("../commands/auth-choice-prompt.js");
|
|
promptAuthChoiceGrouped = authChoicePromptModule.promptAuthChoiceGrouped;
|
|
keepCurrentAuthChoice = authChoicePromptModule.KEEP_CURRENT_AUTH_CHOICE;
|
|
authStore = ensureAuthProfileStore(undefined, {
|
|
allowKeychainPrompt: false,
|
|
});
|
|
}
|
|
while (true) {
|
|
if (authChoiceFromPrompt) {
|
|
authChoice = await promptAuthChoiceGrouped!({
|
|
prompter,
|
|
store: authStore!,
|
|
includeSkip: true,
|
|
config: nextConfig,
|
|
workspaceDir,
|
|
allowKeepCurrentProvider: true,
|
|
});
|
|
}
|
|
if (authChoice === undefined) {
|
|
throw new WizardCancelledError(t("wizard.setup.authChoiceRequired"));
|
|
}
|
|
if (!isAuthChoiceSelected(authChoice, keepCurrentAuthChoice)) {
|
|
break;
|
|
}
|
|
|
|
if (authChoice === "custom-api-key") {
|
|
const { promptCustomApiConfig } = await import("../commands/onboard-custom.js");
|
|
const customResult = await promptCustomApiConfig({
|
|
prompter,
|
|
runtime,
|
|
config: nextConfig,
|
|
secretInputMode: opts.secretInputMode,
|
|
});
|
|
nextConfig = customResult.config;
|
|
prompter.disableBackNavigation?.();
|
|
break;
|
|
}
|
|
if (authChoice === "skip") {
|
|
// Explicit skip should stay cold: do not bootstrap auth/profile machinery
|
|
// or run model/auth checks when the caller already chose to skip setup.
|
|
if (authChoiceFromPrompt) {
|
|
const { applyPrimaryModel, promptDefaultModel } = await loadModelPickerModule();
|
|
const modelSelection = await promptDefaultModel({
|
|
config: nextConfig,
|
|
prompter,
|
|
allowKeep: true,
|
|
ignoreAllowlist: true,
|
|
includeProviderPluginSetups: false,
|
|
loadCatalog: false,
|
|
workspaceDir,
|
|
runtime,
|
|
});
|
|
if (modelSelection.config) {
|
|
nextConfig = modelSelection.config;
|
|
}
|
|
if (modelSelection.model) {
|
|
nextConfig = applyPrimaryModel(nextConfig, modelSelection.model);
|
|
}
|
|
|
|
const { warnIfModelConfigLooksOff } = await loadAuthChoiceModule();
|
|
await warnIfModelConfigLooksOff(nextConfig, prompter, { validateCatalog: false });
|
|
}
|
|
break;
|
|
}
|
|
|
|
const [
|
|
{ applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff },
|
|
{ applyPrimaryModel, promptDefaultModel },
|
|
] = await Promise.all([loadAuthChoiceModule(), loadModelPickerModule()]);
|
|
prompter.disableBackNavigation?.();
|
|
let authResult: Awaited<ReturnType<typeof applyAuthChoice>>;
|
|
try {
|
|
authResult = await applyAuthChoice({
|
|
authChoice,
|
|
config: nextConfig,
|
|
prompter,
|
|
runtime,
|
|
setDefaultModel: true,
|
|
preserveExistingDefaultModel: true,
|
|
opts: {
|
|
...opts,
|
|
token: opts.authChoice === "apiKey" && opts.token ? opts.token : undefined,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
// Provider setup failures (missing CLI login, unreachable endpoint, ...)
|
|
// must not kill the whole wizard: earlier answers only persist later, so
|
|
// re-prompt instead. Explicit --auth-choice callers still fail loudly.
|
|
if (error instanceof WizardCancelledError || !authChoiceFromPrompt) {
|
|
throw error;
|
|
}
|
|
await prompter.note(
|
|
[formatErrorMessage(error), t("wizard.setup.authChoiceFailedRetry")].join("\n"),
|
|
t("wizard.setup.authChoiceFailedTitle"),
|
|
);
|
|
continue;
|
|
}
|
|
nextConfig = authResult.config;
|
|
if (authResult.retrySelection) {
|
|
if (authChoiceFromPrompt) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (authResult.agentModelOverride) {
|
|
nextConfig = applyPrimaryModel(nextConfig, authResult.agentModelOverride);
|
|
}
|
|
|
|
const authChoiceModelSelectionPolicy = await resolveAuthChoiceModelSelectionPolicy({
|
|
authChoice,
|
|
config: nextConfig,
|
|
workspaceDir,
|
|
resolvePreferredProviderForAuthChoice,
|
|
});
|
|
const shouldPromptModelSelection =
|
|
authChoiceFromPrompt || authChoiceModelSelectionPolicy?.promptWhenAuthChoiceProvided;
|
|
if (shouldPromptModelSelection) {
|
|
const modelSelection = await promptDefaultModel({
|
|
config: nextConfig,
|
|
prompter,
|
|
allowKeep: authChoiceModelSelectionPolicy?.allowKeepCurrent ?? true,
|
|
ignoreAllowlist: true,
|
|
includeProviderPluginSetups: true,
|
|
preferredProvider: authChoiceModelSelectionPolicy?.preferredProvider,
|
|
browseCatalogOnDemand: true,
|
|
workspaceDir,
|
|
runtime,
|
|
});
|
|
if (modelSelection.config) {
|
|
nextConfig = modelSelection.config;
|
|
}
|
|
if (modelSelection.model) {
|
|
nextConfig = applyPrimaryModel(nextConfig, modelSelection.model);
|
|
}
|
|
}
|
|
|
|
await warnIfModelConfigLooksOff(nextConfig, prompter, { validateCatalog: false });
|
|
break;
|
|
}
|
|
return nextConfig;
|
|
}
|