From 11fcbadec8ee16aa3b46fd6f4149c2a90cb68700 Mon Sep 17 00:00:00 2001 From: Sebastian <19554889+sebslight@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:01:37 -0500 Subject: [PATCH] fix(daemon): guard preferred node selection --- CHANGELOG.md | 1 + src/daemon/runtime-paths.test.ts | 25 +++++++++++++++++++++++++ src/daemon/runtime-paths.ts | 9 ++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6353fd1e15b..7c3e7744621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai - Gateway/Update: preserve update.run restart delivery context so post-update status replies route back to the initiating channel/thread. (#18267) Thanks @yinghaosang. - CLI/Update: run a standalone restart helper after updates, honoring service-name overrides and reporting restart initiation separately from confirmed restarts. (#18050) - CLI/Daemon: warn when a gateway restart sees a stale service token so users can reinstall with `openclaw gateway install --force`, and skip drift warnings for non-gateway service restarts. (#18018) +- CLI/Daemon: prefer the active version-manager Node when installing daemons and include macOS version-manager bin directories in the service PATH so launchd services resolve user-managed runtimes. - CLI/Status: fix `openclaw status --all` token summaries for bot-token-only channels so Mattermost/Zalo no longer show a bot+app warning. (#18527) Thanks @echo931. - CLI/Configure: make the `/model picker` allowlist prompt searchable with tokenized matching in `openclaw configure` so users can filter huge model lists by typing terms like `gpt-5.2 openai/`. (#19010) Thanks @bjesuiter. - CLI/Message: preserve `--components` JSON payloads in `openclaw message send` so Discord component payloads are no longer dropped. (#18222) Thanks @saurabhchopade. diff --git a/src/daemon/runtime-paths.test.ts b/src/daemon/runtime-paths.test.ts index 45d7142ecec..677bfad30ba 100644 --- a/src/daemon/runtime-paths.test.ts +++ b/src/daemon/runtime-paths.test.ts @@ -70,6 +70,31 @@ describe("resolvePreferredNodePath", () => { expect(execFile).toHaveBeenCalledTimes(2); }); + it("ignores execPath when it is not node", async () => { + fsMocks.access.mockImplementation(async (target: string) => { + if (target === darwinNode) { + return; + } + throw new Error("missing"); + }); + + const execFile = vi.fn().mockResolvedValue({ stdout: "22.12.0\n", stderr: "" }); + + const result = await resolvePreferredNodePath({ + env: {}, + runtime: "node", + platform: "darwin", + execFile, + execPath: "/Users/test/.bun/bin/bun", + }); + + expect(result).toBe(darwinNode); + expect(execFile).toHaveBeenCalledTimes(1); + expect(execFile).toHaveBeenCalledWith(darwinNode, ["-p", "process.versions.node"], { + encoding: "utf8", + }); + }); + it("uses system node when it meets the minimum version", async () => { fsMocks.access.mockImplementation(async (target: string) => { if (target === darwinNode) { diff --git a/src/daemon/runtime-paths.ts b/src/daemon/runtime-paths.ts index eb00841cc6a..5730c24efae 100644 --- a/src/daemon/runtime-paths.ts +++ b/src/daemon/runtime-paths.ts @@ -19,6 +19,12 @@ function getPathModule(platform: NodeJS.Platform) { return platform === "win32" ? path.win32 : path.posix; } +function isNodeExecPath(execPath: string, platform: NodeJS.Platform): boolean { + const pathModule = getPathModule(platform); + const base = pathModule.basename(execPath).toLowerCase(); + return base === "node" || base === "node.exe"; +} + function normalizeForCompare(input: string, platform: NodeJS.Platform): string { const pathModule = getPathModule(platform); const normalized = pathModule.normalize(input).replaceAll("\\", "/"); @@ -160,8 +166,9 @@ export async function resolvePreferredNodePath(params: { // Prefer the node that is currently running `openclaw gateway install`. // This respects the user's active version manager (fnm/nvm/volta/etc.). + const platform = params.platform ?? process.platform; const currentExecPath = params.execPath ?? process.execPath; - if (currentExecPath) { + if (currentExecPath && isNodeExecPath(currentExecPath, platform)) { const execFileImpl = params.execFile ?? execFileAsync; const version = await resolveNodeVersion(currentExecPath, execFileImpl); if (isSupportedNodeVersion(version)) {