From baec665cc5b2ebbf16de32722932e0b07942bcbc Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:15:32 -0600 Subject: [PATCH] Onboard: reject invalid and remote-only tools profile usage --- .../onboard-non-interactive.gateway.test.ts | 20 ++++++++++ src/commands/onboard-non-interactive.ts | 5 +++ src/commands/onboard.test.ts | 37 +++++++++++++++++++ src/commands/onboard.ts | 10 ++++- 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/commands/onboard-non-interactive.gateway.test.ts b/src/commands/onboard-non-interactive.gateway.test.ts index 100df65da3c..35e7bbaf2f5 100644 --- a/src/commands/onboard-non-interactive.gateway.test.ts +++ b/src/commands/onboard-non-interactive.gateway.test.ts @@ -178,6 +178,26 @@ describe("onboard (non-interactive): gateway and remote auth", () => { }); }, 60_000); + it("rejects --tools-profile in remote mode", async () => { + await withStateDir("state-tools-profile-remote-", async (_stateDir) => { + await expect( + runNonInteractiveOnboarding( + { + nonInteractive: true, + mode: "remote", + remoteUrl: "wss://gateway.example.test", + toolsProfile: "coding", + authChoice: "skip", + skipSkills: true, + skipHealth: true, + installDaemon: false, + }, + runtime, + ), + ).rejects.toThrow('--tools-profile is only supported when --mode is "local".'); + }); + }, 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 4b4d1223226..d5fc245acbc 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -27,6 +27,11 @@ export async function runNonInteractiveOnboarding( runtime.exit(1); return; } + if (mode === "remote" && opts.toolsProfile !== undefined) { + runtime.error('--tools-profile is only supported when --mode is "local".'); + 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 882923d6bba..911c5a8b6b4 100644 --- a/src/commands/onboard.test.ts +++ b/src/commands/onboard.test.ts @@ -156,4 +156,41 @@ describe("onboardCommand", () => { expect(mocks.runInteractiveOnboarding).not.toHaveBeenCalled(); expect(mocks.runNonInteractiveOnboarding).not.toHaveBeenCalled(); }); + + it("fails fast for empty --tools-profile", async () => { + const runtime = makeRuntime(); + + await onboardCommand( + { + toolsProfile: "" as never, + }, + runtime, + ); + + expect(runtime.error).toHaveBeenCalledWith( + 'Invalid --tools-profile. Use "minimal", "coding", "messaging", or "full".', + ); + expect(runtime.exit).toHaveBeenCalledWith(1); + expect(mocks.runInteractiveOnboarding).not.toHaveBeenCalled(); + expect(mocks.runNonInteractiveOnboarding).not.toHaveBeenCalled(); + }); + + it("fails fast for --tools-profile in remote mode", async () => { + const runtime = makeRuntime(); + + await onboardCommand( + { + mode: "remote", + toolsProfile: "coding", + }, + 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 bf589bf000b..ea6e0e9d83b 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -46,11 +46,19 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = runtime.exit(1); return; } - if (normalizedOpts.toolsProfile && !VALID_TOOLS_PROFILES.has(normalizedOpts.toolsProfile)) { + if ( + normalizedOpts.toolsProfile !== undefined && + !VALID_TOOLS_PROFILES.has(normalizedOpts.toolsProfile) + ) { runtime.error('Invalid --tools-profile. Use "minimal", "coding", "messaging", or "full".'); 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".');