From 8961eae3f022e39402045c4482d7de2919c6016a Mon Sep 17 00:00:00 2001 From: Gio Della-Libera Date: Thu, 21 May 2026 13:47:28 -0700 Subject: [PATCH] fix(cli): reject invalid node run port (#84307) Co-authored-by: Gio Della-Libera --- CHANGELOG.md | 1 + src/cli/node-cli/register.test.ts | 69 +++++++++++++++++++++++++++++-- src/cli/node-cli/register.ts | 17 ++++++-- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a11180f4256..132cfc1e387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -198,6 +198,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/node: reject invalid explicit `node run --port` values instead of silently falling back to the configured or default port. Fixes #83923. Thanks @davinci282828. - CLI: reject explicit port numbers above 65535 before they reach Gateway or Node bind paths. Fixes #83900. (#84008) Thanks @hclsys. - Codex app-server: preserve plugin tool auth profiles when Codex owns model transport so OpenClaw dynamic tools can resolve their provider credentials. (#83603) Thanks @rubencu. - Memory/search: scan the JS-side fallback vector path (used when the sqlite-vec index is unavailable or has a mismatched dimension) in bounded rowid batches and yield to the event loop between batches so large chunk tables can no longer pin the Node.js main thread for multi-second windows. Also keeps the SQL prepared statement rooted in a local so node:sqlite cannot finalize it mid-scan under heap pressure. Fixes #81172. Thanks @dev23xyz-oss. diff --git a/src/cli/node-cli/register.test.ts b/src/cli/node-cli/register.test.ts index 0baa181b763..73eaee36c02 100644 --- a/src/cli/node-cli/register.test.ts +++ b/src/cli/node-cli/register.test.ts @@ -1,8 +1,16 @@ import { Command } from "commander"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerNodeCli } from "./register.js"; +type LoadNodeHostConfig = typeof import("../../node-host/config.js").loadNodeHostConfig; + const daemonMocks = vi.hoisted(() => ({ + defaultRuntime: { + error: vi.fn(), + exit: vi.fn(), + }, + loadNodeHostConfig: vi.fn(async () => null), + runNodeHost: vi.fn(), runNodeDaemonInstall: vi.fn(), runNodeDaemonRestart: vi.fn(), runNodeDaemonStart: vi.fn(), @@ -14,11 +22,15 @@ const daemonMocks = vi.hoisted(() => ({ vi.mock("./daemon.js", () => daemonMocks); vi.mock("../../node-host/config.js", () => ({ - loadNodeHostConfig: vi.fn(async () => null), + loadNodeHostConfig: daemonMocks.loadNodeHostConfig, })); vi.mock("../../node-host/runner.js", () => ({ - runNodeHost: vi.fn(), + runNodeHost: daemonMocks.runNodeHost, +})); + +vi.mock("../../runtime.js", () => ({ + defaultRuntime: daemonMocks.defaultRuntime, })); function createProgram(): Command { @@ -33,6 +45,20 @@ function createProgram(): Command { } describe("registerNodeCli", () => { + beforeEach(() => { + daemonMocks.defaultRuntime.error.mockClear(); + daemonMocks.defaultRuntime.exit.mockClear(); + daemonMocks.loadNodeHostConfig.mockClear(); + daemonMocks.loadNodeHostConfig.mockResolvedValue(null); + daemonMocks.runNodeHost.mockClear(); + daemonMocks.runNodeDaemonInstall.mockClear(); + daemonMocks.runNodeDaemonRestart.mockClear(); + daemonMocks.runNodeDaemonStart.mockClear(); + daemonMocks.runNodeDaemonStatus.mockClear(); + daemonMocks.runNodeDaemonStop.mockClear(); + daemonMocks.runNodeDaemonUninstall.mockClear(); + }); + it("registers node start for the macOS app node service manager", async () => { const program = createProgram(); @@ -40,4 +66,41 @@ describe("registerNodeCli", () => { expect(daemonMocks.runNodeDaemonStart.mock.calls[0]?.[0]?.json).toBe(true); }); + + it("rejects an explicit invalid node run port", async () => { + const program = createProgram(); + + await program.parseAsync(["node", "run", "--port", "abc"], { from: "user" }); + + expect(daemonMocks.runNodeHost).not.toHaveBeenCalled(); + expect(daemonMocks.defaultRuntime.error).toHaveBeenCalledWith( + expect.stringContaining("Invalid --port"), + ); + expect(daemonMocks.defaultRuntime.exit).toHaveBeenCalledWith(1); + }); + + it("uses an explicit valid node run port", async () => { + const program = createProgram(); + + await program.parseAsync(["node", "run", "--port", "19000"], { from: "user" }); + + expect(daemonMocks.runNodeHost).toHaveBeenCalledWith( + expect.objectContaining({ gatewayPort: 19000 }), + ); + }); + + it("falls back to configured node run port when --port is omitted", async () => { + daemonMocks.loadNodeHostConfig.mockResolvedValue({ + version: 1, + nodeId: "node-existing", + gateway: { host: "10.0.0.2", port: 19001 }, + }); + const program = createProgram(); + + await program.parseAsync(["node", "run"], { from: "user" }); + + expect(daemonMocks.runNodeHost).toHaveBeenCalledWith( + expect.objectContaining({ gatewayHost: "10.0.0.2", gatewayPort: 19001 }), + ); + }); }); diff --git a/src/cli/node-cli/register.ts b/src/cli/node-cli/register.ts index 4b5b00633b9..784852e5130 100644 --- a/src/cli/node-cli/register.ts +++ b/src/cli/node-cli/register.ts @@ -1,10 +1,12 @@ import type { Command } from "commander"; import { loadNodeHostConfig } from "../../node-host/config.js"; import { runNodeHost } from "../../node-host/runner.js"; +import { defaultRuntime } from "../../runtime.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { formatDocsLink } from "../../terminal/links.js"; import { theme } from "../../terminal/theme.js"; import { parsePort } from "../daemon-cli/shared.js"; +import { formatInvalidPortOption } from "../error-format.js"; import { formatHelpExamples } from "../help-format.js"; import { runNodeDaemonInstall, @@ -15,9 +17,11 @@ import { runNodeDaemonUninstall, } from "./daemon.js"; -function parsePortWithFallback(value: unknown, fallback: number): number { - const parsed = parsePort(value); - return parsed ?? fallback; +function parsePortOption(value: unknown, fallback: number): number | null { + if (value === undefined) { + return fallback; + } + return parsePort(value); } export function registerNodeCli(program: Command) { @@ -54,7 +58,12 @@ export function registerNodeCli(program: Command) { normalizeOptionalString(opts.host as string | undefined) || existing?.gateway?.host || "127.0.0.1"; - const port = parsePortWithFallback(opts.port, existing?.gateway?.port ?? 18789); + const port = parsePortOption(opts.port, existing?.gateway?.port ?? 18789); + if (port === null) { + defaultRuntime.error(formatInvalidPortOption("--port")); + defaultRuntime.exit(1); + return; + } await runNodeHost({ gatewayHost: host, gatewayPort: port,