From c899923f21ff3fd71f4ea1ce6fb63ce82fabe65d Mon Sep 17 00:00:00 2001 From: Kumar Abhirup Date: Mon, 13 Apr 2026 00:38:23 -0700 Subject: [PATCH] fix(cli): seed host exec approvals during bootstrap Bootstrap now writes permissive host exec approval defaults before onboard so web chat can execute from first boot. Preserve existing approval rules while reapplying the CLI settings after onboarding. Made-with: Cursor --- ...otstrap-external.bootstrap-command.test.ts | 123 ++++++--- src/cli/bootstrap-external.ts | 246 ++++++++++-------- 2 files changed, 237 insertions(+), 132 deletions(-) diff --git a/src/cli/bootstrap-external.bootstrap-command.test.ts b/src/cli/bootstrap-external.bootstrap-command.test.ts index 62ca1aaf65a73..641205e4591c0 100644 --- a/src/cli/bootstrap-external.bootstrap-command.test.ts +++ b/src/cli/bootstrap-external.bootstrap-command.test.ts @@ -73,10 +73,7 @@ function createWebProfilesResponse(params?: { } as unknown as Response; } -function createJsonResponse(params?: { - status?: number; - payload?: unknown; -}): Response { +function createJsonResponse(params?: { status?: number; payload?: unknown }): Response { const status = params?.status ?? 200; return { status, @@ -137,9 +134,7 @@ function parseConfigSetValue(raw: string): unknown { function applyConfigSet(stateDir: string, keyPath: string, rawValue: string): void { const configPath = path.join(stateDir, "openclaw.json"); - const current = existsSync(configPath) - ? JSON.parse(readFileSync(configPath, "utf-8")) - : {}; + const current = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : {}; const segments = keyPath.split("."); let cursor: Record = current; for (const segment of segments.slice(0, -1)) { @@ -284,7 +279,7 @@ describe("bootstrapCommand always-onboard behavior", () => { promptMocks.confirm.mockImplementation(async () => promptMocks.confirmDecisions.length > 0 ? promptMocks.confirmDecisions.shift()! - : promptMocks.confirmDecision + : promptMocks.confirmDecision, ); promptMocks.select.mockReset(); promptMocks.select.mockImplementation(async () => promptMocks.selectValue); @@ -362,7 +357,11 @@ describe("bootstrapCommand always-onboard behavior", () => { stdout: `${JSON.stringify({ pending: pendingDeviceRequests, paired: pairedDevices })}\n`, }) as never; } - if (commandString === "openclaw" && argList.includes("devices") && argList.includes("approve")) { + if ( + commandString === "openclaw" && + argList.includes("devices") && + argList.includes("approve") + ) { const requestId = argList.at(-1) ?? ""; const match = pendingDeviceRequests.find((entry) => entry.requestId === requestId); if (!match) { @@ -371,7 +370,9 @@ describe("bootstrapCommand always-onboard behavior", () => { stderr: `request not found: ${requestId}\n`, }) as never; } - pendingDeviceRequests = pendingDeviceRequests.filter((entry) => entry.requestId !== requestId); + pendingDeviceRequests = pendingDeviceRequests.filter( + (entry) => entry.requestId !== requestId, + ); pairedDevices = [ ...pairedDevices, { @@ -385,11 +386,7 @@ describe("bootstrapCommand always-onboard behavior", () => { stdout: `Approved ${String(match.deviceId ?? "device")} (${requestId})\n`, }) as never; } - if ( - commandString === "openclaw" && - argList.includes("config") && - argList.includes("set") - ) { + if (commandString === "openclaw" && argList.includes("config") && argList.includes("set")) { const setIndex = argList.lastIndexOf("set"); const keyPath = argList[setIndex + 1]; const rawValue = argList[setIndex + 2]; @@ -791,9 +788,9 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(updatedConfig.agents.defaults.model.primary).toBe( "dench-cloud/anthropic.claude-opus-4-6-v1", ); - expect(updatedConfig.agents.defaults.models["dench-cloud/anthropic.claude-opus-4-6-v1"]).toEqual( - expect.objectContaining({ alias: "Claude Opus 4.6 (Dench Cloud)" }), - ); + expect( + updatedConfig.agents.defaults.models["dench-cloud/anthropic.claude-opus-4-6-v1"], + ).toEqual(expect.objectContaining({ alias: "Claude Opus 4.6 (Dench Cloud)" })); expect(updatedConfig.plugins.allow).toContain("posthog-analytics"); expect(updatedConfig.plugins.allow).toContain("dench-ai-gateway"); expect(updatedConfig.plugins.allow).not.toContain("dench-cloud-provider"); @@ -981,13 +978,13 @@ describe("bootstrapCommand always-onboard behavior", () => { ); }); - expect(runtime.log).toHaveBeenCalledWith( - expect.stringContaining("D E N C H C L O U D"), - ); + expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("D E N C H C L O U D")); expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("App Integrations")); expect(promptMocks.confirm).toHaveBeenCalledWith( expect.objectContaining({ - message: expect.stringContaining("Continue with Dench Cloud? Recommended. API key: dench.com/api"), + message: expect.stringContaining( + "Continue with Dench Cloud? Recommended. API key: dench.com/api", + ), }), ); @@ -1903,7 +1900,9 @@ describe("bootstrapCommand always-onboard behavior", () => { ); const gatewayRestartCalledInAutofix = spawnCalls.some( (call) => - call.command === "openclaw" && call.args.includes("gateway") && call.args.includes("restart"), + call.command === "openclaw" && + call.args.includes("gateway") && + call.args.includes("restart"), ); const toolsProfileSetCall = spawnCalls.find( (call) => @@ -2031,7 +2030,7 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(logMessages).toContain("gateway.err.log"); }); - it("stages exec and elevated commands config in raw JSON before onboard (webchat gets host exec from first boot)", async () => { + it("stages exec, elevated, and host approval config before onboard (webchat gets host exec from first boot)", async () => { const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), @@ -2049,6 +2048,8 @@ describe("bootstrapCommand always-onboard behavior", () => { const configPath = path.join(stateDir, "openclaw.json"); const config = JSON.parse(readFileSync(configPath, "utf-8")); + const execApprovalsPath = path.join(stateDir, "exec-approvals.json"); + const execApprovals = JSON.parse(readFileSync(execApprovalsPath, "utf-8")); expect(config.tools?.exec?.security).toBe("full"); expect(config.tools?.exec?.ask).toBe("off"); expect(config.tools?.elevated?.enabled).toBe(true); @@ -2056,6 +2057,9 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(config.commands?.bash).toBe(true); expect(config.commands?.config).toBe(true); expect(config.agents?.defaults?.elevatedDefault).toBe("on"); + expect(execApprovals.version).toBe(1); + expect(execApprovals.defaults?.security).toBe("full"); + expect(execApprovals.defaults?.ask).toBe("off"); const onboardIndex = spawnCalls.findIndex( (c) => c.command === "openclaw" && c.args.includes("onboard"), @@ -2124,7 +2128,10 @@ describe("bootstrapCommand always-onboard behavior", () => { call.args.includes(key) && call.args.includes(value), ); - expect(postOnboardSetCall, `expected post-onboard config set for ${key}=${value}`).toBeDefined(); + expect( + postOnboardSetCall, + `expected post-onboard config set for ${key}=${value}`, + ).toBeDefined(); expect(postOnboardSetCall?.args).toEqual( expect.arrayContaining(["--profile", "dench", "config", "set", key, value]), ); @@ -2166,12 +2173,19 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(elevatedEnabledCalls).toHaveLength(2); for (const call of elevatedEnabledCalls) { expect(call.args).toEqual( - expect.arrayContaining(["--profile", "dench", "config", "set", "tools.elevated.enabled", "true"]), + expect.arrayContaining([ + "--profile", + "dench", + "config", + "set", + "tools.elevated.enabled", + "true", + ]), ); } }); - it("preserves exec and elevated config in final openclaw.json after full bootstrap cycle", async () => { + it("preserves exec, elevated, and host approval config after full bootstrap cycle", async () => { const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), @@ -2189,6 +2203,8 @@ describe("bootstrapCommand always-onboard behavior", () => { const configPath = path.join(stateDir, "openclaw.json"); const finalConfig = JSON.parse(readFileSync(configPath, "utf-8")); + const execApprovalsPath = path.join(stateDir, "exec-approvals.json"); + const execApprovals = JSON.parse(readFileSync(execApprovalsPath, "utf-8")); expect(finalConfig.tools?.exec?.security).toBe("full"); expect(finalConfig.tools?.exec?.ask).toBe("off"); @@ -2199,6 +2215,56 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(finalConfig.commands?.config).toBe(true); expect(finalConfig.agents?.defaults?.timeoutSeconds).toBe(86400); expect(finalConfig.tools?.profile).toBe("full"); + expect(execApprovals.version).toBe(1); + expect(execApprovals.defaults?.security).toBe("full"); + expect(execApprovals.defaults?.ask).toBe("off"); + }); + + it("preserves existing host exec approval rules while forcing permissive defaults", async () => { + writeFileSync( + path.join(stateDir, "exec-approvals.json"), + JSON.stringify({ + version: 7, + defaults: { + security: "deny", + ask: "on-miss", + }, + agents: { + "assistant:main": { + security: "deny", + ask: "on-request", + }, + }, + }), + ); + + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + await bootstrapCommand( + { + nonInteractive: true, + noOpen: true, + skipUpdate: true, + }, + runtime, + ); + + const execApprovals = JSON.parse( + readFileSync(path.join(stateDir, "exec-approvals.json"), "utf-8"), + ); + expect(execApprovals.version).toBe(7); + expect(execApprovals.defaults?.security).toBe("full"); + expect(execApprovals.defaults?.ask).toBe("off"); + expect(execApprovals.agents).toEqual({ + "assistant:main": { + security: "deny", + ask: "on-request", + }, + }); }); it("strips npm_config_* env vars from npm global commands (prevents npx prefix hijack)", async () => { @@ -2224,8 +2290,7 @@ describe("bootstrapCommand always-onboard behavior", () => { const npmGlobalCalls = spawnCalls.filter( (call) => - call.command === "npm" && - (call.args.includes("-g") || call.args.includes("--global")), + call.command === "npm" && (call.args.includes("-g") || call.args.includes("--global")), ); expect(npmGlobalCalls.length).toBeGreaterThan(0); diff --git a/src/cli/bootstrap-external.ts b/src/cli/bootstrap-external.ts index 2b58f6a7c0be2..e925fc6484584 100644 --- a/src/cli/bootstrap-external.ts +++ b/src/cli/bootstrap-external.ts @@ -12,8 +12,8 @@ import { import path from "node:path"; import process from "node:process"; import { confirm, isCancel, select, spinner, text } from "@clack/prompts"; -import json5 from "json5"; import gradient from "gradient-string"; +import json5 from "json5"; import { isDaemonlessMode } from "../config/paths.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; @@ -52,6 +52,9 @@ const MAX_PORT_SCAN_ATTEMPTS = 100; const DEFAULT_BOOTSTRAP_ROLLOUT_STAGE = "default"; const DEFAULT_GATEWAY_LAUNCH_AGENT_LABEL = "ai.openclaw.gateway"; const REQUIRED_TOOLS_PROFILE = "full"; +const DEFAULT_EXEC_SECURITY = "full"; +const DEFAULT_EXEC_ASK = "off"; +const HOST_EXEC_APPROVALS_FILENAME = "exec-approvals.json"; const OPENCLAW_CLI_CHECK_CACHE_TTL_MS = 5 * 60_000; const OPENCLAW_UPDATE_PROMPT_SUPPRESS_AFTER_INSTALL_MS = 5 * 60_000; const OPENCLAW_CLI_CHECK_CACHE_FILE = "openclaw-cli-check.json"; @@ -445,7 +448,9 @@ function normalizeVersionOutput(raw: string | undefined): string | undefined { return first && first.length > 0 ? first : undefined; } -function parseOpenClawCalendarVersion(raw: string | undefined): [number, number, number] | undefined { +function parseOpenClawCalendarVersion( + raw: string | undefined, +): [number, number, number] | undefined { const match = raw?.match(/\b(\d{4})\.(\d+)\.(\d+)\b/u); if (!match) { return undefined; @@ -554,24 +559,28 @@ function readBundledPluginVersion(pluginDir: string): string | undefined { } function readConfiguredPluginAllowlist(stateDir: string): string[] { - const raw = readBootstrapConfig(stateDir) as { - plugins?: { - allow?: unknown; - }; - } | undefined; + const raw = readBootstrapConfig(stateDir) as + | { + plugins?: { + allow?: unknown; + }; + } + | undefined; return Array.isArray(raw?.plugins?.allow) ? raw.plugins.allow.filter((value): value is string => typeof value === "string") : []; } function readConfiguredPluginLoadPaths(stateDir: string): string[] { - const raw = readBootstrapConfig(stateDir) as { - plugins?: { - load?: { - paths?: unknown; - }; - }; - } | undefined; + const raw = readBootstrapConfig(stateDir) as + | { + plugins?: { + load?: { + paths?: unknown; + }; + }; + } + | undefined; return Array.isArray(raw?.plugins?.load?.paths) ? raw.plugins.load.paths.filter((value): value is string => typeof value === "string") : []; @@ -590,14 +599,7 @@ async function setOpenClawConfigJson(params: { }): Promise { await runOpenClawOrThrow({ openclawCommand: params.openclawCommand, - args: [ - "--profile", - params.profile, - "config", - "set", - params.key, - JSON.stringify(params.value), - ], + args: ["--profile", params.profile, "config", "set", params.key, JSON.stringify(params.value)], timeoutMs: 30_000, errorMessage: params.errorMessage, }); @@ -618,7 +620,9 @@ function readDenchIntegrationsMetadata(stateDir: string): Record): Record | undefined { +function readTtsElevenLabsConfig( + tts: Record, +): Record | undefined { return asRecord(tts.elevenlabs) ?? asRecord(asRecord(tts.providers)?.elevenlabs); } @@ -693,10 +697,7 @@ function disableDenchElevenLabsOverride( ) { delete elevenlabs.baseUrl; } - if ( - typeof elevenlabs.apiKey === "string" && - (!apiKey || elevenlabs.apiKey === apiKey) - ) { + if (typeof elevenlabs.apiKey === "string" && (!apiKey || elevenlabs.apiKey === apiKey)) { delete elevenlabs.apiKey; } if (Object.keys(elevenlabs).length === 0) { @@ -733,7 +734,7 @@ function applyDenchManagedIntegrationDefaults(params: { const tools = { ...(asRecord(nextConfig.tools) ?? {}) }; const deny = Array.isArray(tools.deny) - ? (tools.deny.filter((value): value is string => typeof value === "string")) + ? tools.deny.filter((value): value is string => typeof value === "string") : []; const web = { ...(asRecord(tools.web) ?? {}) }; const search = { ...(asRecord(web.search) ?? {}) }; @@ -809,12 +810,8 @@ async function syncBundledPlugins(params: { }; const currentAllow = readConfiguredPluginAllowlist(params.stateDir); const currentLoadPaths = readConfiguredPluginLoadPaths(params.stateDir); - const nextAllow = currentAllow.filter( - (value) => value !== "dench-cloud-provider", - ); - const nextLoadPaths = currentLoadPaths.filter( - (value) => !isLegacyDenchCloudPluginPath(value), - ); + const nextAllow = currentAllow.filter((value) => value !== "dench-cloud-provider"); + const nextLoadPaths = currentLoadPaths.filter((value) => !isLegacyDenchCloudPluginPath(value)); const legacyPluginDir = path.join(params.stateDir, "extensions", "dench-cloud-provider"); const hadLegacyEntry = entries["dench-cloud-provider"] !== undefined; const hadLegacyInstall = installs["dench-cloud-provider"] !== undefined; @@ -957,12 +954,12 @@ async function ensureDefaultWorkspacePath( /** * Stage all required pre-onboard config directly into `stateDir/openclaw.json` - * without going through the OpenClaw CLI. On a fresh install the "dench" - * profile doesn't exist yet (it's created by `openclaw onboard`), so any - * `openclaw config set` call fails. Writing the file directly sidesteps - * this while still ensuring the config is in place before onboard starts - * the daemon. The CLI-based re-application happens post-onboard once the - * profile is live. + * and `stateDir/exec-approvals.json` without going through the OpenClaw CLI. + * On a fresh install the "dench" profile doesn't exist yet (it's created by + * `openclaw onboard`), so any `openclaw config set` call fails. Writing the + * files directly sidesteps this while still ensuring the config is in place + * before onboard starts the daemon. The CLI-based re-application happens + * post-onboard once the profile is live. */ function stagePreOnboardConfig( stateDir: string, @@ -987,8 +984,8 @@ function stagePreOnboardConfig( const tools = { ...(asRecord(raw.tools) ?? {}) }; const exec = { ...(asRecord(tools.exec) ?? {}) }; - exec.security = "full"; - exec.ask = "off"; + exec.security = DEFAULT_EXEC_SECURITY; + exec.ask = DEFAULT_EXEC_ASK; tools.exec = exec; const elevated = { ...(asRecord(tools.elevated) ?? {}) }; elevated.enabled = true; @@ -1005,10 +1002,31 @@ function stagePreOnboardConfig( defaults.elevatedDefault = "on"; + mkdirSync(stateDir, { recursive: true }); + writeFileSync(path.join(stateDir, "openclaw.json"), `${JSON.stringify(raw, null, 2)}\n`); + ensureHostExecApprovalDefaults(stateDir); +} + +/** + * Recent OpenClaw builds enforce exec approvals from a host-side policy file + * in addition to `tools.exec` inside openclaw.json. Keep both layers aligned + * so bootstrap never leaves web chat waiting on a host approval prompt. + */ +function ensureHostExecApprovalDefaults(stateDir: string): void { + const raw = readHostExecApprovalsConfig(stateDir) ?? {}; + const defaults = { ...(asRecord(raw.defaults) ?? {}) }; + defaults.security = DEFAULT_EXEC_SECURITY; + defaults.ask = DEFAULT_EXEC_ASK; + const next = { + ...raw, + version: toFiniteNumber(raw.version) ?? 1, + defaults, + }; + mkdirSync(stateDir, { recursive: true }); writeFileSync( - path.join(stateDir, "openclaw.json"), - `${JSON.stringify(raw, null, 2)}\n`, + path.join(stateDir, HOST_EXEC_APPROVALS_FILENAME), + `${JSON.stringify(next, null, 2)}\n`, ); } @@ -1027,8 +1045,8 @@ async function ensureAgentDefaults(openclawCommand: string, profile: string): Pr ["agents.defaults.subagents.archiveAfterMinutes", "180"], ["agents.defaults.subagents.runTimeoutSeconds", "0"], ["tools.subagents.tools.deny", "[]"], - ["tools.exec.security", "full"], - ["tools.exec.ask", "off"], + ["tools.exec.security", DEFAULT_EXEC_SECURITY], + ["tools.exec.ask", DEFAULT_EXEC_ASK], ["tools.elevated.enabled", "true"], ["tools.elevated.allowFrom.webchat", '["*"]'], ["agents.defaults.elevatedDefault", "on"], @@ -1301,12 +1319,12 @@ function selectBootstrapDevicePairingRequest(pending: DeviceListEntry[]): { .filter((entry) => { const roles = resolveDeviceListEntryRoles(entry); const platformMatches = !entry.platform || entry.platform === process.platform; - return platformMatches && roles.includes("operator") && hasBootstrapDevicePairingScopes(entry); + return ( + platformMatches && roles.includes("operator") && hasBootstrapDevicePairingScopes(entry) + ); }) .map((entry) => ({ entry, score: scoreBootstrapDevicePairingRequest(entry) })) - .sort( - (a, b) => b.score - a.score || (b.entry.createdAtMs ?? 0) - (a.entry.createdAtMs ?? 0), - ); + .sort((a, b) => b.score - a.score || (b.entry.createdAtMs ?? 0) - (a.entry.createdAtMs ?? 0)); if (candidates.length === 0) { return { status: "none", detail: "no pending local operator pairing request found" }; } @@ -1904,13 +1922,7 @@ async function attemptGatewayAutoFix(params: { }, { name: "openclaw gateway install --force", - args: [ - "--profile", - params.profile, - "gateway", - "install", - "--force", - ], + args: ["--profile", params.profile, "gateway", "install", "--force"], timeoutMs: 2 * 60_000, }, { @@ -2062,6 +2074,18 @@ function readBootstrapConfig(stateDir: string): Record | undefi return undefined; } +function readHostExecApprovalsConfig(stateDir: string): Record | undefined { + const approvalsPath = path.join(stateDir, HOST_EXEC_APPROVALS_FILENAME); + if (!existsSync(approvalsPath)) { + return undefined; + } + try { + return asRecord(json5.parse(readFileSync(approvalsPath, "utf-8"))); + } catch { + return undefined; + } +} + function hasConfiguredComposioServer(_stateDir: string): boolean { return true; } @@ -2108,9 +2132,11 @@ function writeAuthProfileKey(stateDir: string, apiKey: string): void { if (existsSync(authPath)) { raw = json5.parse(readFileSync(authPath, "utf-8")); } - } catch { /* fresh file */ } + } catch { + /* fresh file */ + } - const profiles = ((raw.profiles ?? {}) as Record); + const profiles = (raw.profiles ?? {}) as Record; profiles["dench-cloud:default"] = { type: "api_key", provider: "dench-cloud", @@ -2132,11 +2158,13 @@ export function checkAgentAuth( if (!provider) { return { ok: false, detail: "No model provider configured." }; } - const rawConfig = readBootstrapConfig(stateDir) as { - models?: { - providers?: Record; - }; - } | undefined; + const rawConfig = readBootstrapConfig(stateDir) as + | { + models?: { + providers?: Record; + }; + } + | undefined; const customProvider = rawConfig?.models?.providers?.[provider]; if (customProvider && typeof customProvider === "object") { const apiKey = (customProvider as Record).apiKey; @@ -2399,9 +2427,9 @@ function logBootstrapChecklist(diagnostics: BootstrapDiagnostics, runtime: Runti function isExplicitDenchCloudRequest(opts: BootstrapOptions): boolean { return Boolean( opts.denchCloud || - opts.denchCloudApiKey?.trim() || - opts.denchCloudModel?.trim() || - opts.denchGatewayUrl?.trim(), + opts.denchCloudApiKey?.trim() || + opts.denchCloudModel?.trim() || + opts.denchGatewayUrl?.trim(), ); } @@ -2513,9 +2541,7 @@ function renderDenchCloudRecommendationBanner(): string { ].join(dot); const check = rich ? theme.success("✓") : "✓"; - const cta = rich - ? theme.success("Recommended for most users") - : "Recommended for most users"; + const cta = rich ? theme.success("Recommended for most users") : "Recommended for most users"; return [ "", @@ -2561,14 +2587,20 @@ function preStageDenchCloudConfig(params: { nextConfig.models = models; const tools = { ...asRecord(nextConfig.tools) }; - tools.alsoAllow = mergeAllowedTools(nextConfig.tools, (configPatch as Record).tools); + tools.alsoAllow = mergeAllowedTools( + nextConfig.tools, + (configPatch as Record).tools, + ); delete tools.allow; nextConfig.tools = tools; if (params.selectedModel) { const agents = { ...asRecord(nextConfig.agents) }; const defaults = { ...asRecord(agents.defaults) }; - defaults.model = { ...asRecord(defaults.model), primary: `dench-cloud/${params.selectedModel}` }; + defaults.model = { + ...asRecord(defaults.model), + primary: `dench-cloud/${params.selectedModel}`, + }; agents.defaults = defaults; nextConfig.agents = agents; } @@ -2624,7 +2656,10 @@ function rewriteDenchCloudTtsConfigFile(params: { elevenlabs.apiKey = params.apiKey; messages.tts = tts; nextConfig.messages = messages; - writeFileSync(path.join(params.stateDir, "openclaw.json"), `${JSON.stringify(nextConfig, null, 2)}\n`); + writeFileSync( + path.join(params.stateDir, "openclaw.json"), + `${JSON.stringify(nextConfig, null, 2)}\n`, + ); } function isExpectedTtsShapeValidationError( @@ -2669,9 +2704,10 @@ async function applyDenchCloudTtsConfig(params: { openclawCommand: params.openclawCommand, profile: params.profile, key: shape === "providers" ? "messages.tts.providers.elevenlabs" : "messages.tts.elevenlabs", - value: shape === "providers" - ? asRecord(asRecord(ttsConfig.providers)?.elevenlabs) - : asRecord(ttsConfig.elevenlabs), + value: + shape === "providers" + ? asRecord(asRecord(ttsConfig.providers)?.elevenlabs) + : asRecord(ttsConfig.elevenlabs), errorMessage: "Failed to configure ElevenLabs TTS via Dench Cloud gateway.", }); }; @@ -2699,13 +2735,15 @@ async function applyDenchCloudBootstrapConfig(params: { selectedModel: string; openClawVersion?: string; }): Promise { - const raw = readBootstrapConfig(params.stateDir) as { - agents?: { - defaults?: { - models?: unknown; - }; - }; - } | undefined; + const raw = readBootstrapConfig(params.stateDir) as + | { + agents?: { + defaults?: { + models?: unknown; + }; + }; + } + | undefined; const existingAgentModels = raw?.agents?.defaults?.models && typeof raw.agents.defaults.models === "object" ? (raw.agents.defaults.models as Record) @@ -2846,9 +2884,11 @@ async function resolveDenchCloudBootstrapSelection(params: { const wantsDenchCloud = explicitRequest ? true : await confirm({ - message: stylePromptMessage("Continue with Dench Cloud? Recommended. API key: dench.com/api"), - initialValue: existingDenchConfigured || !currentProvider, - }); + message: stylePromptMessage( + "Continue with Dench Cloud? Recommended. API key: dench.com/api", + ), + initialValue: existingDenchConfigured || !currentProvider, + }); if (isCancel(wantsDenchCloud) || !wantsDenchCloud) { return { enabled: false }; } @@ -2899,10 +2939,16 @@ async function resolveDenchCloudBootstrapSelection(params: { catalogSpinner?.stop("Models loaded."); } - const explicitModel = params.opts.denchCloudModel?.trim() || process.env.DENCH_CLOUD_MODEL?.trim(); - const preselected = resolveDenchCloudModel(catalog.models, explicitModel || existing.selectedModel); + const explicitModel = + params.opts.denchCloudModel?.trim() || process.env.DENCH_CLOUD_MODEL?.trim(); + const preselected = resolveDenchCloudModel( + catalog.models, + explicitModel || existing.selectedModel, + ); if (!preselected && explicitModel) { - params.runtime.log(theme.warn(`Configured Dench Cloud model "${explicitModel}" is unavailable.`)); + params.runtime.log( + theme.warn(`Configured Dench Cloud model "${explicitModel}" is unavailable.`), + ); } const selection = await promptForDenchCloudModel({ models: catalog.models, @@ -2923,9 +2969,7 @@ async function resolveDenchCloudBootstrapSelection(params: { verifySpinner?.stop("Dench Cloud ready."); } catch (error) { verifySpinner?.stop("Verification failed."); - params.runtime.log( - theme.warn(error instanceof Error ? error.message : String(error)), - ); + params.runtime.log(theme.warn(error instanceof Error ? error.message : String(error))); const retry = await confirm({ message: stylePromptMessage("Re-enter your Dench Cloud API key?"), initialValue: true, @@ -3131,11 +3175,11 @@ export async function bootstrapCommand( sourceDirName: "posthog-analytics", ...(process.env.POSTHOG_KEY ? { - enabled: true, - config: { - apiKey: process.env.POSTHOG_KEY, - }, - } + enabled: true, + config: { + apiKey: process.env.POSTHOG_KEY, + }, + } : {}), }, { @@ -3159,17 +3203,13 @@ export async function bootstrapCommand( pluginId: "apollo-enrichment", sourceDirName: "apollo-enrichment", enabled: denchCloudSelection.enabled, - ...(denchCloudSelection.enabled - ? { config: { enabled: true } } - : {}), + ...(denchCloudSelection.enabled ? { config: { enabled: true } } : {}), }, { pluginId: "exa-search", sourceDirName: "exa-search", enabled: denchCloudSelection.enabled, - ...(denchCloudSelection.enabled - ? { config: { enabled: true } } - : {}), + ...(denchCloudSelection.enabled ? { config: { enabled: true } } : {}), }, ];