2026-02-18 01:34:35 +00:00
|
|
|
|
import { formatCliCommand } from "../cli/command-format.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
|
import type {
|
|
|
|
|
|
GatewayAuthChoice,
|
|
|
|
|
|
OnboardMode,
|
|
|
|
|
|
OnboardOptions,
|
|
|
|
|
|
ResetScope,
|
|
|
|
|
|
} from "../commands/onboard-types.js";
|
|
|
|
|
|
import type { OpenClawConfig } from "../config/config.js";
|
2026-01-03 16:04:19 +01:00
|
|
|
|
import {
|
2026-01-08 11:54:40 +01:00
|
|
|
|
DEFAULT_GATEWAY_PORT,
|
2026-01-03 16:04:19 +01:00
|
|
|
|
readConfigFileSnapshot,
|
|
|
|
|
|
resolveGatewayPort,
|
|
|
|
|
|
writeConfigFile,
|
|
|
|
|
|
} from "../config/config.js";
|
2026-03-02 20:58:20 -06:00
|
|
|
|
import { normalizeSecretInputString } from "../config/types.secrets.js";
|
2026-03-17 20:31:59 -07:00
|
|
|
|
import {
|
|
|
|
|
|
buildPluginCompatibilityNotices,
|
|
|
|
|
|
formatPluginCompatibilityNotice,
|
|
|
|
|
|
} from "../plugins/status.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
|
import type { RuntimeEnv } from "../runtime.js";
|
2026-01-03 16:04:19 +01:00
|
|
|
|
import { defaultRuntime } from "../runtime.js";
|
2026-01-14 05:40:10 +00:00
|
|
|
|
import { resolveUserPath } from "../utils.js";
|
2026-01-15 01:25:11 +00:00
|
|
|
|
import { WizardCancelledError, type WizardPrompter } from "./prompts.js";
|
2026-03-15 21:39:23 -07:00
|
|
|
|
import { resolveSetupSecretInputString } from "./setup.secret-input.js";
|
|
|
|
|
|
import type { QuickstartGatewayDefaults, WizardFlow } from "./setup.types.js";
|
2026-01-15 01:25:11 +00:00
|
|
|
|
|
|
|
|
|
|
async function requireRiskAcknowledgement(params: {
|
|
|
|
|
|
opts: OnboardOptions;
|
|
|
|
|
|
prompter: WizardPrompter;
|
|
|
|
|
|
}) {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (params.opts.acceptRisk === true) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-15 01:25:11 +00:00
|
|
|
|
|
|
|
|
|
|
await params.prompter.note(
|
|
|
|
|
|
[
|
2026-01-26 16:58:51 +00:00
|
|
|
|
"Security warning — please read.",
|
2026-01-15 01:25:11 +00:00
|
|
|
|
"",
|
2026-01-30 03:15:10 +01:00
|
|
|
|
"OpenClaw is a hobby project and still in beta. Expect sharp edges.",
|
2026-02-26 02:59:10 +01:00
|
|
|
|
"By default, OpenClaw is a personal agent: one trusted operator boundary.",
|
2026-01-26 16:58:51 +00:00
|
|
|
|
"This bot can read files and run actions if tools are enabled.",
|
|
|
|
|
|
"A bad prompt can trick it into doing unsafe things.",
|
2026-01-15 01:25:11 +00:00
|
|
|
|
"",
|
2026-02-26 02:59:10 +01:00
|
|
|
|
"OpenClaw is not a hostile multi-tenant boundary by default.",
|
|
|
|
|
|
"If multiple users can message one tool-enabled agent, they share that delegated tool authority.",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"If you’re not comfortable with security hardening and access control, don’t run OpenClaw.",
|
2026-01-26 16:58:51 +00:00
|
|
|
|
"Ask someone experienced to help before enabling tools or exposing it to the internet.",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"Recommended baseline:",
|
|
|
|
|
|
"- Pairing/allowlists + mention gating.",
|
2026-02-26 02:59:10 +01:00
|
|
|
|
"- Multi-user/shared inbox: split trust boundaries (separate gateway/credentials, ideally separate OS users/hosts).",
|
2026-01-26 16:58:51 +00:00
|
|
|
|
"- Sandbox + least-privilege tools.",
|
2026-02-26 02:59:10 +01:00
|
|
|
|
"- Shared inboxes: isolate DM sessions (`session.dmScope: per-channel-peer`) and keep tool access minimal.",
|
2026-01-26 16:58:51 +00:00
|
|
|
|
"- Keep secrets out of the agent’s reachable filesystem.",
|
|
|
|
|
|
"- Use the strongest available model for any bot with tools or untrusted inboxes.",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"Run regularly:",
|
2026-01-30 03:15:10 +01:00
|
|
|
|
"openclaw security audit --deep",
|
|
|
|
|
|
"openclaw security audit --fix",
|
2026-01-26 16:58:51 +00:00
|
|
|
|
"",
|
2026-01-30 03:15:10 +01:00
|
|
|
|
"Must read: https://docs.openclaw.ai/gateway/security",
|
2026-01-15 01:25:11 +00:00
|
|
|
|
].join("\n"),
|
|
|
|
|
|
"Security",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const ok = await params.prompter.confirm({
|
2026-02-26 02:59:10 +01:00
|
|
|
|
message:
|
|
|
|
|
|
"I understand this is personal-by-default and shared/multi-user use requires lock-down. Continue?",
|
2026-01-15 01:25:11 +00:00
|
|
|
|
initialValue: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!ok) {
|
|
|
|
|
|
throw new WizardCancelledError("risk not accepted");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-03-15 21:39:23 -07:00
|
|
|
|
export async function runSetupWizard(
|
2026-01-03 16:04:19 +01:00
|
|
|
|
opts: OnboardOptions,
|
|
|
|
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
|
|
|
|
prompter: WizardPrompter,
|
|
|
|
|
|
) {
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const onboardHelpers = await import("../commands/onboard-helpers.js");
|
|
|
|
|
|
onboardHelpers.printWizardHeader(runtime);
|
2026-03-15 21:59:51 -07:00
|
|
|
|
await prompter.intro("OpenClaw setup");
|
2026-01-15 01:25:11 +00:00
|
|
|
|
await requireRiskAcknowledgement({ opts, prompter });
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
|
|
|
|
|
const snapshot = await readConfigFileSnapshot();
|
2026-03-09 08:33:28 +00:00
|
|
|
|
let baseConfig: OpenClawConfig = snapshot.valid ? (snapshot.exists ? snapshot.config : {}) : {};
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-01-22 23:07:40 +00:00
|
|
|
|
if (snapshot.exists && !snapshot.valid) {
|
2026-02-15 19:18:07 +00:00
|
|
|
|
await prompter.note(onboardHelpers.summarizeExistingConfig(baseConfig), "Invalid config");
|
2026-01-22 23:07:40 +00:00
|
|
|
|
if (snapshot.issues.length > 0) {
|
2026-01-03 16:04:19 +01:00
|
|
|
|
await prompter.note(
|
2026-01-07 01:19:31 +01:00
|
|
|
|
[
|
|
|
|
|
|
...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`),
|
|
|
|
|
|
"",
|
2026-01-30 03:15:10 +01:00
|
|
|
|
"Docs: https://docs.openclaw.ai/gateway/configuration",
|
2026-01-07 01:19:31 +01:00
|
|
|
|
].join("\n"),
|
2026-01-03 16:04:19 +01:00
|
|
|
|
"Config issues",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-22 23:07:40 +00:00
|
|
|
|
await prompter.outro(
|
2026-03-15 21:39:23 -07:00
|
|
|
|
`Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run setup.`,
|
2026-01-22 23:07:40 +00:00
|
|
|
|
);
|
|
|
|
|
|
runtime.exit(1);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-17 19:58:40 -07:00
|
|
|
|
|
|
|
|
|
|
const compatibilityNotices = snapshot.valid
|
|
|
|
|
|
? buildPluginCompatibilityNotices({ config: baseConfig })
|
|
|
|
|
|
: [];
|
|
|
|
|
|
if (compatibilityNotices.length > 0) {
|
|
|
|
|
|
await prompter.note(
|
|
|
|
|
|
[
|
|
|
|
|
|
`Detected ${compatibilityNotices.length} plugin compatibility notice${compatibilityNotices.length === 1 ? "" : "s"} in the current config.`,
|
|
|
|
|
|
...compatibilityNotices
|
|
|
|
|
|
.slice(0, 4)
|
2026-03-17 20:31:59 -07:00
|
|
|
|
.map((notice) => `- ${formatPluginCompatibilityNotice(notice)}`),
|
2026-03-17 19:58:40 -07:00
|
|
|
|
...(compatibilityNotices.length > 4
|
|
|
|
|
|
? [`- ... +${compatibilityNotices.length - 4} more`]
|
|
|
|
|
|
: []),
|
|
|
|
|
|
"",
|
|
|
|
|
|
`Review: ${formatCliCommand("openclaw doctor")}`,
|
|
|
|
|
|
`Inspect: ${formatCliCommand("openclaw plugins inspect --all")}`,
|
|
|
|
|
|
].join("\n"),
|
|
|
|
|
|
"Plugin compatibility",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
|
const quickstartHint = `Configure details later via ${formatCliCommand("openclaw configure")}.`;
|
2026-01-22 23:07:40 +00:00
|
|
|
|
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 ??
|
2026-01-31 16:03:28 +09:00
|
|
|
|
(await prompter.select({
|
2026-03-15 21:59:51 -07:00
|
|
|
|
message: "Setup mode",
|
2026-01-22 23:07:40 +00:00
|
|
|
|
options: [
|
|
|
|
|
|
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
|
|
|
|
|
|
{ value: "advanced", label: "Manual", hint: manualHint },
|
|
|
|
|
|
],
|
|
|
|
|
|
initialValue: "quickstart",
|
2026-01-31 16:03:28 +09:00
|
|
|
|
}));
|
2026-01-22 23:07:40 +00:00
|
|
|
|
|
|
|
|
|
|
if (opts.mode === "remote" && flow === "quickstart") {
|
|
|
|
|
|
await prompter.note(
|
|
|
|
|
|
"QuickStart only supports local gateways. Switching to Manual mode.",
|
|
|
|
|
|
"QuickStart",
|
|
|
|
|
|
);
|
|
|
|
|
|
flow = "advanced";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (snapshot.exists) {
|
2026-02-15 19:18:07 +00:00
|
|
|
|
await prompter.note(
|
|
|
|
|
|
onboardHelpers.summarizeExistingConfig(baseConfig),
|
|
|
|
|
|
"Existing config detected",
|
|
|
|
|
|
);
|
2026-01-13 01:18:18 +00:00
|
|
|
|
|
2026-01-31 16:03:28 +09:00
|
|
|
|
const action = await prompter.select({
|
2026-01-03 16:04:19 +01:00
|
|
|
|
message: "Config handling",
|
|
|
|
|
|
options: [
|
|
|
|
|
|
{ value: "keep", label: "Use existing values" },
|
|
|
|
|
|
{ value: "modify", label: "Update values" },
|
|
|
|
|
|
{ value: "reset", label: "Reset" },
|
|
|
|
|
|
],
|
2026-01-31 16:03:28 +09:00
|
|
|
|
});
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
|
|
|
|
|
if (action === "reset") {
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const workspaceDefault =
|
|
|
|
|
|
baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE;
|
2026-01-03 16:04:19 +01:00
|
|
|
|
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;
|
2026-02-15 19:18:07 +00:00
|
|
|
|
await onboardHelpers.handleReset(resetScope, resolveUserPath(workspaceDefault), runtime);
|
2026-01-03 16:04:19 +01:00
|
|
|
|
baseConfig = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 05:40:10 +00:00
|
|
|
|
const quickstartGateway: QuickstartGatewayDefaults = (() => {
|
2026-01-08 15:23:29 +03:00
|
|
|
|
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 ||
|
2026-01-11 14:13:13 -06:00
|
|
|
|
baseConfig.gateway?.customBindHost !== undefined ||
|
2026-01-08 15:23:29 +03:00
|
|
|
|
baseConfig.gateway?.tailscale?.mode !== undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const bindRaw = baseConfig.gateway?.bind;
|
|
|
|
|
|
const bind =
|
2026-01-21 20:35:39 +00:00
|
|
|
|
bindRaw === "loopback" ||
|
|
|
|
|
|
bindRaw === "lan" ||
|
|
|
|
|
|
bindRaw === "auto" ||
|
|
|
|
|
|
bindRaw === "custom" ||
|
|
|
|
|
|
bindRaw === "tailnet"
|
2026-01-08 15:23:29 +03:00
|
|
|
|
? bindRaw
|
|
|
|
|
|
: "loopback";
|
|
|
|
|
|
|
2026-01-11 01:50:46 +01:00
|
|
|
|
let authMode: GatewayAuthChoice = "token";
|
2026-01-08 15:23:29 +03:00
|
|
|
|
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 =
|
2026-01-14 14:31:43 +00:00
|
|
|
|
tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel"
|
2026-01-08 15:23:29 +03:00
|
|
|
|
? tailscaleRaw
|
|
|
|
|
|
: "off";
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
hasExisting,
|
|
|
|
|
|
port: resolveGatewayPort(baseConfig),
|
|
|
|
|
|
bind,
|
|
|
|
|
|
authMode,
|
|
|
|
|
|
tailscaleMode,
|
|
|
|
|
|
token: baseConfig.gateway?.auth?.token,
|
|
|
|
|
|
password: baseConfig.gateway?.auth?.password,
|
2026-01-11 14:13:13 -06:00
|
|
|
|
customBindHost: baseConfig.gateway?.customBindHost,
|
2026-01-08 15:23:29 +03:00
|
|
|
|
tailscaleResetOnExit: baseConfig.gateway?.tailscale?.resetOnExit ?? false,
|
|
|
|
|
|
};
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
2026-01-08 11:54:40 +01:00
|
|
|
|
if (flow === "quickstart") {
|
2026-01-21 20:35:39 +00:00
|
|
|
|
const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
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)";
|
|
|
|
|
|
}
|
2026-01-08 15:23:29 +03:00
|
|
|
|
return "Auto";
|
|
|
|
|
|
};
|
|
|
|
|
|
const formatAuth = (value: GatewayAuthChoice) => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (value === "token") {
|
|
|
|
|
|
return "Token (default)";
|
|
|
|
|
|
}
|
2026-01-08 15:23:29 +03:00
|
|
|
|
return "Password";
|
|
|
|
|
|
};
|
|
|
|
|
|
const formatTailscale = (value: "off" | "serve" | "funnel") => {
|
2026-01-31 16:19:20 +09:00
|
|
|
|
if (value === "off") {
|
|
|
|
|
|
return "Off";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (value === "serve") {
|
|
|
|
|
|
return "Serve";
|
|
|
|
|
|
}
|
2026-01-08 15:23:29 +03:00
|
|
|
|
return "Funnel";
|
|
|
|
|
|
};
|
|
|
|
|
|
const quickstartLines = quickstartGateway.hasExisting
|
|
|
|
|
|
? [
|
|
|
|
|
|
"Keeping your current gateway settings:",
|
|
|
|
|
|
`Gateway port: ${quickstartGateway.port}`,
|
|
|
|
|
|
`Gateway bind: ${formatBind(quickstartGateway.bind)}`,
|
2026-01-14 14:31:43 +00:00
|
|
|
|
...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost
|
2026-01-11 14:13:13 -06:00
|
|
|
|
? [`Gateway custom IP: ${quickstartGateway.customBindHost}`]
|
|
|
|
|
|
: []),
|
2026-01-08 15:23:29 +03:00
|
|
|
|
`Gateway auth: ${formatAuth(quickstartGateway.authMode)}`,
|
2026-01-14 14:31:43 +00:00
|
|
|
|
`Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`,
|
2026-01-13 08:11:59 +00:00
|
|
|
|
"Direct to chat channels.",
|
2026-01-08 15:23:29 +03:00
|
|
|
|
]
|
|
|
|
|
|
: [
|
|
|
|
|
|
`Gateway port: ${DEFAULT_GATEWAY_PORT}`,
|
|
|
|
|
|
"Gateway bind: Loopback (127.0.0.1)",
|
2026-01-11 01:50:46 +01:00
|
|
|
|
"Gateway auth: Token (default)",
|
2026-01-08 15:23:29 +03:00
|
|
|
|
"Tailscale exposure: Off",
|
2026-01-13 08:11:59 +00:00
|
|
|
|
"Direct to chat channels.",
|
2026-01-08 15:23:29 +03:00
|
|
|
|
];
|
2026-01-08 15:30:54 +03:00
|
|
|
|
await prompter.note(quickstartLines.join("\n"), "QuickStart");
|
2026-01-08 11:54:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 16:04:19 +01:00
|
|
|
|
const localPort = resolveGatewayPort(baseConfig);
|
|
|
|
|
|
const localUrl = `ws://127.0.0.1:${localPort}`;
|
2026-03-05 12:53:56 -06:00
|
|
|
|
let localGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN ?? process.env.CLAWDBOT_GATEWAY_TOKEN;
|
|
|
|
|
|
try {
|
2026-03-15 21:39:23 -07:00
|
|
|
|
const resolvedGatewayToken = await resolveSetupSecretInputString({
|
2026-03-05 12:53:56 -06:00
|
|
|
|
config: baseConfig,
|
|
|
|
|
|
value: baseConfig.gateway?.auth?.token,
|
|
|
|
|
|
path: "gateway.auth.token",
|
|
|
|
|
|
env: process.env,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (resolvedGatewayToken) {
|
|
|
|
|
|
localGatewayToken = resolvedGatewayToken;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await prompter.note(
|
|
|
|
|
|
[
|
2026-03-15 21:59:51 -07:00
|
|
|
|
"Could not resolve gateway.auth.token SecretRef for setup probe.",
|
2026-03-05 12:53:56 -06:00
|
|
|
|
error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
].join("\n"),
|
|
|
|
|
|
"Gateway auth",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-02 20:58:20 -06:00
|
|
|
|
let localGatewayPassword =
|
2026-03-05 12:53:56 -06:00
|
|
|
|
process.env.OPENCLAW_GATEWAY_PASSWORD ?? process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
2026-03-02 20:58:20 -06:00
|
|
|
|
try {
|
2026-03-15 21:39:23 -07:00
|
|
|
|
const resolvedGatewayPassword = await resolveSetupSecretInputString({
|
2026-03-02 20:58:20 -06:00
|
|
|
|
config: baseConfig,
|
|
|
|
|
|
value: baseConfig.gateway?.auth?.password,
|
|
|
|
|
|
path: "gateway.auth.password",
|
|
|
|
|
|
env: process.env,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (resolvedGatewayPassword) {
|
|
|
|
|
|
localGatewayPassword = resolvedGatewayPassword;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await prompter.note(
|
|
|
|
|
|
[
|
2026-03-15 21:59:51 -07:00
|
|
|
|
"Could not resolve gateway.auth.password SecretRef for setup probe.",
|
2026-03-02 20:58:20 -06:00
|
|
|
|
error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
].join("\n"),
|
|
|
|
|
|
"Gateway auth",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const localProbe = await onboardHelpers.probeGatewayReachable({
|
2026-01-03 16:04:19 +01:00
|
|
|
|
url: localUrl,
|
2026-03-05 12:53:56 -06:00
|
|
|
|
token: localGatewayToken,
|
2026-03-02 20:58:20 -06:00
|
|
|
|
password: localGatewayPassword,
|
2026-01-03 16:04:19 +01:00
|
|
|
|
});
|
|
|
|
|
|
const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? "";
|
2026-03-05 12:53:56 -06:00
|
|
|
|
let remoteGatewayToken = normalizeSecretInputString(baseConfig.gateway?.remote?.token);
|
|
|
|
|
|
try {
|
2026-03-15 21:39:23 -07:00
|
|
|
|
const resolvedRemoteGatewayToken = await resolveSetupSecretInputString({
|
2026-03-05 12:53:56 -06:00
|
|
|
|
config: baseConfig,
|
|
|
|
|
|
value: baseConfig.gateway?.remote?.token,
|
|
|
|
|
|
path: "gateway.remote.token",
|
|
|
|
|
|
env: process.env,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (resolvedRemoteGatewayToken) {
|
|
|
|
|
|
remoteGatewayToken = resolvedRemoteGatewayToken;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await prompter.note(
|
|
|
|
|
|
[
|
2026-03-15 21:59:51 -07:00
|
|
|
|
"Could not resolve gateway.remote.token SecretRef for setup probe.",
|
2026-03-05 12:53:56 -06:00
|
|
|
|
error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
].join("\n"),
|
|
|
|
|
|
"Gateway auth",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-03 16:04:19 +01:00
|
|
|
|
const remoteProbe = remoteUrl
|
2026-02-15 19:18:07 +00:00
|
|
|
|
? await onboardHelpers.probeGatewayReachable({
|
2026-01-03 16:04:19 +01:00
|
|
|
|
url: remoteUrl,
|
2026-03-05 12:53:56 -06:00
|
|
|
|
token: remoteGatewayToken,
|
2026-01-03 16:04:19 +01:00
|
|
|
|
})
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
const mode =
|
|
|
|
|
|
opts.mode ??
|
2026-01-08 11:54:40 +01:00
|
|
|
|
(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));
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
|
|
|
|
|
if (mode === "remote") {
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const { promptRemoteGatewayConfig } = await import("../commands/onboard-remote.js");
|
|
|
|
|
|
const { logConfigUpdated } = await import("../config/logging.js");
|
2026-03-02 20:58:20 -06:00
|
|
|
|
let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter, {
|
|
|
|
|
|
secretInputMode: opts.secretInputMode,
|
|
|
|
|
|
});
|
2026-02-15 19:18:07 +00:00
|
|
|
|
nextConfig = onboardHelpers.applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
2026-01-03 16:04:19 +01:00
|
|
|
|
await writeConfigFile(nextConfig);
|
2026-01-23 04:01:11 +00:00
|
|
|
|
logConfigUpdated(runtime);
|
2026-01-03 16:04:19 +01:00
|
|
|
|
await prompter.outro("Remote gateway configured.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const workspaceInput =
|
|
|
|
|
|
opts.workspace ??
|
2026-01-08 11:54:40 +01:00
|
|
|
|
(flow === "quickstart"
|
2026-02-15 19:18:07 +00:00
|
|
|
|
? (baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE)
|
2026-01-08 11:54:40 +01:00
|
|
|
|
: await prompter.text({
|
|
|
|
|
|
message: "Workspace directory",
|
2026-02-15 19:18:07 +00:00
|
|
|
|
initialValue: baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE,
|
2026-01-08 11:54:40 +01:00
|
|
|
|
}));
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const workspaceDir = resolveUserPath(workspaceInput.trim() || onboardHelpers.DEFAULT_WORKSPACE);
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-03-15 21:39:23 -07:00
|
|
|
|
const { applyLocalSetupWorkspaceConfig } = await import("../commands/onboard-config.js");
|
|
|
|
|
|
let nextConfig: OpenClawConfig = applyLocalSetupWorkspaceConfig(baseConfig, workspaceDir);
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-03-13 01:33:29 +00:00
|
|
|
|
const { ensureAuthProfileStore } = await import("../agents/auth-profiles.runtime.js");
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const { promptAuthChoiceGrouped } = await import("../commands/auth-choice-prompt.js");
|
|
|
|
|
|
const { promptCustomApiConfig } = await import("../commands/onboard-custom.js");
|
|
|
|
|
|
const { applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff } =
|
|
|
|
|
|
await import("../commands/auth-choice.js");
|
|
|
|
|
|
const { applyPrimaryModel, promptDefaultModel } = await import("../commands/model-picker.js");
|
|
|
|
|
|
|
2026-01-08 23:17:08 +01:00
|
|
|
|
const authStore = ensureAuthProfileStore(undefined, {
|
|
|
|
|
|
allowKeychainPrompt: false,
|
|
|
|
|
|
});
|
2026-01-09 23:13:25 +01:00
|
|
|
|
const authChoiceFromPrompt = opts.authChoice === undefined;
|
2026-01-09 22:16:17 +01:00
|
|
|
|
const authChoice =
|
|
|
|
|
|
opts.authChoice ??
|
2026-01-12 05:08:07 +00:00
|
|
|
|
(await promptAuthChoiceGrouped({
|
|
|
|
|
|
prompter,
|
|
|
|
|
|
store: authStore,
|
|
|
|
|
|
includeSkip: true,
|
2026-03-12 22:24:22 +00:00
|
|
|
|
config: nextConfig,
|
|
|
|
|
|
workspaceDir,
|
2026-01-12 05:08:07 +00:00
|
|
|
|
}));
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-02-10 20:31:02 +08:00
|
|
|
|
if (authChoice === "custom-api-key") {
|
|
|
|
|
|
const customResult = await promptCustomApiConfig({
|
|
|
|
|
|
prompter,
|
|
|
|
|
|
runtime,
|
|
|
|
|
|
config: nextConfig,
|
2026-02-24 22:26:33 -06:00
|
|
|
|
secretInputMode: opts.secretInputMode,
|
2026-02-10 20:31:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
nextConfig = customResult.config;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const authResult = await applyAuthChoice({
|
|
|
|
|
|
authChoice,
|
|
|
|
|
|
config: nextConfig,
|
|
|
|
|
|
prompter,
|
|
|
|
|
|
runtime,
|
2026-03-12 22:24:22 +00:00
|
|
|
|
setDefaultModel: true,
|
2026-02-10 20:31:02 +08:00
|
|
|
|
opts: {
|
|
|
|
|
|
tokenProvider: opts.tokenProvider,
|
|
|
|
|
|
token: opts.authChoice === "apiKey" && opts.token ? opts.token : undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
nextConfig = authResult.config;
|
2026-03-03 17:21:37 -08:00
|
|
|
|
|
|
|
|
|
|
if (authResult.agentModelOverride) {
|
|
|
|
|
|
nextConfig = applyPrimaryModel(nextConfig, authResult.agentModelOverride);
|
|
|
|
|
|
}
|
2026-02-10 20:31:02 +08:00
|
|
|
|
}
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-03-19 00:42:58 +00:00
|
|
|
|
const shouldPromptModelSelection =
|
|
|
|
|
|
authChoice !== "custom-api-key" && (authChoiceFromPrompt || authChoice === "ollama");
|
|
|
|
|
|
if (shouldPromptModelSelection) {
|
2026-01-09 23:13:25 +01:00
|
|
|
|
const modelSelection = await promptDefaultModel({
|
|
|
|
|
|
config: nextConfig,
|
|
|
|
|
|
prompter,
|
2026-03-17 13:59:44 -07:00
|
|
|
|
// For ollama, don't allow "keep current" since we may need to download the selected model
|
|
|
|
|
|
allowKeep: authChoice !== "ollama",
|
2026-01-09 23:13:25 +01:00
|
|
|
|
ignoreAllowlist: true,
|
2026-03-12 22:24:22 +00:00
|
|
|
|
includeProviderPluginSetups: true,
|
2026-03-15 10:29:31 -07:00
|
|
|
|
preferredProvider: await resolvePreferredProviderForAuthChoice({
|
2026-03-12 22:24:22 +00:00
|
|
|
|
choice: authChoice,
|
|
|
|
|
|
config: nextConfig,
|
|
|
|
|
|
workspaceDir,
|
|
|
|
|
|
}),
|
|
|
|
|
|
workspaceDir,
|
|
|
|
|
|
runtime,
|
2026-01-09 23:13:25 +01:00
|
|
|
|
});
|
2026-02-09 10:20:45 +00:00
|
|
|
|
if (modelSelection.config) {
|
|
|
|
|
|
nextConfig = modelSelection.config;
|
|
|
|
|
|
}
|
2026-01-09 23:13:25 +01:00
|
|
|
|
if (modelSelection.model) {
|
|
|
|
|
|
nextConfig = applyPrimaryModel(nextConfig, modelSelection.model);
|
|
|
|
|
|
}
|
2026-01-09 22:01:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-06 02:48:53 +01:00
|
|
|
|
await warnIfModelConfigLooksOff(nextConfig, prompter);
|
|
|
|
|
|
|
2026-03-15 21:39:23 -07:00
|
|
|
|
const { configureGatewayForSetup } = await import("./setup.gateway-config.js");
|
|
|
|
|
|
const gateway = await configureGatewayForSetup({
|
2026-01-14 05:40:10 +00:00
|
|
|
|
flow,
|
|
|
|
|
|
baseConfig,
|
|
|
|
|
|
nextConfig,
|
|
|
|
|
|
localPort,
|
|
|
|
|
|
quickstartGateway,
|
2026-03-02 20:58:20 -06:00
|
|
|
|
secretInputMode: opts.secretInputMode,
|
2026-01-14 05:40:10 +00:00
|
|
|
|
prompter,
|
|
|
|
|
|
runtime,
|
|
|
|
|
|
});
|
|
|
|
|
|
nextConfig = gateway.nextConfig;
|
|
|
|
|
|
const settings = gateway.settings;
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-01-13 06:16:43 +00:00
|
|
|
|
if (opts.skipChannels ?? opts.skipProviders) {
|
|
|
|
|
|
await prompter.note("Skipping channel setup.", "Channels");
|
2026-01-09 22:16:17 +01:00
|
|
|
|
} else {
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const { listChannelPlugins } = await import("../channels/plugins/index.js");
|
|
|
|
|
|
const { setupChannels } = await import("../commands/onboard-channels.js");
|
2026-01-13 06:16:43 +00:00
|
|
|
|
const quickstartAllowFromChannels =
|
2026-01-11 11:45:25 +00:00
|
|
|
|
flow === "quickstart"
|
2026-01-13 06:16:43 +00:00
|
|
|
|
? listChannelPlugins()
|
2026-01-11 11:45:25 +00:00
|
|
|
|
.filter((plugin) => plugin.meta.quickstartAllowFrom)
|
|
|
|
|
|
.map((plugin) => plugin.id)
|
|
|
|
|
|
: [];
|
2026-01-13 06:16:43 +00:00
|
|
|
|
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
|
2026-01-09 22:16:17 +01:00
|
|
|
|
allowSignalInstall: true,
|
2026-01-13 06:16:43 +00:00
|
|
|
|
forceAllowFromChannels: quickstartAllowFromChannels,
|
2026-01-09 22:16:17 +01:00
|
|
|
|
skipDmPolicyPrompt: flow === "quickstart",
|
|
|
|
|
|
skipConfirm: flow === "quickstart",
|
|
|
|
|
|
quickstartDefaults: flow === "quickstart",
|
2026-03-02 20:58:20 -06:00
|
|
|
|
secretInputMode: opts.secretInputMode,
|
2026-01-09 22:16:17 +01:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
|
|
|
|
|
await writeConfigFile(nextConfig);
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const { logConfigUpdated } = await import("../config/logging.js");
|
2026-01-23 04:01:11 +00:00
|
|
|
|
logConfigUpdated(runtime);
|
2026-02-15 19:18:07 +00:00
|
|
|
|
await onboardHelpers.ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
2026-01-09 12:44:23 +00:00
|
|
|
|
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
2026-01-06 19:50:06 +01:00
|
|
|
|
});
|
2026-01-03 16:04:19 +01:00
|
|
|
|
|
2026-03-06 19:09:00 +00:00
|
|
|
|
if (opts.skipSearch) {
|
|
|
|
|
|
await prompter.note("Skipping search setup.", "Search");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const { setupSearch } = await import("../commands/onboard-search.js");
|
|
|
|
|
|
nextConfig = await setupSearch(nextConfig, runtime, prompter, {
|
|
|
|
|
|
quickstartDefaults: flow === "quickstart",
|
|
|
|
|
|
secretInputMode: opts.secretInputMode,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 22:16:17 +01:00
|
|
|
|
if (opts.skipSkills) {
|
|
|
|
|
|
await prompter.note("Skipping skills setup.", "Skills");
|
|
|
|
|
|
} else {
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const { setupSkills } = await import("../commands/onboard-skills.js");
|
2026-01-09 22:16:17 +01:00
|
|
|
|
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
|
|
|
|
|
|
}
|
2026-01-17 01:31:39 +00:00
|
|
|
|
|
2026-01-17 07:32:50 +00:00
|
|
|
|
// Setup hooks (session memory on /new)
|
2026-02-15 19:18:07 +00:00
|
|
|
|
const { setupInternalHooks } = await import("../commands/onboard-hooks.js");
|
2026-01-17 01:31:39 +00:00
|
|
|
|
nextConfig = await setupInternalHooks(nextConfig, runtime, prompter);
|
|
|
|
|
|
|
2026-02-15 19:18:07 +00:00
|
|
|
|
nextConfig = onboardHelpers.applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
2026-01-03 16:04:19 +01:00
|
|
|
|
await writeConfigFile(nextConfig);
|
|
|
|
|
|
|
2026-03-15 21:39:23 -07:00
|
|
|
|
const { finalizeSetupWizard } = await import("./setup.finalize.js");
|
|
|
|
|
|
const { launchedTui } = await finalizeSetupWizard({
|
2026-01-14 05:40:10 +00:00
|
|
|
|
flow,
|
|
|
|
|
|
opts,
|
|
|
|
|
|
baseConfig,
|
|
|
|
|
|
nextConfig,
|
|
|
|
|
|
workspaceDir,
|
|
|
|
|
|
settings,
|
|
|
|
|
|
prompter,
|
|
|
|
|
|
runtime,
|
2026-01-09 09:59:58 +01:00
|
|
|
|
});
|
2026-02-03 06:11:11 +00:00
|
|
|
|
if (launchedTui) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-03 16:04:19 +01:00
|
|
|
|
}
|