From ddd4b55cf6e48c5fbdb57868b2c97bf6ee9b2fd8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 19:38:52 +0000 Subject: [PATCH] fix: prevent onboarding TUI auto-delivery (#791) (thanks @roshanasingh4) --- CHANGELOG.md | 1 + src/cli/program.ts | 2 +- src/commands/dashboard.test.ts | 7 ++-- src/commands/dashboard.ts | 11 ++++-- src/wizard/onboarding.test.ts | 64 ++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1cee6c8..8264bfe28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Google: apply patched pi-ai `google-gemini-cli` function call handling (strips ids) after upgrading to pi-ai 0.43.0. - Auto-reply: elevated/reasoning toggles now enqueue system events so the model sees the mode change immediately. - Tools: keep `image` available in sandbox and fail over when image models return empty output (fixes “(no text returned)”). +- Onboarding: TUI defaults to `deliver: false` to avoid cross-provider auto-delivery leaks; onboarding spawns the TUI with explicit `deliver: false`. (#791 — thanks @roshanasingh4) ## 2026.1.11 diff --git a/src/cli/program.ts b/src/cli/program.ts index 7a4383ac1..c83f5203a 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -5,12 +5,12 @@ import { agentsDeleteCommand, agentsListCommand, } from "../commands/agents.js"; -import { dashboardCommand } from "../commands/dashboard.js"; import { CONFIGURE_WIZARD_SECTIONS, configureCommand, configureCommandWithSections, } from "../commands/configure.js"; +import { dashboardCommand } from "../commands/dashboard.js"; import { doctorCommand } from "../commands/doctor.js"; import { healthCommand } from "../commands/health.js"; import { messageCommand } from "../commands/message.js"; diff --git a/src/commands/dashboard.test.ts b/src/commands/dashboard.test.ts index 369c6790e..6261703cd 100644 --- a/src/commands/dashboard.test.ts +++ b/src/commands/dashboard.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { dashboardCommand } from "./dashboard.js"; @@ -94,7 +94,10 @@ describe("dashboardCommand", () => { it("prints SSH hint when browser cannot open", async () => { mockSnapshot("shhhh"); mocks.copyToClipboard.mockResolvedValue(false); - mocks.detectBrowserOpenSupport.mockResolvedValue({ ok: false, reason: "ssh" }); + mocks.detectBrowserOpenSupport.mockResolvedValue({ + ok: false, + reason: "ssh", + }); mocks.formatControlUiSshHint.mockReturnValue("ssh hint"); await dashboardCommand(runtime); diff --git a/src/commands/dashboard.ts b/src/commands/dashboard.ts index 3a679e04c..79f0046b5 100644 --- a/src/commands/dashboard.ts +++ b/src/commands/dashboard.ts @@ -1,6 +1,9 @@ -import { resolveGatewayPort, readConfigFileSnapshot } from "../config/config.js"; -import { defaultRuntime } from "../runtime.js"; +import { + readConfigFileSnapshot, + resolveGatewayPort, +} from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; +import { defaultRuntime } from "../runtime.js"; import { copyToClipboard, detectBrowserOpenSupport, @@ -33,7 +36,9 @@ export async function dashboardCommand( runtime.log(`Dashboard URL: ${authedUrl}`); const copied = await copyToClipboard(authedUrl).catch(() => false); - runtime.log(copied ? "Copied to clipboard." : "Copy to clipboard unavailable."); + runtime.log( + copied ? "Copied to clipboard." : "Copy to clipboard unavailable.", + ); let opened = false; let hint: string | undefined; diff --git a/src/wizard/onboarding.test.ts b/src/wizard/onboarding.test.ts index 28f3a4353..ad39b5c06 100644 --- a/src/wizard/onboarding.test.ts +++ b/src/wizard/onboarding.test.ts @@ -1,5 +1,9 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js"; import type { RuntimeEnv } from "../runtime.js"; import { runOnboardingWizard } from "./onboarding.js"; import type { WizardPrompter } from "./prompts.js"; @@ -117,4 +121,64 @@ describe("runOnboardingWizard", () => { expect(healthCommand).not.toHaveBeenCalled(); expect(runTui).not.toHaveBeenCalled(); }); + + it("launches TUI without auto-delivery when hatching", async () => { + runTui.mockClear(); + + const workspaceDir = await fs.mkdtemp( + path.join(os.tmpdir(), "clawdbot-onboard-"), + ); + await fs.writeFile( + path.join(workspaceDir, DEFAULT_BOOTSTRAP_FILENAME), + "{}", + ); + + const confirm: WizardPrompter["confirm"] = vi.fn(async (opts) => { + if (opts.message === "Do you want to hatch your bot now?") return true; + return opts.initialValue ?? false; + }); + + const prompter: WizardPrompter = { + intro: vi.fn(async () => {}), + outro: vi.fn(async () => {}), + note: vi.fn(async () => {}), + select: vi.fn(async () => "quickstart"), + multiselect: vi.fn(async () => []), + text: vi.fn(async () => ""), + confirm, + progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })), + }; + + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + await runOnboardingWizard( + { + flow: "quickstart", + mode: "local", + workspace: workspaceDir, + authChoice: "skip", + skipProviders: true, + skipSkills: true, + skipHealth: true, + installDaemon: false, + }, + runtime, + prompter, + ); + + expect(runTui).toHaveBeenCalledWith( + expect.objectContaining({ + deliver: false, + message: "Wake up, my friend!", + }), + ); + + await fs.rm(workspaceDir, { recursive: true, force: true }); + }); });