fix(daemon): bound Node binary lookup (#109239)

* fix(daemon): bound Node binary lookup

* docs(changelog): note bounded daemon Node lookup

Co-authored-by: Alix-007 <li.long15@xydigit.com>

* docs(changelog): keep release notes out of contributor PR

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Alix-007
2026-07-16 12:32:08 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent b36011bb2a
commit 8f1c6df748
2 changed files with 43 additions and 3 deletions
+37 -2
View File
@@ -1,7 +1,7 @@
// Daemon program argument tests cover CLI argument construction for services.
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js";
import { withMockedPlatform, withMockedWindowsPlatform } from "../test-utils/vitest-spies.js";
const execFileSyncMock = vi.hoisted(() => vi.fn());
@@ -203,7 +203,7 @@ describe("resolveGatewayProgramArguments", () => {
expect(execFileSyncMock).toHaveBeenCalledWith(
path.win32.join(String.raw`D:\Windows`, "System32", "where.exe"),
["node"],
{ encoding: "utf8" },
{ encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL" },
);
expect(result?.programArguments).toEqual([
String.raw`D:\Tools\node.exe`,
@@ -216,6 +216,41 @@ describe("resolveGatewayProgramArguments", () => {
]);
});
it("bounds POSIX Node runtime lookup", async () => {
const repoIndexPath = path.resolve("/repo/src/index.ts");
const repoEntryPath = path.resolve("/repo/src/entry.ts");
process.argv = ["/usr/local/bin/bun", repoIndexPath];
process.execPath = "/usr/local/bin/bun";
fsMocks.realpath.mockResolvedValue(repoIndexPath);
fsMocks.access.mockResolvedValue(undefined);
execFileSyncMock.mockReturnValue("/usr/local/bin/node\n");
const result = await withMockedPlatform(
"linux",
async () =>
await resolveGatewayProgramArguments({
dev: true,
port: 18789,
runtime: "node",
}),
);
expect(execFileSyncMock).toHaveBeenCalledWith("which", ["node"], {
encoding: "utf8",
timeout: 5_000,
killSignal: "SIGKILL",
});
expect(result.programArguments).toEqual([
"/usr/local/bin/node",
"--import",
"tsx",
repoEntryPath,
"gateway",
"--port",
"18789",
]);
});
it("uses an executable wrapper when provided", async () => {
const wrapperPath = path.resolve("/usr/local/bin/openclaw-doppler");
fsMocks.stat.mockResolvedValue({ isFile: () => true } as never);
+6 -1
View File
@@ -19,6 +19,7 @@ type GatewayProgramArgs = {
type GatewayRuntimePreference = "auto" | "node";
export const OPENCLAW_WRAPPER_ENV_KEY = "OPENCLAW_WRAPPER";
const NODE_BINARY_LOOKUP_TIMEOUT_MS = 5_000;
async function resolveCliEntrypointPathForService(): Promise<string> {
const argv1 = process.argv[1];
@@ -163,7 +164,11 @@ async function resolveNodePath(): Promise<string> {
async function resolveBinaryPath(binary: string): Promise<string> {
const cmd = process.platform === "win32" ? getWindowsSystem32ExePath("where.exe") : "which";
try {
const output = execFileSync(cmd, [binary], { encoding: "utf8" }).trim();
const output = execFileSync(cmd, [binary], {
encoding: "utf8",
timeout: NODE_BINARY_LOOKUP_TIMEOUT_MS,
killSignal: "SIGKILL",
}).trim();
const resolved = output.split(/\r?\n/)[0]?.trim();
if (!resolved) {
throw new Error("empty");