refactor(exec): dedupe executable candidate resolution

This commit is contained in:
Peter Steinberger
2026-04-03 01:57:20 +09:00
parent 48279dca84
commit e36c563775
4 changed files with 51 additions and 30 deletions

View File

@@ -445,5 +445,14 @@ describe("exec-command-resolution", () => {
String.raw`C:\Users\demo\AI\system\openclaw`,
),
).toBeUndefined();
expect(
resolveAllowlistCandidatePath(
{
rawExecutable: String.raw`:/Users/demo/AI/system/openclaw`,
executableName: "openclaw",
},
String.raw`C:\Users\demo\AI\system\openclaw`,
),
).toBeUndefined();
});
});

View File

@@ -3,8 +3,10 @@ import path from "node:path";
import { matchesExecAllowlistPattern } from "./exec-allowlist-pattern.js";
import type { ExecAllowlistEntry } from "./exec-approvals.js";
import { resolveExecWrapperTrustPlan } from "./exec-wrapper-trust-plan.js";
import { resolveExecutablePath as resolveExecutableCandidatePath } from "./executable-path.js";
import { expandHomePrefix } from "./home-dir.js";
import {
resolveExecutablePath as resolveExecutableCandidatePath,
resolveExecutablePathCandidate,
} from "./executable-path.js";
export type ExecutableResolution = {
rawExecutable: string;
@@ -45,10 +47,6 @@ 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;
@@ -179,18 +177,10 @@ function resolveExecutableCandidatePathFromResolution(
if (!raw) {
return undefined;
}
const expanded = raw.startsWith("~") ? expandHomePrefix(raw) : raw;
if (isDriveLessWindowsRootedPath(expanded)) {
return undefined;
}
if (!expanded.includes("/") && !expanded.includes("\\")) {
return undefined;
}
if (path.isAbsolute(expanded)) {
return expanded;
}
const base = cwd && cwd.trim() ? cwd.trim() : process.cwd();
return path.resolve(base, expanded);
return resolveExecutablePathCandidate(raw, {
cwd,
requirePathSeparator: true,
});
}
export function resolveExecutionTargetResolution(

View File

@@ -85,5 +85,10 @@ describe("executable path helpers", () => {
cwd: String.raw`C:\Users\demo\AI\system\openclaw`,
}),
).toBeUndefined();
expect(
resolveExecutablePath(String.raw`:/Users/demo/AI/system/openclaw/git.exe`, {
cwd: String.raw`C:\Users\demo\AI\system\openclaw`,
}),
).toBeUndefined();
});
});

View File

@@ -2,10 +2,34 @@ import fs from "node:fs";
import path from "node:path";
import { expandHomePrefix } from "./home-dir.js";
function isDriveLessWindowsRootedPath(value: string): boolean {
export function isDriveLessWindowsRootedPath(value: string): boolean {
return process.platform === "win32" && /^:[\\/]/.test(value);
}
export function resolveExecutablePathCandidate(
rawExecutable: string,
options?: { cwd?: string; env?: NodeJS.ProcessEnv; requirePathSeparator?: boolean },
): string | undefined {
const expanded = rawExecutable.startsWith("~")
? expandHomePrefix(rawExecutable, { env: options?.env })
: rawExecutable;
if (isDriveLessWindowsRootedPath(expanded)) {
return undefined;
}
const hasPathSeparator = expanded.includes("/") || expanded.includes("\\");
if (options?.requirePathSeparator && !hasPathSeparator) {
return undefined;
}
if (!hasPathSeparator) {
return expanded;
}
if (path.isAbsolute(expanded)) {
return expanded;
}
const base = options?.cwd && options.cwd.trim() ? options.cwd.trim() : process.cwd();
return path.resolve(base, expanded);
}
function resolveWindowsExecutableExtensions(
executable: string,
env: NodeJS.ProcessEnv | undefined,
@@ -87,21 +111,14 @@ export function resolveExecutablePath(
rawExecutable: string,
options?: { cwd?: string; env?: NodeJS.ProcessEnv },
): string | undefined {
const expanded = rawExecutable.startsWith("~")
? expandHomePrefix(rawExecutable, { env: options?.env })
: rawExecutable;
if (isDriveLessWindowsRootedPath(expanded)) {
const candidate = resolveExecutablePathCandidate(rawExecutable, options);
if (!candidate) {
return undefined;
}
if (expanded.includes("/") || expanded.includes("\\")) {
if (path.isAbsolute(expanded)) {
return isExecutableFile(expanded) ? expanded : undefined;
}
const base = options?.cwd && options.cwd.trim() ? options.cwd.trim() : process.cwd();
const candidate = path.resolve(base, expanded);
if (candidate.includes("/") || candidate.includes("\\")) {
return isExecutableFile(candidate) ? candidate : undefined;
}
const envPath =
options?.env?.PATH ?? options?.env?.Path ?? process.env.PATH ?? process.env.Path ?? "";
return resolveExecutableFromPathEnv(expanded, envPath, options?.env);
return resolveExecutableFromPathEnv(candidate, envPath, options?.env);
}