





















@@ -104,7 +104,12 @@ vi.mock("../process/supervisor/index.js", () => {
104104};
105105106106const immediate = () => new Promise<void>((resolve) => setImmediate(resolve));
107-const readEnvPath = (env?: NodeJS.ProcessEnv) => env?.PATH ?? env?.Path ?? "";
107+const readPathKey = (env?: NodeJS.ProcessEnv) =>
108+env && "Path" in env && !("PATH" in env) ? "Path" : "PATH";
109+const readEnvPath = (env?: NodeJS.ProcessEnv) => env?.[readPathKey(env)] ?? "";
110+const writeEnvPath = (env: NodeJS.ProcessEnv, value: string) => {
111+env[readPathKey(env)] = value;
112+};
108113const extractCommand = (input: SpawnInput) => input.ptyCommand ?? input.argv?.at(-1) ?? "";
109114const splitCommands = (command: string) => {
110115const commands: string[] = [];
@@ -116,7 +121,18 @@ vi.mock("../process/supervisor/index.js", () => {
116121}
117122return commands;
118123};
119-const stdoutForSegment = (segment: string, env?: NodeJS.ProcessEnv) => {
124+const applySegmentShellEffects = (segment: string, env: NodeJS.ProcessEnv) => {
125+if (segment === 'export PATH="${OPENCLAW_PREPEND_PATH}${PATH:+:$PATH}"') {
126+const prepend = env.OPENCLAW_PREPEND_PATH ?? "";
127+const current = readEnvPath(env);
128+writeEnvPath(env, `${prepend}${current ? `:${current}` : ""}`);
129+return;
130+}
131+if (segment === "unset OPENCLAW_PREPEND_PATH") {
132+delete env.OPENCLAW_PREPEND_PATH;
133+}
134+};
135+const stdoutForSegment = (segment: string, env: NodeJS.ProcessEnv) => {
120136if (segment === "echo $PATH" || segment === "Write-Output $env:PATH") {
121137return `${readEnvPath(env)}\n`;
122138}
@@ -129,10 +145,15 @@ vi.mock("../process/supervisor/index.js", () => {
129145return "";
130146};
131147132-const commandOutput = (command: string, env?: NodeJS.ProcessEnv) =>
133-splitCommands(command)
134-.map((segment) => stdoutForSegment(segment, env))
148+const commandOutput = (command: string, env?: NodeJS.ProcessEnv) => {
149+const shellEnv = { ...env };
150+return splitCommands(command)
151+.map((segment) => {
152+applySegmentShellEffects(segment, shellEnv);
153+return stdoutForSegment(segment, shellEnv);
154+})
135155.join("");
156+};
136157137158return {
138159getProcessSupervisor: () => ({
@@ -845,6 +866,23 @@ describe("exec PATH handling", () => {
845866expect(index).toBeLessThan(baseIndex);
846867}
847868});
869+870+it("protects POSIX prepended paths from shell startup overrides", async () => {
871+if (isWin) {
872+return;
873+}
874+process.env.PATH = "/evil/bin:/usr/bin";
875+const tool = createTestExecTool({ pathPrepend: ["/custom/bin"] });
876+877+const result = await executeExecCommand(tool, COMMAND_PRINT_PATH);
878+879+const text = readNormalizedTextContent(result.content);
880+const entries = text.split(path.delimiter);
881+882+// Simulate a shell startup file prepending /evil/bin before the command runs.
883+// The exec wrapper must still restore configured pathPrepend entries to the front.
884+expect(entries).toEqual(["/custom/bin", "/evil/bin", "/usr/bin"]);
885+});
848886});
849887850888describe("findPathKey", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。