diff --git a/src/commands/onboard-non-interactive.gateway.test.ts b/src/commands/onboard-non-interactive.gateway.test.ts index 35e7bbaf2f5..49fb0fbc5a7 100644 --- a/src/commands/onboard-non-interactive.gateway.test.ts +++ b/src/commands/onboard-non-interactive.gateway.test.ts @@ -198,6 +198,30 @@ describe("onboard (non-interactive): gateway and remote auth", () => { }); }, 60_000); + it("rejects invalid --tools-profile in local mode", async () => { + await withStateDir("state-tools-profile-invalid-", async (stateDir) => { + const workspace = path.join(stateDir, "openclaw"); + + await expect( + runNonInteractiveOnboarding( + { + nonInteractive: true, + mode: "local", + workspace, + toolsProfile: "invalid" as never, + authChoice: "skip", + skipSkills: true, + skipHealth: true, + installDaemon: false, + }, + runtime, + ), + ).rejects.toThrow( + 'Invalid --tools-profile. Use "minimal", "coding", "messaging", or "full".', + ); + }); + }, 60_000); + it("uses OPENCLAW_GATEWAY_TOKEN when --gateway-token is omitted", async () => { await withStateDir("state-env-token-", async (stateDir) => { const envToken = "tok_env_fallback_123"; diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index d5fc245acbc..93adfc43db0 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -5,7 +5,7 @@ import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { runNonInteractiveOnboardingLocal } from "./onboard-non-interactive/local.js"; import { runNonInteractiveOnboardingRemote } from "./onboard-non-interactive/remote.js"; -import type { OnboardOptions } from "./onboard-types.js"; +import { VALID_TOOLS_PROFILES, type OnboardOptions } from "./onboard-types.js"; export async function runNonInteractiveOnboarding( opts: OnboardOptions, @@ -32,6 +32,11 @@ export async function runNonInteractiveOnboarding( runtime.exit(1); return; } + if (opts.toolsProfile !== undefined && !VALID_TOOLS_PROFILES.has(opts.toolsProfile)) { + runtime.error('Invalid --tools-profile. Use "minimal", "coding", "messaging", or "full".'); + runtime.exit(1); + return; + } if (mode === "remote") { await runNonInteractiveOnboardingRemote({ opts, runtime, baseConfig }); diff --git a/src/commands/onboard.test.ts b/src/commands/onboard.test.ts index 911c5a8b6b4..a1717061f1c 100644 --- a/src/commands/onboard.test.ts +++ b/src/commands/onboard.test.ts @@ -193,4 +193,23 @@ describe("onboardCommand", () => { expect(mocks.runInteractiveOnboarding).not.toHaveBeenCalled(); expect(mocks.runNonInteractiveOnboarding).not.toHaveBeenCalled(); }); + + it("prefers the remote-mode error when --tools-profile is invalid in remote mode", async () => { + const runtime = makeRuntime(); + + await onboardCommand( + { + mode: "remote", + toolsProfile: "invalid" as never, + }, + runtime, + ); + + expect(runtime.error).toHaveBeenCalledWith( + '--tools-profile is only supported when --mode is "local".', + ); + expect(runtime.exit).toHaveBeenCalledWith(1); + expect(mocks.runInteractiveOnboarding).not.toHaveBeenCalled(); + expect(mocks.runNonInteractiveOnboarding).not.toHaveBeenCalled(); + }); }); diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index ea6e0e9d83b..7dc4af1dd3a 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -46,6 +46,11 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = runtime.exit(1); return; } + if (normalizedOpts.mode === "remote" && normalizedOpts.toolsProfile !== undefined) { + runtime.error('--tools-profile is only supported when --mode is "local".'); + runtime.exit(1); + return; + } if ( normalizedOpts.toolsProfile !== undefined && !VALID_TOOLS_PROFILES.has(normalizedOpts.toolsProfile) @@ -54,11 +59,6 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = runtime.exit(1); return; } - if (normalizedOpts.mode === "remote" && normalizedOpts.toolsProfile !== undefined) { - runtime.error('--tools-profile is only supported when --mode is "local".'); - runtime.exit(1); - return; - } if (normalizedOpts.resetScope && !VALID_RESET_SCOPES.has(normalizedOpts.resetScope)) { runtime.error('Invalid --reset-scope. Use "config", "config+creds+sessions", or "full".'); diff --git a/src/wizard/onboarding.test.ts b/src/wizard/onboarding.test.ts index 1edc80244fc..19c7b8c6675 100644 --- a/src/wizard/onboarding.test.ts +++ b/src/wizard/onboarding.test.ts @@ -645,4 +645,42 @@ describe("runOnboardingWizard", () => { }), ); }); + + it("prompts for tool access profile before workspace in local advanced flow", async () => { + const promptOrder: string[] = []; + const select = vi.fn(async (params: WizardSelectParams) => { + promptOrder.push(`select:${params.message}`); + if (params.message === "Tool access profile") { + return "coding"; + } + return "quickstart"; + }) as unknown as WizardPrompter["select"]; + const text = vi.fn(async (params: { message: string }) => { + promptOrder.push(`text:${params.message}`); + return "/tmp/openclaw-advanced-workspace"; + }) as unknown as WizardPrompter["text"]; + const prompter = buildWizardPrompter({ select, text }); + + await runOnboardingWizard( + { + acceptRisk: true, + flow: "advanced", + mode: "local", + authChoice: "skip", + installDaemon: false, + skipProviders: true, + skipSkills: true, + skipHealth: true, + skipUi: true, + }, + createRuntime(), + prompter, + ); + + expect(promptOrder).toContain("select:Tool access profile"); + expect(promptOrder).toContain("text:Workspace directory"); + expect(promptOrder.indexOf("select:Tool access profile")).toBeLessThan( + promptOrder.indexOf("text:Workspace directory"), + ); + }); }); diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 99ec4794746..ac87867e5e6 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -76,22 +76,22 @@ const TOOL_PROFILE_CHOICES: Array<{ value: ToolProfileId; label: string; hint: s { value: "messaging", label: "Messaging", - hint: "Chat-focused: send messages + use session history; no files, shell, or browser automation.", + hint: "[chat + memory] Chat-focused: send messages + use session history; no files, shell, or browser automation.", }, { value: "coding", label: "Coding", - hint: "Builder mode: read/edit files, run shell, use coding tools + sessions; no direct channel messaging.", + hint: "[files + shell] Builder mode: read/edit files, run shell, use coding tools + sessions; no direct channel messaging.", }, { value: "full", label: "Full", - hint: "Unrestricted built-in tool profile, including higher-risk capabilities.", + hint: "[all built-ins] Unrestricted built-in tool profile, including higher-risk capabilities.", }, { value: "minimal", label: "Minimal", - hint: "Status-only: check session status; no file access, shell commands, browsing, or messaging.", + hint: "[status only] Status-only: check session status; no file access, shell commands, browsing, or messaging.", }, ]; @@ -420,17 +420,6 @@ export async function runOnboardingWizard( return; } - const workspaceInput = - opts.workspace ?? - (flow === "quickstart" - ? (baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE) - : await prompter.text({ - message: "Workspace directory", - initialValue: baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE, - })); - - const workspaceDir = resolveUserPath(workspaceInput.trim() || onboardHelpers.DEFAULT_WORKSPACE); - const existingToolsProfile = baseConfig.tools?.profile; const resolvedExistingToolsProfile = existingToolsProfile ? VALID_TOOLS_PROFILES.has(existingToolsProfile) @@ -449,6 +438,17 @@ export async function runOnboardingWizard( initialValue: resolvedExistingToolsProfile ?? "messaging", })); + const workspaceInput = + opts.workspace ?? + (flow === "quickstart" + ? (baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE) + : await prompter.text({ + message: "Workspace directory", + initialValue: baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE, + })); + + const workspaceDir = resolveUserPath(workspaceInput.trim() || onboardHelpers.DEFAULT_WORKSPACE); + const { applyOnboardingLocalWorkspaceConfig } = await import("../commands/onboard-config.js"); let nextConfig: OpenClawConfig = applyOnboardingLocalWorkspaceConfig(baseConfig, workspaceDir, { toolsProfile,