Spaces:
Paused
Paused
| import type { | |
| GatewayAuthChoice, | |
| OnboardMode, | |
| OnboardOptions, | |
| ResetScope, | |
| } from "../commands/onboard-types.js"; | |
| import type { OpenClawConfig } from "../config/config.js"; | |
| import type { RuntimeEnv } from "../runtime.js"; | |
| import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; | |
| import { ensureAuthProfileStore } from "../agents/auth-profiles.js"; | |
| import { listChannelPlugins } from "../channels/plugins/index.js"; | |
| import { formatCliCommand } from "../cli/command-format.js"; | |
| import { installCompletion } from "../cli/completion-cli.js"; | |
| import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.js"; | |
| import { | |
| applyAuthChoice, | |
| resolvePreferredProviderForAuthChoice, | |
| warnIfModelConfigLooksOff, | |
| } from "../commands/auth-choice.js"; | |
| import { applyPrimaryModel, promptDefaultModel } from "../commands/model-picker.js"; | |
| import { setupChannels } from "../commands/onboard-channels.js"; | |
| import { | |
| applyWizardMetadata, | |
| DEFAULT_WORKSPACE, | |
| ensureWorkspaceAndSessions, | |
| handleReset, | |
| printWizardHeader, | |
| probeGatewayReachable, | |
| summarizeExistingConfig, | |
| } from "../commands/onboard-helpers.js"; | |
| import { setupInternalHooks } from "../commands/onboard-hooks.js"; | |
| import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js"; | |
| import { setupSkills } from "../commands/onboard-skills.js"; | |
| import { | |
| DEFAULT_GATEWAY_PORT, | |
| readConfigFileSnapshot, | |
| resolveGatewayPort, | |
| writeConfigFile, | |
| } from "../config/config.js"; | |
| import { logConfigUpdated } from "../config/logging.js"; | |
| import { defaultRuntime } from "../runtime.js"; | |
| import { resolveUserPath } from "../utils.js"; | |
| import { finalizeOnboardingWizard } from "./onboarding.finalize.js"; | |
| import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; | |
| import { WizardCancelledError, type WizardPrompter } from "./prompts.js"; | |
| async function requireRiskAcknowledgement(params: { | |
| opts: OnboardOptions; | |
| prompter: WizardPrompter; | |
| }) { | |
| if (params.opts.acceptRisk === true) { | |
| return; | |
| } | |
| await params.prompter.note( | |
| [ | |
| "Security warning — please read.", | |
| "", | |
| "OpenClaw is a hobby project and still in beta. Expect sharp edges.", | |
| "This bot can read files and run actions if tools are enabled.", | |
| "A bad prompt can trick it into doing unsafe things.", | |
| "", | |
| "If you’re not comfortable with basic security and access control, don’t run OpenClaw.", | |
| "Ask someone experienced to help before enabling tools or exposing it to the internet.", | |
| "", | |
| "Recommended baseline:", | |
| "- Pairing/allowlists + mention gating.", | |
| "- Sandbox + least-privilege tools.", | |
| "- Keep secrets out of the agent’s reachable filesystem.", | |
| "- Use the strongest available model for any bot with tools or untrusted inboxes.", | |
| "", | |
| "Run regularly:", | |
| "openclaw security audit --deep", | |
| "openclaw security audit --fix", | |
| "", | |
| "Must read: https://docs.openclaw.ai/gateway/security", | |
| ].join("\n"), | |
| "Security", | |
| ); | |
| const ok = await params.prompter.confirm({ | |
| message: "I understand this is powerful and inherently risky. Continue?", | |
| initialValue: false, | |
| }); | |
| if (!ok) { | |
| throw new WizardCancelledError("risk not accepted"); | |
| } | |
| } | |
| export async function runOnboardingWizard( | |
| opts: OnboardOptions, | |
| runtime: RuntimeEnv = defaultRuntime, | |
| prompter: WizardPrompter, | |
| ) { | |
| printWizardHeader(runtime); | |
| await prompter.intro("OpenClaw onboarding"); | |
| await requireRiskAcknowledgement({ opts, prompter }); | |
| const snapshot = await readConfigFileSnapshot(); | |
| let baseConfig: OpenClawConfig = snapshot.valid ? snapshot.config : {}; | |
| if (snapshot.exists && !snapshot.valid) { | |
| await prompter.note(summarizeExistingConfig(baseConfig), "Invalid config"); | |
| if (snapshot.issues.length > 0) { | |
| await prompter.note( | |
| [ | |
| ...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`), | |
| "", | |
| "Docs: https://docs.openclaw.ai/gateway/configuration", | |
| ].join("\n"), | |
| "Config issues", | |
| ); | |
| } | |
| await prompter.outro( | |
| `Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run onboarding.`, | |
| ); | |
| runtime.exit(1); | |
| return; | |
| } | |
| const quickstartHint = `Configure details later via ${formatCliCommand("openclaw configure")}.`; | |
| const manualHint = "Configure port, network, Tailscale, and auth options."; | |
| const explicitFlowRaw = opts.flow?.trim(); | |
| const normalizedExplicitFlow = explicitFlowRaw === "manual" ? "advanced" : explicitFlowRaw; | |
| if ( | |
| normalizedExplicitFlow && | |
| normalizedExplicitFlow !== "quickstart" && | |
| normalizedExplicitFlow !== "advanced" | |
| ) { | |
| runtime.error("Invalid --flow (use quickstart, manual, or advanced)."); | |
| runtime.exit(1); | |
| return; | |
| } | |
| const explicitFlow: WizardFlow | undefined = | |
| normalizedExplicitFlow === "quickstart" || normalizedExplicitFlow === "advanced" | |
| ? normalizedExplicitFlow | |
| : undefined; | |
| let flow: WizardFlow = | |
| explicitFlow ?? | |
| (await prompter.select({ | |
| message: "Onboarding mode", | |
| options: [ | |
| { value: "quickstart", label: "QuickStart", hint: quickstartHint }, | |
| { value: "advanced", label: "Manual", hint: manualHint }, | |
| ], | |
| initialValue: "quickstart", | |
| })); | |
| if (opts.mode === "remote" && flow === "quickstart") { | |
| await prompter.note( | |
| "QuickStart only supports local gateways. Switching to Manual mode.", | |
| "QuickStart", | |
| ); | |
| flow = "advanced"; | |
| } | |
| if (snapshot.exists) { | |
| await prompter.note(summarizeExistingConfig(baseConfig), "Existing config detected"); | |
| const action = await prompter.select({ | |
| message: "Config handling", | |
| options: [ | |
| { value: "keep", label: "Use existing values" }, | |
| { value: "modify", label: "Update values" }, | |
| { value: "reset", label: "Reset" }, | |
| ], | |
| }); | |
| if (action === "reset") { | |
| const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; | |
| const resetScope = (await prompter.select({ | |
| message: "Reset scope", | |
| options: [ | |
| { value: "config", label: "Config only" }, | |
| { | |
| value: "config+creds+sessions", | |
| label: "Config + creds + sessions", | |
| }, | |
| { | |
| value: "full", | |
| label: "Full reset (config + creds + sessions + workspace)", | |
| }, | |
| ], | |
| })) as ResetScope; | |
| await handleReset(resetScope, resolveUserPath(workspaceDefault), runtime); | |
| baseConfig = {}; | |
| } | |
| } | |
| const quickstartGateway: QuickstartGatewayDefaults = (() => { | |
| const hasExisting = | |
| typeof baseConfig.gateway?.port === "number" || | |
| baseConfig.gateway?.bind !== undefined || | |
| baseConfig.gateway?.auth?.mode !== undefined || | |
| baseConfig.gateway?.auth?.token !== undefined || | |
| baseConfig.gateway?.auth?.password !== undefined || | |
| baseConfig.gateway?.customBindHost !== undefined || | |
| baseConfig.gateway?.tailscale?.mode !== undefined; | |
| const bindRaw = baseConfig.gateway?.bind; | |
| const bind = | |
| bindRaw === "loopback" || | |
| bindRaw === "lan" || | |
| bindRaw === "auto" || | |
| bindRaw === "custom" || | |
| bindRaw === "tailnet" | |
| ? bindRaw | |
| : "loopback"; | |
| let authMode: GatewayAuthChoice = "token"; | |
| if ( | |
| baseConfig.gateway?.auth?.mode === "token" || | |
| baseConfig.gateway?.auth?.mode === "password" | |
| ) { | |
| authMode = baseConfig.gateway.auth.mode; | |
| } else if (baseConfig.gateway?.auth?.token) { | |
| authMode = "token"; | |
| } else if (baseConfig.gateway?.auth?.password) { | |
| authMode = "password"; | |
| } | |
| const tailscaleRaw = baseConfig.gateway?.tailscale?.mode; | |
| const tailscaleMode = | |
| tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel" | |
| ? tailscaleRaw | |
| : "off"; | |
| return { | |
| hasExisting, | |
| port: resolveGatewayPort(baseConfig), | |
| bind, | |
| authMode, | |
| tailscaleMode, | |
| token: baseConfig.gateway?.auth?.token, | |
| password: baseConfig.gateway?.auth?.password, | |
| customBindHost: baseConfig.gateway?.customBindHost, | |
| tailscaleResetOnExit: baseConfig.gateway?.tailscale?.resetOnExit ?? false, | |
| }; | |
| })(); | |
| if (flow === "quickstart") { | |
| const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => { | |
| if (value === "loopback") { | |
| return "Loopback (127.0.0.1)"; | |
| } | |
| if (value === "lan") { | |
| return "LAN"; | |
| } | |
| if (value === "custom") { | |
| return "Custom IP"; | |
| } | |
| if (value === "tailnet") { | |
| return "Tailnet (Tailscale IP)"; | |
| } | |
| return "Auto"; | |
| }; | |
| const formatAuth = (value: GatewayAuthChoice) => { | |
| if (value === "token") { | |
| return "Token (default)"; | |
| } | |
| return "Password"; | |
| }; | |
| const formatTailscale = (value: "off" | "serve" | "funnel") => { | |
| if (value === "off") { | |
| return "Off"; | |
| } | |
| if (value === "serve") { | |
| return "Serve"; | |
| } | |
| return "Funnel"; | |
| }; | |
| const quickstartLines = quickstartGateway.hasExisting | |
| ? [ | |
| "Keeping your current gateway settings:", | |
| `Gateway port: ${quickstartGateway.port}`, | |
| `Gateway bind: ${formatBind(quickstartGateway.bind)}`, | |
| ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost | |
| ? [`Gateway custom IP: ${quickstartGateway.customBindHost}`] | |
| : []), | |
| `Gateway auth: ${formatAuth(quickstartGateway.authMode)}`, | |
| `Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`, | |
| "Direct to chat channels.", | |
| ] | |
| : [ | |
| `Gateway port: ${DEFAULT_GATEWAY_PORT}`, | |
| "Gateway bind: Loopback (127.0.0.1)", | |
| "Gateway auth: Token (default)", | |
| "Tailscale exposure: Off", | |
| "Direct to chat channels.", | |
| ]; | |
| await prompter.note(quickstartLines.join("\n"), "QuickStart"); | |
| } | |
| const localPort = resolveGatewayPort(baseConfig); | |
| const localUrl = `ws://127.0.0.1:${localPort}`; | |
| const localProbe = await probeGatewayReachable({ | |
| url: localUrl, | |
| token: baseConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN, | |
| password: baseConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD, | |
| }); | |
| const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? ""; | |
| const remoteProbe = remoteUrl | |
| ? await probeGatewayReachable({ | |
| url: remoteUrl, | |
| token: baseConfig.gateway?.remote?.token, | |
| }) | |
| : null; | |
| const mode = | |
| opts.mode ?? | |
| (flow === "quickstart" | |
| ? "local" | |
| : ((await prompter.select({ | |
| message: "What do you want to set up?", | |
| options: [ | |
| { | |
| value: "local", | |
| label: "Local gateway (this machine)", | |
| hint: localProbe.ok | |
| ? `Gateway reachable (${localUrl})` | |
| : `No gateway detected (${localUrl})`, | |
| }, | |
| { | |
| value: "remote", | |
| label: "Remote gateway (info-only)", | |
| hint: !remoteUrl | |
| ? "No remote URL configured yet" | |
| : remoteProbe?.ok | |
| ? `Gateway reachable (${remoteUrl})` | |
| : `Configured but unreachable (${remoteUrl})`, | |
| }, | |
| ], | |
| })) as OnboardMode)); | |
| if (mode === "remote") { | |
| let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter); | |
| nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); | |
| await writeConfigFile(nextConfig); | |
| logConfigUpdated(runtime); | |
| await prompter.outro("Remote gateway configured."); | |
| return; | |
| } | |
| const workspaceInput = | |
| opts.workspace ?? | |
| (flow === "quickstart" | |
| ? (baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE) | |
| : await prompter.text({ | |
| message: "Workspace directory", | |
| initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE, | |
| })); | |
| const workspaceDir = resolveUserPath(workspaceInput.trim() || DEFAULT_WORKSPACE); | |
| let nextConfig: OpenClawConfig = { | |
| ...baseConfig, | |
| agents: { | |
| ...baseConfig.agents, | |
| defaults: { | |
| ...baseConfig.agents?.defaults, | |
| workspace: workspaceDir, | |
| }, | |
| }, | |
| gateway: { | |
| ...baseConfig.gateway, | |
| mode: "local", | |
| }, | |
| }; | |
| const authStore = ensureAuthProfileStore(undefined, { | |
| allowKeychainPrompt: false, | |
| }); | |
| const authChoiceFromPrompt = opts.authChoice === undefined; | |
| const authChoice = | |
| opts.authChoice ?? | |
| (await promptAuthChoiceGrouped({ | |
| prompter, | |
| store: authStore, | |
| includeSkip: true, | |
| })); | |
| const authResult = await applyAuthChoice({ | |
| authChoice, | |
| config: nextConfig, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| opts: { | |
| tokenProvider: opts.tokenProvider, | |
| token: opts.authChoice === "apiKey" && opts.token ? opts.token : undefined, | |
| }, | |
| }); | |
| nextConfig = authResult.config; | |
| if (authChoiceFromPrompt) { | |
| const modelSelection = await promptDefaultModel({ | |
| config: nextConfig, | |
| prompter, | |
| allowKeep: true, | |
| ignoreAllowlist: true, | |
| preferredProvider: resolvePreferredProviderForAuthChoice(authChoice), | |
| }); | |
| if (modelSelection.model) { | |
| nextConfig = applyPrimaryModel(nextConfig, modelSelection.model); | |
| } | |
| } | |
| await warnIfModelConfigLooksOff(nextConfig, prompter); | |
| const gateway = await configureGatewayForOnboarding({ | |
| flow, | |
| baseConfig, | |
| nextConfig, | |
| localPort, | |
| quickstartGateway, | |
| prompter, | |
| runtime, | |
| }); | |
| nextConfig = gateway.nextConfig; | |
| const settings = gateway.settings; | |
| if (opts.skipChannels ?? opts.skipProviders) { | |
| await prompter.note("Skipping channel setup.", "Channels"); | |
| } else { | |
| const quickstartAllowFromChannels = | |
| flow === "quickstart" | |
| ? listChannelPlugins() | |
| .filter((plugin) => plugin.meta.quickstartAllowFrom) | |
| .map((plugin) => plugin.id) | |
| : []; | |
| nextConfig = await setupChannels(nextConfig, runtime, prompter, { | |
| allowSignalInstall: true, | |
| forceAllowFromChannels: quickstartAllowFromChannels, | |
| skipDmPolicyPrompt: flow === "quickstart", | |
| skipConfirm: flow === "quickstart", | |
| quickstartDefaults: flow === "quickstart", | |
| }); | |
| } | |
| await writeConfigFile(nextConfig); | |
| logConfigUpdated(runtime); | |
| await ensureWorkspaceAndSessions(workspaceDir, runtime, { | |
| skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), | |
| }); | |
| if (opts.skipSkills) { | |
| await prompter.note("Skipping skills setup.", "Skills"); | |
| } else { | |
| nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter); | |
| } | |
| // Setup hooks (session memory on /new) | |
| nextConfig = await setupInternalHooks(nextConfig, runtime, prompter); | |
| nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); | |
| await writeConfigFile(nextConfig); | |
| await finalizeOnboardingWizard({ | |
| flow, | |
| opts, | |
| baseConfig, | |
| nextConfig, | |
| workspaceDir, | |
| settings, | |
| prompter, | |
| runtime, | |
| }); | |
| const installShell = await prompter.confirm({ | |
| message: "Install shell completion script?", | |
| initialValue: true, | |
| }); | |
| if (installShell) { | |
| const shell = process.env.SHELL?.split("/").pop() || "zsh"; | |
| // We pass 'yes=true' to skip any double-confirmation inside the helper, | |
| // as the wizard prompt above serves as confirmation. | |
| await installCompletion(shell, true); | |
| } | |
| } | |