openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,364 lines • 55.2 kB
JavaScript
import { g as readStringValue, l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { r as defaultRuntime, t as ExitError } from "./runtime-CF2WjnNZ.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.js";
import { l as readConfigFileSnapshotForWrite, n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { D as tryResolveLegacyCompatibilityAgentId, b as resolveMutableAgentEntry, o as listAgentIds, w as tryResolveAmbientOwnerAgentId } from "./agent-scope-config-DcbEhP0R.js";
import { _ as resolveGatewayPort, t as CONFIG_PATH } from "./paths-D2sRr1a_.js";
import { t as inheritLegacyDefaultAgentId } from "./legacy.default-agent-owner-BGwEdQRe.js";
import { d as isSecretRef, p as isValidEnvSecretRefId } from "./types.secrets-kC0nOetj.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { u as resolveDefaultSecretProviderAlias } from "./ref-contract-D92DqQ-r.js";
import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js";
import { o as resolvePluginContributionOwners } from "./plugin-registry-contributions-BwuDvm89.js";
import "./plugin-registry-DD_0nL_t.js";
import { o as resolveAgentEffectiveModelPrimary } from "./agent-scope-DbtJyKUL.js";
import { r as listChatChannels } from "./chat-meta-tsXGPguN.js";
import { v as parseIpAddressOrCidr } from "./ip-BkT2Is0E.js";
import "./config-Cs0XXL3x.js";
import { n as gatewayInstallErrorHint, t as buildGatewayInstallPlan } from "./daemon-install-helpers-w30vR2Jt.js";
import { n as GATEWAY_DAEMON_RUNTIME_OPTIONS, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-CdTwKGNX.js";
import { t as resolveGatewayInstallToken } from "./gateway-install-token-09efDYo7.js";
import { a as isLoopbackAddress, d as isTrustedProxyAddress } from "./net-DbNPs6Xm.js";
import { n as findTailscaleBinary } from "./tailscale-Boe4Jx-9.js";
import { t as randomToken } from "./random-token-B1woZa_H.js";
import { s as isNonFatalSystemdInstallProbeError } from "./systemd-exec-C9fneC4I.js";
import { o as resolveGatewayService, t as describeGatewayServiceRestart } from "./service-Cc6NX6Jm.js";
import "./systemd-C1W5NZTY.js";
import { o as formatPortRangeHint } from "./error-format-Cae7EwnT.js";
import { t as parsePort } from "./parse-port-Dw2bUWKg.js";
import { r as isTerminalInteractive } from "./terminal-interactivity-DXUXAq5U.js";
import { r as resolveGatewayProbeAuthSafeWithSecretInputs } from "./probe-auth-DiKkHhJQ.js";
import { t as resolveGatewayStartupTiming } from "./gateway-startup-timing-D9NqKiRl.js";
import { r as resolveLocalControlUiProbeLinks, t as resolveAdvertisedControlUiLinks } from "./control-ui-links-BY4F5DYV.js";
import { t as formatWindowsGatewayFirewallGuidance } from "./windows-gateway-firewall-diagnostics-CJp45O42.js";
import { r as ensureAuthProfileStore } from "./store-F1B2duCT.js";
import "./auth-profiles-BdUEhE7u.js";
import { t as describeCodexNativeWebSearch } from "./codex-native-web-search.shared-CvCOr9VP.js";
import { t as note } from "./note-DqHk3fA1.js";
import { n as t } from "./i18n-hynzGFbD.js";
import { r as withProgress } from "./progress-DSeqxbrQ.js";
import { n as WizardCancelledError } from "./prompts-DLsO8MlU.js";
import { a as guardCancel, d as summarizeExistingConfig, f as validateGatewayPasswordInput, n as applyWizardMetadata, p as waitForGatewayReachable, s as normalizeGatewayTokenInput, t as DEFAULT_WORKSPACE, u as probeGatewayReachable } from "./onboard-helpers-BDY-6Z-t.js";
import { i as ensureOnboardingAgentWorkspace, n as applyOnboardingPrimaryModel, r as applyOnboardingWorkspace, s as resolveOnboardingAgentTarget, t as applyAgentModelDefaults } from "./onboard-agent-target-CNKxVAZK.js";
import { t as createClackPrompter } from "./clack-prompter-BSpWH_ak.js";
import { r as logConfigUpdated } from "./logging-Idqd9ylL.js";
import { t as commitConfigWithPendingPluginInstalls } from "./install-record-commit-C4pioTsf.js";
import { a as outro, c as select, i as intro, l as text, n as CONFIGURE_WIZARD_SECTIONS, o as parseConfigureWizardSections, r as confirm, s as password, t as CONFIGURE_SECTION_OPTIONS } from "./configure.shared-D77XM94J.js";
import { i as setupChannels, n as createChannelSetupTransaction } from "./onboard-channels-B4-ZMnMR.js";
import { n as promptAuthChoiceGrouped } from "./auth-choice-prompt-DhFOt_H7.js";
import { i as applyAuthChoice } from "./auth-choice-DY18kI3l.js";
import { t as resolvePreferredProviderForAuthChoice } from "./provider-auth-choice-preference-DxhJufZS.js";
import { c as writeWizardConfigFile } from "./setup.shared-BnC_pCYr.js";
import { t as ensureSystemdUserLingerInteractive } from "./systemd-linger-5WlVACdb.js";
import { n as loadStaticManifestCatalogRowsForList } from "./list.manifest-catalog-CunsKb7O.js";
import { i as promptModelAllowlist, n as applyModelFallbacksFromSelection, r as promptDefaultModel, t as applyModelAllowlist } from "./model-picker-BLeF-ZH5.js";
import { t as promptCustomApiConfig } from "./onboard-custom-DnO-G8lT.js";
import { t as validateDottedDecimalIPv4Input } from "./ipv4-DSCxlk4V.js";
import { i as maybeAddTailnetOriginToControlUiAllowedOrigins, n as TAILSCALE_EXPOSURE_OPTIONS, r as TAILSCALE_MISSING_BIN_NOTE_LINES, t as TAILSCALE_DOCS_LINES } from "./gateway-config-prompts.shared-Ds1yKtWH.js";
import { i as formatHealthCheckFailure } from "./health-format-BptmWUeD.js";
import { a as healthCommandNonExiting } from "./health-CmrSgBb8.js";
import { t as promptRemoteGatewayConfig } from "./onboard-remote-BR-mUULq.js";
import { t as setupSkills } from "./onboard-skills-CLNNehPz.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/commands/configure.channels.ts
const RESERVED_CHANNEL_CONFIG_KEYS = /* @__PURE__ */ new Set(["defaults", "modelByChannel"]);
const DONE_VALUE = { kind: "done" };
function listConfiguredChannelRemovalChoices(cfg) {
const channels = cfg.channels;
if (!channels) return [];
const labelsById = new Map(listChatChannels().map((meta) => [meta.id, formatChannelRemovalLabel(meta.label, meta.id)]));
return Object.keys(channels).filter((id) => !RESERVED_CHANNEL_CONFIG_KEYS.has(id)).filter((id) => !isBlockedObjectKey(id)).map((id) => ({
id,
label: labelsById.get(id) ?? formatUnknownChannelRemovalLabel(id)
})).toSorted(compareChannelRemovalChoices);
}
function formatChannelRemovalLabel(label, fallback) {
return sanitizeTerminalText(label) || formatUnknownChannelRemovalLabel(fallback);
}
function formatUnknownChannelRemovalLabel(id) {
return sanitizeTerminalText(id) || "<invalid channel key>";
}
function compareChannelRemovalChoices(left, right) {
return left.label.localeCompare(right.label, void 0, {
numeric: true,
sensitivity: "base"
}) || left.id.localeCompare(right.id, void 0, {
numeric: true,
sensitivity: "base"
});
}
/** Prompt for configured channel sections to remove from openclaw.json. */
async function removeChannelConfigWizard(cfg, runtime) {
const next = { ...cfg };
while (true) {
const configured = listConfiguredChannelRemovalChoices(next);
if (configured.length === 0) {
note(["No channel config found in openclaw.json.", `Tip: \`${formatCliCommand("openclaw channels status")}\` shows what is configured and enabled.`].join("\n"), "Remove channel");
return next;
}
const channelOptions = configured.map((meta) => ({
value: {
kind: "channel",
id: meta.id
},
label: meta.label,
hint: "Deletes tokens + settings from config (credentials stay on disk)"
}));
const doneOption = {
value: DONE_VALUE,
label: "Done"
};
const options = [...channelOptions, doneOption];
const choice = guardCancel(await select({
message: "Remove which channel config?",
options
}), runtime, 1);
if (choice.kind === "done") return next;
const channel = choice.id;
const label = configured.find((entry) => entry.id === channel)?.label ?? channel;
if (!guardCancel(await confirm({
message: `Delete ${label} configuration from ${shortenHomePath(CONFIG_PATH)}?`,
initialValue: false
}), runtime, 1)) continue;
const nextChannels = { ...next.channels };
delete nextChannels[channel];
if (Object.keys(nextChannels).length) next.channels = nextChannels;
else delete next.channels;
note([`${label} selected for removal from config.`, "Note: credentials/sessions on disk are unchanged."].join("\n"), "Channel removal");
}
}
//#endregion
//#region src/commands/configure.daemon.ts
/** Prompt to install, reinstall, restart, or skip the local Gateway service. */
async function maybeInstallDaemon(params) {
const service = resolveGatewayService();
let loaded;
try {
loaded = await service.isLoaded({ env: process.env });
} catch (error) {
if (!isNonFatalSystemdInstallProbeError(error)) throw error;
loaded = false;
}
let shouldCheckLinger = false;
let shouldInstall = true;
let daemonRuntime = params.daemonRuntime ?? "node";
if (loaded) {
const action = guardCancel(await select({
message: "Gateway service already installed",
options: [
{
value: "restart",
label: "Restart"
},
{
value: "reinstall",
label: "Reinstall"
},
{
value: "skip",
label: "Skip"
}
]
}), params.runtime, 1);
if (action === "restart") {
await withProgress({
label: "Gateway service",
indeterminate: true,
delayMs: 0
}, async (progress) => {
progress.setLabel("Restarting Gateway service…");
const restartResult = await service.restart({
env: process.env,
stdout: process.stdout
});
progress.setLabel(describeGatewayServiceRestart("Gateway", restartResult).progressMessage);
});
shouldCheckLinger = true;
shouldInstall = false;
}
if (action === "skip") return "skipped";
}
if (shouldInstall) {
let installError = null;
if (!params.daemonRuntime) {
if (GATEWAY_DAEMON_RUNTIME_OPTIONS.length === 1) daemonRuntime = GATEWAY_DAEMON_RUNTIME_OPTIONS[0]?.value ?? "node";
else daemonRuntime = guardCancel(await select({
message: "Gateway service runtime",
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME
}), params.runtime, 1);
}
await withProgress({
label: "Gateway service",
indeterminate: true,
delayMs: 0
}, async (progress) => {
progress.setLabel("Preparing Gateway service…");
const cfg = getRuntimeConfig();
const tokenResolution = await resolveGatewayInstallToken({
config: cfg,
env: process.env
});
for (const warning of tokenResolution.warnings) note(warning, "Gateway");
if (tokenResolution.unavailableReason) {
installError = [
"Gateway install blocked:",
tokenResolution.unavailableReason,
"Fix gateway auth config/token input and rerun configure."
].join(" ");
progress.setLabel("Gateway service install blocked.");
return;
}
const existingCommand = await service.readCommand(process.env).catch(() => null);
const { programArguments, workingDirectory, environment, environmentValueSources } = await buildGatewayInstallPlan({
env: process.env,
port: params.port,
runtime: daemonRuntime,
existingCommand,
warn: (message, title) => note(message, title),
config: cfg
});
progress.setLabel("Installing Gateway service…");
try {
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment,
environmentValueSources
});
progress.setLabel("Gateway service installed.");
} catch (err) {
installError = formatErrorMessage(err);
progress.setLabel("Gateway service install failed.");
}
});
if (installError) {
note("Gateway service install failed: ".concat(installError), "Gateway");
note(gatewayInstallErrorHint(), "Gateway");
return "failed";
}
shouldCheckLinger = true;
}
if (shouldCheckLinger) await ensureSystemdUserLingerInteractive({
runtime: params.runtime,
prompter: {
confirm: async (p) => guardCancel(await confirm(p), params.runtime, 1),
note
},
reason: "Linux installs use a systemd user service. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.",
requireConfirm: true
});
return "succeeded";
}
//#endregion
//#region src/commands/configure.gateway-auth.ts
/** Reject undefined, empty, and common JS string-coercion artifacts for token auth. */
function sanitizeTokenValue(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
if (!trimmed || trimmed === "undefined" || trimmed === "null") return;
return trimmed;
}
async function resolveProviderChoiceModelPrompt(params) {
const { resolvePluginProviders, resolveProviderPluginChoice } = await import("./provider-auth-choice.runtime.js");
const resolved = resolveProviderPluginChoice({
providers: resolvePluginProviders({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
mode: "setup"
}),
choice: params.authChoice
});
const wizard = resolved?.provider.wizard?.setup;
if (!wizard) return resolved?.provider.id ? { provider: resolved.provider.id } : void 0;
return {
provider: resolved.provider.id,
...wizard.modelAllowlist,
...wizard.modelSelection?.promptWhenAuthChoiceProvided === true ? { loadCatalog: true } : {}
};
}
function hasConfiguredProviderModels(cfg, provider) {
if (!provider) return false;
if ((cfg.models?.providers?.[provider]?.models?.length ?? 0) > 0) return true;
const providerPrefix = `${provider}/`;
return Object.keys(cfg.agents?.defaults?.models ?? {}).some((key) => key.trim().startsWith(providerPrefix));
}
function hasStaticManifestCatalogRows(cfg, provider) {
if (!provider) return false;
return loadStaticManifestCatalogRowsForList({
cfg,
providerFilter: provider
}).length > 0;
}
function listConfiguredModelProviders(cfg) {
return Object.entries(cfg.models?.providers ?? {}).filter(([, provider]) => (provider.models?.length ?? 0) > 0).map(([provider]) => provider);
}
function resolveSingleConfiguredProvider(cfg) {
const configuredProviders = listConfiguredModelProviders(cfg);
return configuredProviders.length === 1 ? configuredProviders[0] : void 0;
}
function resolveProviderFromModelRef(model) {
const trimmed = model?.trim();
const slashIndex = trimmed?.indexOf("/") ?? -1;
return slashIndex > 0 ? trimmed?.slice(0, slashIndex) : void 0;
}
function resolveCanonicalOpenAISelectionForLegacyCodexPrimary(cfg, target, selectedModels) {
const currentModel = resolveMutableAgentEntry(cfg, target.agentId)?.model ?? cfg.agents?.defaults?.model;
const primary = typeof currentModel === "string" ? currentModel.trim() : currentModel && typeof currentModel === "object" && typeof currentModel.primary === "string" ? currentModel.primary.trim() : void 0;
const modelId = primary?.startsWith("codex/") ? primary.slice(6).trim() : "";
if (!modelId) return;
const canonical = `openai/${modelId}`;
return selectedModels.find((model) => model.trim() === canonical);
}
function resolveConfiguredProviderFromAuthChange(params) {
if (hasConfiguredProviderModels(params.after, params.preferredProvider)) return params.preferredProvider;
const beforeProviders = params.before.models?.providers ?? {};
const configuredProviders = listConfiguredModelProviders(params.after);
const changedProviders = configuredProviders.filter((provider) => {
const beforeCount = beforeProviders[provider]?.models?.length ?? 0;
return (params.after.models?.providers?.[provider]?.models?.length ?? 0) > beforeCount;
});
if (changedProviders.length === 1) return changedProviders[0];
return params.preferredProvider ?? (configuredProviders.length === 1 ? configuredProviders[0] : void 0);
}
/** Preserve unrelated auth policy; replace mode-owned credentials and proxy settings. */
function buildGatewayAuthConfig(params) {
const base = { ...params.existing };
delete base.token;
delete base.password;
delete base.trustedProxy;
if (params.mode === "token") {
if (isSecretRef(params.token)) return {
...base,
mode: "token",
token: params.token
};
const token = sanitizeTokenValue(params.token) ?? randomToken();
return {
...base,
mode: "token",
token
};
}
if (params.mode === "password") {
const password = params.password?.trim();
return {
...base,
mode: "password",
...password && { password }
};
}
if (params.mode === "trusted-proxy") {
if (!params.trustedProxy) throw new Error(`trustedProxy config is required when mode is trusted-proxy. Run ${formatCliCommand("openclaw configure --section gateway")} to configure Gateway auth interactively.`);
return {
...base,
mode: "trusted-proxy",
trustedProxy: params.trustedProxy
};
}
return base;
}
/** Prompt for model provider credentials and explicit default model policy settings. */
async function promptAuthConfig(cfg, runtime, prompter, target = resolveOnboardingAgentTarget(cfg)) {
let next = cfg;
let authChoice = "skip";
let preferredProvider;
while (true) {
authChoice = await promptAuthChoiceGrouped({
prompter,
store: ensureAuthProfileStore(target.agentDir, { allowKeychainPrompt: false }),
includeSkip: true,
config: next
});
preferredProvider = authChoice === "skip" ? void 0 : await resolvePreferredProviderForAuthChoice({
choice: authChoice,
config: next
});
if (authChoice === "custom-api-key") {
next = (await promptCustomApiConfig({
prompter,
runtime,
config: next,
target,
setAsPrimary: !resolveAgentEffectiveModelPrimary(next, target.agentId)
})).config;
break;
}
if (authChoice === "skip") {
const modelSelection = await promptDefaultModel({
config: next,
prompter,
allowKeep: true,
ignoreAllowlist: true,
includeProviderPluginSetups: false,
loadCatalog: true,
browseCatalogOnDemand: true,
preferredProvider,
agentId: target.agentId,
agentDir: target.agentDir,
workspaceDir: target.workspaceDir,
runtime
});
if (modelSelection.config) next = modelSelection.config;
if (modelSelection.model) {
next = applyOnboardingPrimaryModel(next, target, modelSelection.model);
preferredProvider = resolveProviderFromModelRef(modelSelection.model) ?? preferredProvider;
}
break;
}
const beforeAuthConfig = next;
const applied = await applyAuthChoice({
authChoice,
config: next,
prompter,
runtime,
agentId: target.agentId,
agentDir: target.agentDir,
setDefaultModel: false,
preserveExistingDefaultModel: true
});
next = applied.config;
if (applied.agentModelOverride && !resolveAgentEffectiveModelPrimary(beforeAuthConfig, target.agentId)) next = applyOnboardingPrimaryModel(next, target, applied.agentModelOverride);
preferredProvider = resolveConfiguredProviderFromAuthChange({
before: beforeAuthConfig,
after: next,
preferredProvider
});
if (applied.retrySelection) continue;
break;
}
if (authChoice !== "custom-api-key") {
const modelPrompt = await resolveProviderChoiceModelPrompt({
authChoice,
config: next,
workspaceDir: target.workspaceDir,
env: process.env
});
const promptProvider = modelPrompt?.provider ?? preferredProvider ?? resolveSingleConfiguredProvider(next);
const hasPromptProviderConfiguredModels = hasConfiguredProviderModels(next, promptProvider);
const hasPromptProviderStaticManifestRows = hasStaticManifestCatalogRows(next, promptProvider);
const shouldLoadModelCatalog = modelPrompt?.loadCatalog ?? (hasPromptProviderConfiguredModels || hasPromptProviderStaticManifestRows);
const useProviderScopedCatalog = Boolean(promptProvider && shouldLoadModelCatalog && (modelPrompt?.loadCatalog === true || hasPromptProviderConfiguredModels));
const allowlistSelection = await promptModelAllowlist({
config: next,
prompter,
agentId: target.agentId,
agentDir: target.agentDir,
workspaceDir: target.workspaceDir,
env: process.env,
allowedKeys: modelPrompt?.allowedKeys,
initialSelections: modelPrompt?.initialSelections,
message: modelPrompt?.message,
preferredProvider: promptProvider,
providerScopedCatalog: useProviderScopedCatalog,
loadCatalog: shouldLoadModelCatalog
});
if (allowlistSelection.models) {
const selectedModels = allowlistSelection.models;
const canonicalPrimary = resolveCanonicalOpenAISelectionForLegacyCodexPrimary(next, target, selectedModels);
if (canonicalPrimary) next = applyOnboardingPrimaryModel(next, target, canonicalPrimary);
next = applyAgentModelDefaults(next, target, (projected) => applyModelAllowlist(applyModelFallbacksFromSelection(projected, selectedModels, { scopeKeys: allowlistSelection.scopeKeys }), selectedModels, { scopeKeys: allowlistSelection.scopeKeys }));
}
}
return next;
}
//#endregion
//#region src/commands/configure.gateway.ts
function validateGatewayPortInput$1(value) {
if (parsePort(value) === null) return formatPortRangeHint();
}
/** Prompt for local Gateway network/auth settings and return config plus call token. */
async function promptGatewayConfig(cfg, runtime) {
const portRaw = guardCancel(await text({
message: "Gateway port",
initialValue: String(resolveGatewayPort(cfg)),
validate: validateGatewayPortInput$1
}), runtime, 1);
const port = parsePort(portRaw) ?? resolveGatewayPort(cfg);
let bind = guardCancel(await select({
message: "Gateway bind mode",
options: [
{
value: "loopback",
label: "Loopback (Local only)",
hint: "Bind to 127.0.0.1 - secure, local-only access"
},
{
value: "tailnet",
label: "Tailnet (Tailscale IP)",
hint: "Bind to your Tailscale IP plus local loopback"
},
{
value: "auto",
label: "Auto (Loopback → LAN)",
hint: "Prefer loopback; fall back to all interfaces if unavailable"
},
{
value: "lan",
label: "LAN (All interfaces)",
hint: "Bind to 0.0.0.0 - accessible from anywhere on your network"
},
{
value: "custom",
label: "Custom IP",
hint: "Specific IPv4s also bind 127.0.0.1"
}
]
}), runtime, 1);
let customBindHost;
if (bind === "custom") {
const input = guardCancel(await text({
message: "Custom IP address",
placeholder: "192.168.1.100",
validate: validateDottedDecimalIPv4Input
}), runtime, 1);
customBindHost = readStringValue(input);
}
let authMode = guardCancel(await select({
message: "Gateway access protection",
options: [
{
value: "token",
label: "Token (recommended)",
hint: "Recommended default"
},
{
value: "password",
label: "Password"
},
{
value: "trusted-proxy",
label: "Trusted Proxy",
hint: "Behind reverse proxy (Pomerium, Caddy, Traefik, etc.)"
}
],
initialValue: "token"
}), runtime, 1);
let tailscaleMode = guardCancel(await select({
message: "Tailscale exposure",
options: [...TAILSCALE_EXPOSURE_OPTIONS]
}), runtime, 1);
let tailscaleBin = null;
if (tailscaleMode !== "off") {
tailscaleBin = await findTailscaleBinary();
if (!tailscaleBin) note(TAILSCALE_MISSING_BIN_NOTE_LINES.join("\n"), "Tailscale Warning");
}
if (tailscaleMode !== "off") note(TAILSCALE_DOCS_LINES.join("\n"), "Tailscale");
if (tailscaleMode !== "off" && bind !== "loopback") {
note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note");
bind = "loopback";
}
if (tailscaleMode === "funnel" && authMode !== "password") {
note("Tailscale funnel requires password auth.", "Note");
authMode = "password";
}
if (authMode === "trusted-proxy" && tailscaleMode !== "off") {
note("Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.", "Note");
tailscaleMode = "off";
}
let gatewayToken;
let gatewayTokenForCalls;
let gatewayPassword;
let trustedProxyConfig;
let trustedProxies;
let next = cfg;
if (authMode === "token") {
if (guardCancel(await select({
message: "Gateway token source",
options: [{
value: "plaintext",
label: "Generate/store plaintext token",
hint: "Default"
}, {
value: "ref",
label: "Use SecretRef",
hint: "Store an env-backed reference instead of plaintext"
}],
initialValue: "plaintext"
}), runtime, 1) === "ref") {
const envVar = guardCancel(await text({
message: "Gateway token env var",
initialValue: "OPENCLAW_GATEWAY_TOKEN",
placeholder: "OPENCLAW_GATEWAY_TOKEN",
validate: (value) => {
const candidate = normalizeOptionalString(value) ?? "";
if (!isValidEnvSecretRefId(candidate)) return "Use an env var name like OPENCLAW_GATEWAY_TOKEN.";
if (!process.env[candidate]?.trim()) return `Environment variable "${candidate}" is missing or empty in this session.`;
}
}), runtime, 1);
const envVarName = normalizeOptionalString(envVar) ?? "";
gatewayToken = {
source: "env",
provider: resolveDefaultSecretProviderAlias(cfg, "env", { preferFirstProviderForSource: true }),
id: envVarName
};
note(`Validated ${envVarName}. OpenClaw will store a token SecretRef.`, "Gateway token");
} else {
const tokenInput = guardCancel(await password({ message: "Gateway token (blank to generate)" }), runtime, 1);
gatewayTokenForCalls = normalizeGatewayTokenInput(tokenInput) || randomToken();
gatewayToken = gatewayTokenForCalls;
}
}
if (authMode === "password") {
const passwordInput = guardCancel(await password({
message: "Gateway password",
validate: validateGatewayPasswordInput
}), runtime, 1);
gatewayPassword = normalizeOptionalString(passwordInput) ?? "";
}
if (authMode === "trusted-proxy") {
note([
"Trusted proxy mode: OpenClaw trusts user identity from a reverse proxy.",
"The proxy must authenticate users and pass identity via headers.",
"Only requests from specified proxy IPs will be trusted.",
"",
"Common use cases: Pomerium, Caddy + OAuth, Traefik + forward auth",
"Docs: https://docs.openclaw.ai/gateway/trusted-proxy-auth"
].join("\n"), "Trusted Proxy Auth");
const userHeader = guardCancel(await text({
message: "Header containing user identity",
placeholder: "x-forwarded-user",
initialValue: "x-forwarded-user",
validate: (value) => value?.trim() ? void 0 : "User header is required"
}), runtime, 1);
const requiredHeadersRaw = guardCancel(await text({
message: "Required headers (comma-separated, optional)",
placeholder: "x-forwarded-proto,x-forwarded-host"
}), runtime, 1);
const requiredHeaders = requiredHeadersRaw ? normalizeStringEntries(requiredHeadersRaw.split(",")) : [];
const allowUsersRaw = guardCancel(await text({
message: "Allowed users (comma-separated, blank = all authenticated users)",
placeholder: "nick@example.com,admin@company.com"
}), runtime, 1);
const allowUsers = allowUsersRaw ? normalizeStringEntries(allowUsersRaw.split(",")) : [];
const trustedProxiesRaw = guardCancel(await text({
message: "Trusted proxy IPs (comma-separated)",
placeholder: "10.0.1.10,192.168.1.5",
validate: (value) => (value ?? "").split(",").every((address) => parseIpAddressOrCidr(address)) ? void 0 : "Enter comma-separated IPv4 or IPv6 addresses or CIDR ranges (e.g. 10.0.0.1, ::1, 10.0.0.0/24); no empty entries."
}), runtime, 1);
trustedProxies = normalizeStringEntries(trustedProxiesRaw.split(","));
const existingProxy = cfg.gateway?.auth?.mode === "trusted-proxy" ? cfg.gateway.auth.trustedProxy : void 0;
let allowLoopback = existingProxy?.allowLoopback;
if (trustedProxies.some((address) => {
return [
parseIpAddressOrCidr(address)?.[0].toString(),
"127.0.0.1",
"::1"
].some((peer) => isLoopbackAddress(peer) && isTrustedProxyAddress(peer, [address]));
})) {
const title = t("wizard.gateway.trustedProxyLoopbackTitle");
note(t("wizard.gateway.trustedProxyLoopbackWarning"), title);
allowLoopback = guardCancel(await confirm({
message: t("wizard.gateway.trustedProxyAllowLoopback"),
initialValue: allowLoopback === true
}), runtime, 1) || void 0;
if (!allowLoopback) note(t("wizard.gateway.trustedProxyLoopbackRefused"), title);
}
trustedProxyConfig = {
...existingProxy,
userHeader: normalizeOptionalString(userHeader) ?? "",
requiredHeaders: requiredHeaders.length > 0 ? requiredHeaders : void 0,
allowUsers: allowUsers.length > 0 ? allowUsers : void 0,
allowLoopback
};
}
const authConfig = buildGatewayAuthConfig({
existing: next.gateway?.auth,
mode: authMode,
token: gatewayToken,
password: gatewayPassword,
trustedProxy: trustedProxyConfig
});
next = {
...next,
gateway: {
...next.gateway,
mode: "local",
port,
bind,
auth: authConfig,
...customBindHost && { customBindHost },
...trustedProxies && { trustedProxies },
tailscale: {
...next.gateway?.tailscale,
mode: tailscaleMode
}
}
};
next = await maybeAddTailnetOriginToControlUiAllowedOrigins({
config: next,
tailscaleMode,
tailscaleBin
});
return {
config: next,
port,
token: gatewayTokenForCalls
};
}
//#endregion
//#region src/commands/configure.wizard.ts
const GATEWAY_HINT_PROBE_TIMEOUT_MS = 300;
const setupPluginConfigModuleLoader = createLazyImportLoader(() => import("./setup.plugin-config-BNGcGkQh.js"));
function validateGatewayPortInput(value) {
if (parsePort(value) === null) return formatPortRangeHint();
}
function loadSetupPluginConfigModule() {
return setupPluginConfigModuleLoader.load();
}
async function runGatewayHealthCheck(params) {
const localLinks = resolveLocalControlUiProbeLinks({
bind: params.cfg.gateway?.bind ?? "loopback",
port: params.port,
customBindHost: params.cfg.gateway?.customBindHost,
basePath: void 0,
tlsEnabled: params.cfg.gateway?.tls?.enabled === true
});
const remoteUrl = params.cfg.gateway?.remote?.url?.trim();
const remoteWsUrl = params.cfg.gateway?.mode === "remote" ? remoteUrl : void 0;
const probeMode = remoteWsUrl ? "remote" : "local";
const wsUrl = remoteWsUrl ?? localLinks.wsUrl;
let token;
let password;
if (probeMode === "remote") {
const remoteProbeAuth = await resolveGatewayProbeAuthSafeWithSecretInputs({
cfg: params.cfg,
env: process.env,
mode: "remote"
});
if (remoteProbeAuth.warning) {
const hasResolvedRemoteAuth = Boolean(remoteProbeAuth.auth.token || remoteProbeAuth.auth.password);
note([
"Could not resolve remote gateway SecretRef for health check.",
remoteProbeAuth.warning,
...hasResolvedRemoteAuth ? ["Continuing with the other configured remote credential."] : ["Health check skipped to avoid falling back to ambient credentials.", `Fix the SecretRef, then run \`${formatCliCommand("openclaw health")}\` again.`]
].join("\n"), "Gateway auth");
if (!hasResolvedRemoteAuth) return "skipped";
}
({token, password} = remoteProbeAuth.auth);
} else {
const localProbeAuth = await resolveGatewayProbeAuthSafeWithSecretInputs({
cfg: params.cfg,
env: process.env,
mode: "local",
localPrecedence: "env-first"
});
if (localProbeAuth.warning) {
note([
"Could not resolve local gateway SecretRef for health check.",
localProbeAuth.warning,
"Health check skipped to avoid falling back to ambient credentials.",
`Fix the SecretRef, then run \`${formatCliCommand("openclaw health")}\` again.`
].join("\n"), "Gateway auth");
return "skipped";
}
({token, password} = localProbeAuth.auth);
}
try {
const gatewayProbe = await waitForGatewayReachable({
url: wsUrl,
...probeMode === "remote" ? {
config: params.cfg,
originScopedDeviceAuth: true
} : {},
token,
password,
...params.daemonSetupOutcome === "succeeded" ? resolveGatewayStartupTiming() : { deadlineMs: 15e3 }
});
if (!gatewayProbe.ok) throw new Error(gatewayProbe.detail ?? `gateway did not become reachable at ${wsUrl}`);
await healthCommandNonExiting({
json: false,
timeoutMs: 1e4,
config: params.cfg,
token,
password,
...probeMode === "local" ? { localPortOverride: params.port } : { ignoreEnvUrlOverride: true }
}, params.runtime);
} catch (err) {
if (!(err instanceof ExitError)) params.runtime.error(formatHealthCheckFailure(err));
note([
"Docs:",
"https://docs.openclaw.ai/gateway/health",
"https://docs.openclaw.ai/gateway/troubleshooting"
].join("\n"), "Health check help");
return "failed";
}
return "succeeded";
}
async function promptConfigureSection(runtime, hasSelection) {
return guardCancel(await select({
message: "What do you want to configure?",
options: [...CONFIGURE_SECTION_OPTIONS, {
value: "__continue",
label: hasSelection ? "Done" : "Skip for now"
}],
initialValue: CONFIGURE_SECTION_OPTIONS[0]?.value
}), runtime, 1);
}
async function promptChannelMode(runtime) {
return guardCancel(await select({
message: "Channel setup",
options: [{
value: "configure",
label: "Add or update channels",
hint: "Configure accounts and disable unselected accounts"
}, {
value: "remove",
label: "Remove channel config",
hint: "Delete channel tokens/settings from openclaw.json"
}],
initialValue: "configure"
}), runtime, 1);
}
async function promptWebToolsConfig(nextConfig, runtime, prompter) {
const existingSearch = nextConfig.tools?.web?.search;
const existingFetch = nextConfig.tools?.web?.fetch;
const { isCodexNativeWebSearchRelevant } = await import("./codex-native-web-search-CGMwUpY2.js");
const hasManagedSearchProviders = resolvePluginContributionOwners({
config: nextConfig,
contribution: "contracts",
matches: "webSearchProviders"
}).length > 0;
note([
"Web search lets your agent look things up online using the `web_search` tool.",
"Codex-capable models can use native Codex web search.",
"Other models use a separate web search provider, which you can configure here.",
"Docs: https://docs.openclaw.ai/tools/web"
].join("\n"), "Web search");
const enableSearch = guardCancel(await confirm({
message: "Enable the web_search tool?",
initialValue: existingSearch?.enabled ?? hasManagedSearchProviders
}), runtime, 1);
let nextSearch = {
...existingSearch,
enabled: enableSearch
};
let workingConfig = nextConfig;
if (enableSearch) {
const codexRelevant = isCodexNativeWebSearchRelevant({ config: nextConfig });
let configureManagedProvider = true;
if (codexRelevant) {
note([
"Codex-capable models can use native Codex web search instead of a separate provider.",
"Other models need a separate web search provider.",
"If you do not choose one, OpenClaw can select a provider from available credentials; otherwise other models may not have web search.",
...describeCodexNativeWebSearch(nextConfig) ? [describeCodexNativeWebSearch(nextConfig)] : []
].join("\n"), "Codex native search");
if (guardCancel(await confirm({
message: "Enable native Codex web search for Codex-capable models?",
initialValue: existingSearch?.openaiCodex?.enabled === true
}), runtime, 1)) {
const codexMode = guardCancel(await select({
message: "Native Codex web search mode",
options: [{
value: "cached",
label: "cached (recommended)",
hint: "Uses cached web content"
}, {
value: "live",
label: "live",
hint: "Allows live external web access"
}],
initialValue: existingSearch?.openaiCodex?.mode ?? "cached"
}), runtime, 1);
nextSearch = {
...nextSearch,
openaiCodex: {
...existingSearch?.openaiCodex,
enabled: true,
mode: codexMode
}
};
configureManagedProvider = guardCancel(await confirm({
message: existingSearch?.provider ? `Change the separate web search provider (currently ${existingSearch.provider})?` : "Also configure a separate web search provider for other models?",
initialValue: Boolean(existingSearch?.provider)
}), runtime, 1);
} else nextSearch = {
...nextSearch,
openaiCodex: {
...existingSearch?.openaiCodex,
enabled: false
}
};
}
if (configureManagedProvider) {
const { resolveSearchProviderOptions, runSearchSetupFlow } = await import("./search-setup-3CsvMo3b.js");
if (resolveSearchProviderOptions(nextConfig).length === 0) {
note([
"No web search providers are currently available under this plugin policy.",
"Enable plugins or remove deny rules, then rerun configure.",
"Docs: https://docs.openclaw.ai/tools/web"
].join("\n"), "Web search");
if (nextSearch.openaiCodex?.enabled !== true) nextSearch = {
...existingSearch,
enabled: false
};
} else {
workingConfig = (await runSearchSetupFlow(workingConfig, runtime, prompter, { preserveDisabledSearchState: false })).config;
const selectedSearch = workingConfig.tools?.web?.search;
nextSearch = {
...selectedSearch,
enabled: selectedSearch?.enabled ?? (selectedSearch?.provider ? true : existingSearch?.enabled),
openaiCodex: {
...existingSearch?.openaiCodex,
...nextSearch.openaiCodex
}
};
}
}
}
note(["`web_fetch` is a separate tool for reading a specific URL.", "It does not require an API key and works independently of web search providers, including Codex."].join("\n"), "Web fetch");
const enableFetch = guardCancel(await confirm({
message: "Enable the web_fetch tool?",
initialValue: existingFetch?.enabled ?? true
}), runtime, 1);
const nextFetch = {
...workingConfig.tools?.web?.fetch,
enabled: enableFetch
};
return {
...workingConfig,
tools: {
...workingConfig.tools,
web: {
...workingConfig.tools?.web,
search: nextSearch,
fetch: nextFetch
}
}
};
}
/** Run the configure/update wizard, optionally limited to selected sections. */
async function runConfigureWizard(opts, runtime = defaultRuntime) {
try {
intro(opts.command === "update" ? "OpenClaw update wizard" : "OpenClaw configure");
const prompter = createClackPrompter();
const prepared = await readConfigFileSnapshotForWrite();
const snapshot = prepared.snapshot;
const configWriteOwnership = {
...prepared.writeOptions.assertConfigPathForWrite ? { assertConfigPathForWrite: prepared.writeOptions.assertConfigPathForWrite } : {},
expectedConfigPath: prepared.writeOptions.expectedConfigPath,
ownedConfigPathForWrite: prepared.writeOptions.ownedConfigPathForWrite
};
const currentBaseHash = snapshot.hash;
const baseConfig = snapshot.valid ? snapshot.sourceConfig ?? snapshot.config : {};
if (snapshot.exists) {
const title = snapshot.valid ? "Existing config detected" : "Invalid config";
note(summarizeExistingConfig(baseConfig), title);
if (!snapshot.valid && snapshot.issues.length > 0) note([
...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`),
"",
"Docs: https://docs.openclaw.ai/gateway/configuration"
].join("\n"), "Config issues");
if (!snapshot.valid) {
outro(`Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run configure.`);
runtime.exit(1);
return;
}
}
const selectedSections = opts.sections;
const shouldPromptGatewayRunMode = !selectedSections || selectedSections.includes("gateway") || selectedSections.includes("daemon") || selectedSections.includes("health");
const promptGatewayRunMode = async () => {
const localUrl = `ws://127.0.0.1:${resolveGatewayPort(baseConfig)}`;
const remoteUrl = normalizeOptionalString(baseConfig.gateway?.remote?.url) ?? "";
const localProbePromise = (async () => {
const localProbeAuth = await resolveGatewayProbeAuthSafeWithSecretInputs({
cfg: baseConfig,
env: process.env,
mode: "local",
localPrecedence: "env-first"
});
if (localProbeAuth.warning) return {
ok: false,
authUnavailable: true
};
return probeGatewayReachable({
url: localUrl,
token: localProbeAuth.auth.token,
password: localProbeAuth.auth.password,
timeoutMs: GATEWAY_HINT_PROBE_TIMEOUT_MS
});
})();
const remoteProbePromise = remoteUrl ? (async () => {
const remoteProbeAuth = await resolveGatewayProbeAuthSafeWithSecretInputs({
cfg: baseConfig,
env: process.env,
mode: "remote"
});
return probeGatewayReachable({
url: remoteUrl,
config: baseConfig,
originScopedDeviceAuth: true,
token: remoteProbeAuth.auth.token,
...remoteProbeAuth.auth.password ? { password: remoteProbeAuth.auth.password } : {},
timeoutMs: GATEWAY_HINT_PROBE_TIMEOUT_MS
});
})() : Promise.resolve(null);
const [localProbe, remoteProbe] = await Promise.all([localProbePromise, remoteProbePromise]);
return guardCancel(await select({
message: "Where will the Gateway run?",
options: [{
value: "local",
label: "Local (this machine)",
hint: localProbe.ok ? `Gateway reachable (${localUrl})` : "authUnavailable" in localProbe ? `Gateway auth unavailable; probe skipped (${localUrl})` : `No gateway detected (${localUrl})`
}, {
value: "remote",
label: "Remote (info-only)",
hint: !remoteUrl ? "No remote URL configured yet" : remoteProbe?.ok ? `Gateway reachable (${remoteUrl})` : `Configured but unreachable (${remoteUrl})`
}]
}), runtime, 1);
};
const mode = shouldPromptGatewayRunMode ? await promptGatewayRunMode() : "local";
const metadataMode = shouldPromptGatewayRunMode || baseConfig.gateway?.mode !== "remote" ? mode : "remote";
const shouldSkipGatewaySummary = !shouldPromptGatewayRunMode;
if (shouldPromptGatewayRunMode && mode === "remote") {
let remoteConfig = await promptRemoteGatewayConfig(baseConfig, prompter);
remoteConfig = applyWizardMetadata(remoteConfig, {
command: opts.command,
mode: metadataMode
});
remoteConfig = (await commitConfigWithPendingPluginInstalls({
nextConfig: remoteConfig,
...currentBaseHash !== void 0 ? { baseHash: currentBaseHash } : {},
writeOptions: configWriteOwnership
})).config;
logConfigUpdated(runtime);
if (selectedSections?.includes("health")) {
const healthCheckOutcome = await runGatewayHealthCheck({
cfg: remoteConfig,
runtime,
port: resolveGatewayPort(remoteConfig)
});
outro(healthCheckOutcome === "succeeded" ? "Remote gateway configured and health check completed." : healthCheckOutcome === "failed" ? "Remote gateway configured, but health check failed." : "Remote gateway configured; health check skipped.");
} else outro("Remote gateway configured.");
return;
}
let nextConfig = { ...baseConfig };
let mergeBaseConfig = structuredClone(baseConfig);
let hasPendingConfig = shouldPromptGatewayRunMode && nextConfig.gateway?.mode !== "local";
if (hasPendingConfig) nextConfig = {
...nextConfig,
gateway: {
...nextConfig.gateway,
mode: "local"
}
};
let setupAgentId;
const resolveSetupTarget = async () => {
if (nextConfig.agents?.ownership !== "explicit") inheritLegacyDefaultAgentId(baseConfig, nextConfig);
setupAgentId ??= nextConfig.agents?.ownership === "explicit" ? tryResolveAmbientOwnerAgentId(nextConfig) : tryResolveLegacyCompatibilityAgentId(nextConfig);
const agentIds = listAgentIds(nextConfig);
if (!setupAgentId && agentIds.length > 1) setupAgentId = guardCancel(await select({
message: "Which agent do you want to configure?",
options: agentIds.map((id) => ({
value: id,
label: id
}))
}), runtime, 1);
return resolveOnboardingAgentTarget(nextConfig, setupAgentId);
};
let gatewayPort = resolveGatewayPort(baseConfig);
let didPersistConfig = false;
let daemonSetupOutcome;
let healthCheckOutcome;
const channelSetup = createChannelSetupTransaction({ runtime });
const persistPendingConfig = async () => {
if (!hasPendingConfig) return;
nextConfig = applyWizardMetadata(nextConfig, {
command: opts.command,
mode: metadataMode
});
nextConfig = await channelSetup.commit(nextConfig, async (configToCommit) => {
const committedConfig = await writeWizardConfigFile(configToCommit, {
mergeBase: mergeBaseConfig,
writeOptions: configWriteOwnership
});
mergeBaseConfig = structuredClone(committedConfig);
return committedConfig;
});
hasPendingConfig = false;
didPersistConfig = true;
logConfigUpdated(runtime);
};
const configureWorkspace = async () => {
const target = await resolveSetupTarget();
const workspaceInput = guardCancel(await text({
message: "Workspace directory",
initialValue: target.workspaceDir
}), runtime, 1);
const workspaceDir = resolveUserPath(normalizeOptionalString(workspaceInput ?? "") || DEFAULT_WORKSPACE);
if (!snapshot.exists) {
const indicators = [
"MEMORY.md",
"memory",
".git"
].map((name) => path.join(workspaceDir, name));
if ((await Promise.all(indicators.map(async (candidate) => {
try {
await fs.access(candidate);
return true;
} catch {
return false;
}
}))).some(Boolean)) note([`Existing workspace detected at ${workspaceDir}`, "Existing files are preserved. Missing templates may be created, never overwritten."].join("\n"), "Existing workspace");
}
nextConfig = applyOnboardingWorkspace(nextConfig, target, workspaceDir);
await ensureOnboardingAgentWorkspace(await resolveSetupTarget(), runtime, {
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles
});
};
const configureChannelsSection = async () => {
if (await promptChannelMode(runtime) === "configure") {
const target = await resolveSetupTarget();
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
workspaceDir: target.workspaceDir,
allowDisable: true,
allowIMessageInstall: true,
allowSignalInstall: true,
deferStatusUntilSelection: true,
skipConfirm: true,
skipStatusNote: true,
onPostWriteHook: channelSetup.onPostWriteHook
});
} else nextConfig = await removeChannelConfigWizard(nextConfig, runtime);
};
const promptDaemonPort = async () => {
const portInput = guardCancel(await text({
message: "Gateway port for service install",
initialValue: String(gatewayPort),
validate: validateGatewayPortInput
}), runtime, 1);
gatewayPort = parsePort(portInput) ?? gatewayPort;
};
let didConfigureGateway = false;
const sectionActions = {
workspace: configureWorkspace,
model: async () => {
nextConfig = await promptAuthConfig(nextConfig, runtime, prompter, await resolveSetupTarget());
},
web: async () => {
nextConfig = await promptWebToolsConfig(nextConfig, runtime, prompter);
},
gateway: async () => {
const gateway = await promptGatewayConfig(nextConfig, runtime);
nextConfig = gateway.config;
gatewayPort = gateway.port;
didConfigureGateway = true;
},
channels: configureChannelsSection,
plugins: async () => {
const { configurePluginConfig } = await loadSetupPluginConfigModule();
nextConfig = await configurePluginConfig({
config: nextConfig,
prompter,
workspaceDir: (await resolveSetupTarget()).workspaceDir
});
},
skills: async () => {
nextConfig = await setupSkills(nextConfig, (await resolveSetupTarget()).workspaceDir, runtime, prompter);
},
daemon: async () => {
if (!didConfigureGateway) await promptDaemonPort();
daemonSetupOutcome = await maybeInstallDaemon({
runtime,
port: gatewayPort
});
},
health: async () => {
healthCheckOutcome = await runGatewayHealthCheck({
cfg: nextConfig,
runtime,
port: gatewayPort,
daemonSetupOutcome
});
}
};
if (selectedSections) {
if (selectedSections.length === 0) {
outro("No configuration changes selected.");
return;
}
for (const section of [
"workspace",
"model",
"web",
"gateway",
"channels",
"plugins",
"skills"
]) if (selectedSections.includes(section)) {
await sectionActions[section]();
hasPendingConfig = true;
}
await persistPendingConfig();
for (const section of ["daemon", "health"]) if (selectedSections.includes(section)) await sectionActions[section]();
} else {
let ranSection = false;
while (true) {
const choice = await promptConfigureSection(runtime, ranSection);
if (choice === "__continue") break;
ranSection = true;
if (choice === "daemon" || choice === "health") await persistPendingConfig();
await sectionActions[choice]();
if (choice !== "daemon" && choice !== "health") {
hasPendingConfig = true;
await persistPendingConfig();
}
}
if (!ranSection) {
if (hasPendingConfig) {
await persistPendingConfig();
outro("Gateway mode set to local.");
return;
}
outro("No configuration changes selected.");
return;
}
}
const failedSideEffects = [...daemonSetupOutcome === "failed" ? ["daemon setup"] : [], ...healthCheckOutcome === "failed" ? ["health check"] : []];
let completionMessage = didPersistConfig ? "Configuration updated." : "No configuration changes selected.";
if (failedSideEffects.length > 0) completionMessage = `${didPersistConfig ? "Configuration updated" : "Configuration unchanged"}, but ${failedSideEffects.join(" and ")} failed.`;
else if (!didPersistConfig && healthCheckOutcome) completionMessage = `Health check ${healthCheckOutcome === "succeeded" ? "completed" : "skipped"}.`;
else if (!didPersistConfig && daemonSetupOutcome) completionMessage = `Daemon setup ${daemonSetupOutcome === "succeeded" ? "completed" : "skipped"}.`;
if (shouldSkipGatewaySummary) {
const remoteUrl = normalizeOptionalString(nextConfig.gateway?.remote?.url);
if (remoteUrl) note([
"Remote Gateway:",
remoteUrl,
"Docs: https://docs.openclaw.ai/gateway/remote"
].join("\n"), "Gateway");
outro(completionMessage);
return;
}
const bind = nextConfig.gateway?.bind ?? "loopback";
const displayLinks = await resolveAdvertisedControlUiLinks({
bind,
port: gatewayPort,
customBindHost: nextConfig.gateway?.customBindHost,
basePath: nextConfig.gateway?.controlUi?.basePath,
tlsEnabled: nextConfig.gateway?.tls?.enabled === true
});
const probeLinks = resolveLocalControlUiProbeLinks({
bind,
port: gatewayPort,
customBindHost: nextConfig.gateway?.customBindHost,
basePath: nextConfig.gateway?.controlUi?.basePath,
tlsEnabled: nextConfig.gateway?.tls?.enabled === true
});
const probeAuth = await resolveGatewayProbeAuthSafeWithSecretInputs({
cfg: nextConfig,
env: process.env,
mode: "local",
localPrecedence: "env-first"
});
const probe = daemonSetupOutcome === "succeeded" ? waitForGatewayReachable : probeGatewayReachable;
let gatewayProbe = probeAuth.warning ? {
ok: false,
detail: "auth unavailable; probe skipped"
} : await probe({
...daemonSetupOutcome === "succeeded" ? resolveGatewayStartupTiming() : {},
url: probeLinks.wsUrl,
token: probeAuth.auth.token,
password: probeAuth.auth.password
});
if (!gatewayProbe.ok && !probeAuth.warning && baseConfig.gateway?.auth?.password) {
const oldProbeAuth = await resolveGatewayProbeAuthSafeWithSecretInputs({
cfg: baseConfig,
env: process.env,
mode: "local",
localPrecedence: "env-first"
});
if (!oldProbeAuth.warning && oldProbeAuth.auth.password && probeAuth.auth.password !== oldProbeAuth.auth.password) gatewayProbe = await probeGatewayReachable({
url: probeLinks.wsUrl,
token: probeAuth.auth.token,
password: oldProbeAuth.auth.password
});
}
const gatewayStatusLine = probeAuth.warning ? "Gateway: auth unavailable (probe skipped)" : gatewayProbe.ok ? "Gateway: reachable" : `Gateway: not detected${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`;
const windowsFirewallLines = formatWindowsGatewayFirewallGuidance({ bind });
note([
`Web UI: ${displayLinks.httpUrl}`,
`Gateway WS: ${displayLinks.wsUrl}`,
gatewayStatusLine,
...windowsFirewallLines,
"Docs: https://docs.openclaw.ai/web/control-ui"
].join("\n"), "Control UI");
outro(completionMessage);
} catch (err) {
if (err instanceof WizardCancelledError) {
runtime.exit(1);
return;
}
throw err;
}
}
//#endregion
//#region src/commands/configure.commands.ts
/**
* Non-interactive config subcommands surfaced when the wizard cannot run.
* Mirrors the real `openclaw config <sub>` surface so the message only ever
* points users at commands that exist (see `src/cli/config-cli.ts`).
*/
const CONFIGURE_NON_TTY_HINT = [
"Interactive configuration requires an interactive terminal (TTY).",
"For non-interactive setup, use these subcommands instead:",
` ${formatCliCommand("openclaw config set <path> <value>")} write a config entry`,
` ${formatCliCommand("openclaw config get <path>")} read a config entry`,
` ${formatCliCommand("openclaw config patch")} apply a JSON patch`,
` ${formatCliCommand("openclaw config validate")} validate configuration`
].join("\n");
/**
* Refuses to launch the interactive wizard without a TTY.
*
* `interactive` lets callers/tests override the detected terminal state
* (mirrors the `params.interactive ?? process.stdin.isTTY` pattern used by
* `src/commands/gateway-readiness.ts`), so the fail-closed path is exercisable
* without mutating the global `process` streams. Both stdin and stdout must be
* TTYs: the wizard reads from stdin and renders prompts to stdout, so either
* being piped means it cannot run correctly.
*
* Returns true when the wizard may proceed.
*/
function assertInteractiveConfigureTerminal(runtime, interactive) {
if (interactive ?? isTerminalInteractive()) return true;
runtime.error(CONFIGURE_NON_TTY_HINT);
runtime.exit(1);
return false;
}
async function configureCommand(runtime = defaultRuntime) {
await runConfigureWizard({ command: "configure" }, runtime);
}
async function configureCommandWithSections(sections, runtime = defaultRuntime) {
await runConfigureWizard({
command: "configure",
sections
}, runtime);
}
/** Parse `--section` input and run the requested configure wizard sections. */
async function configureCommandFromSectionsArg(rawSections, runtime = defaultRuntime, options) {
const { sections, invalid } = parseConfigureWizardSections(rawSections);
if (invalid.length > 0) {
runtime.error(`Invalid --section: ${invalid.map((section) => section || "\"\"").join(", ")}. Expected one of: ${CONFIGURE_WIZARD_SECTIONS.join(", ")}. Run ${formatCliCommand("openclaw configure")} without --section to use the full wizard.`);
runtime.exit(1);
return;
}
if (!assertInteractiveConfigureTerminal(runtime, options?.interactive)) return;
if (sections.length === 0) {
await configureCommand(runtime);
return;
}
await configureCommandWithSections(sections, runtime);
}
//#endregion
export { configureCommandFromSectionsArg as t };