diff --git a/src/infra/exec-command-resolution.test.ts b/src/infra/exec-command-resolution.test.ts index 063cefc8df0..213914f5c70 100644 --- a/src/infra/exec-command-resolution.test.ts +++ b/src/infra/exec-command-resolution.test.ts @@ -9,6 +9,7 @@ import { parseExecArgvToken, resolveCommandResolution, resolveCommandResolutionFromArgv, + resolveAllowlistCandidatePath, resolveExecutionTargetCandidatePath, resolvePolicyTargetCandidatePath, } from "./exec-approvals.js"; @@ -429,4 +430,20 @@ describe("exec-command-resolution", () => { expect(long.inlineValue).toBe("blocked.txt"); } }); + + it("does not synthesize cwd-joined allowlist candidates from drive-less windows roots", () => { + if (process.platform !== "win32") { + return; + } + + expect( + resolveAllowlistCandidatePath( + { + rawExecutable: String.raw`:\Users\demo\AI\system\openclaw`, + executableName: "openclaw", + }, + String.raw`C:\Users\demo\AI\system\openclaw`, + ), + ).toBeUndefined(); + }); }); diff --git a/src/infra/exec-command-resolution.ts b/src/infra/exec-command-resolution.ts index bfafd9b87f0..3d8d43e6b90 100644 --- a/src/infra/exec-command-resolution.ts +++ b/src/infra/exec-command-resolution.ts @@ -45,6 +45,10 @@ function parseFirstToken(command: string): string | null { return match ? match[0] : null; } +function isDriveLessWindowsRootedPath(value: string): boolean { + return process.platform === "win32" && /^:[\\/]/.test(value); +} + function tryResolveRealpath(filePath: string | undefined): string | undefined { if (!filePath) { return undefined; @@ -176,6 +180,9 @@ function resolveExecutableCandidatePathFromResolution( return undefined; } const expanded = raw.startsWith("~") ? expandHomePrefix(raw) : raw; + if (isDriveLessWindowsRootedPath(expanded)) { + return undefined; + } if (!expanded.includes("/") && !expanded.includes("\\")) { return undefined; } diff --git a/src/infra/executable-path.test.ts b/src/infra/executable-path.test.ts index 8c7412fb385..a0ea9664404 100644 --- a/src/infra/executable-path.test.ts +++ b/src/infra/executable-path.test.ts @@ -74,4 +74,16 @@ describe("executable path helpers", () => { ); expect(resolveExecutablePath("~/missing-tool", { env: { HOME: homeDir } })).toBeUndefined(); }); + + it("does not treat drive-less rooted windows paths as cwd-relative executables", () => { + if (process.platform !== "win32") { + return; + } + + expect( + resolveExecutablePath(String.raw`:\Users\demo\AI\system\openclaw\git.exe`, { + cwd: String.raw`C:\Users\demo\AI\system\openclaw`, + }), + ).toBeUndefined(); + }); }); diff --git a/src/infra/executable-path.ts b/src/infra/executable-path.ts index a1d596cccf8..fdc558458d7 100644 --- a/src/infra/executable-path.ts +++ b/src/infra/executable-path.ts @@ -2,6 +2,10 @@ import fs from "node:fs"; import path from "node:path"; import { expandHomePrefix } from "./home-dir.js"; +function isDriveLessWindowsRootedPath(value: string): boolean { + return process.platform === "win32" && /^:[\\/]/.test(value); +} + function resolveWindowsExecutableExtensions( executable: string, env: NodeJS.ProcessEnv | undefined, @@ -86,6 +90,9 @@ export function resolveExecutablePath( const expanded = rawExecutable.startsWith("~") ? expandHomePrefix(rawExecutable, { env: options?.env }) : rawExecutable; + if (isDriveLessWindowsRootedPath(expanded)) { + return undefined; + } if (expanded.includes("/") || expanded.includes("\\")) { if (path.isAbsolute(expanded)) { return isExecutableFile(expanded) ? expanded : undefined;