import { ensureAuthProfileStore } from "../agents/auth-profiles.js"; import { listChannelPlugins } from "../channels/plugins/index.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff, } from "../commands/auth-choice.js"; import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.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 { promptRemoteGatewayConfig } from "../commands/onboard-remote.js"; import { setupSkills } from "../commands/onboard-skills.js"; import { setupInternalHooks } from "../commands/onboard-hooks.js"; import type { GatewayAuthChoice, OnboardMode, OnboardOptions, ResetScope, } from "../commands/onboard-types.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { ClawdbotConfig } from "../config/config.js"; import { DEFAULT_GATEWAY_PORT, readConfigFileSnapshot, resolveGatewayPort, writeConfigFile, } from "../config/config.js"; import { logConfigUpdated } from "../config/logging.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; import { resolveUserPath } from "../utils.js"; import { finalizeOnboardingWizard } from "./onboarding.finalize.js"; import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; import { WizardCancelledError, type WizardPrompter } from "./prompts.js"; async function requireRiskAcknowledgement(params: { opts: OnboardOptions; prompter: WizardPrompter; }) { if (params.opts.acceptRisk === true) return; const rich = isRich(); const danger = (value: string) => colorize(rich, theme.error.bold, value); const warning = (value: string) => colorize(rich, theme.warn.bold, value); const good = (value: string) => colorize(rich, theme.success.bold, value); const emphasis = (value: string) => colorize(rich, theme.accentBright.bold, value); const alert = (value: string) => colorize(rich, theme.error.bold, value); await params.prompter.note( [ danger("STOP - SECURITY WARNING"), "", alert("Prompt injection can override your bot. This risk is real and not solved."), "", emphasis( "Untrusted content can carry hostile instructions (links, web pages, files, logs, pasted text).", ), emphasis( "If tools are enabled, a bad prompt can leak secrets, run commands, or access files.", ), "", warning("Model policy (required for tool-enabled or untrusted input):"), `- ${good("Recommended:")} Anthropic Opus 4.5.`, `- ${warning("Supported but not recommended:")} weaker tiers like Sonnet or Haiku, free models, cheap models, or local models.`, "", emphasis("Required safeguards:"), "- Pairing/allowlists + mention-gating.", "- Sandboxing + minimal tool access.", "- Keep secrets out of the agent's reachable filesystem.", "", emphasis( "Run this regularly (especially after changing config or exposing network surfaces):", ), "clawdbot security audit", "clawdbot security audit --deep", "clawdbot security audit --fix", "", "Docs: https://docs.clawd.bot/gateway/security", ].join("\n"), "Security", ); const ok = await params.prompter.confirm({ message: "I have read and understand the security warning, and I accept responsibility for secure configuration. 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("Clawdbot onboarding"); await requireRiskAcknowledgement({ opts, prompter }); const snapshot = await readConfigFileSnapshot(); let baseConfig: ClawdbotConfig = 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.clawd.bot/gateway/configuration", ].join("\n"), "Config issues", ); } await prompter.outro( `Config invalid. Run \`${formatCliCommand("clawdbot doctor")}\` to repair it, then re-run onboarding.`, ); runtime.exit(1); return; } const quickstartHint = `Configure details later via ${formatCliCommand("clawdbot 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", })) as "quickstart" | "advanced"); 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" }, ], })) as "keep" | "modify" | "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 === "off") return "Off (loopback only)"; 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.CLAWDBOT_GATEWAY_TOKEN, password: baseConfig.gateway?.auth?.password ?? process.env.CLAWDBOT_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: ClawdbotConfig = { ...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, includeClaudeCliIfMissing: 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, }); }