From 11590eb6ce38e2b3af145d5c7d018402bf9ae1b7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 31 Mar 2026 21:49:28 +0900 Subject: [PATCH] fix(ci): restore dotenv trust boundary and windows npm exit handling --- src/infra/dotenv.ts | 13 +++++++++++-- src/process/exec.ts | 5 +---- src/process/exec.windows.test.ts | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 827d7442479..b1cdde62e96 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -40,14 +40,23 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_BASE_URL"]; const BLOCKED_WORKSPACE_DOTENV_PREFIXES = ["ANTHROPIC_API_KEY_", "OPENAI_API_KEY_"]; -function shouldBlockRuntimeDotEnvKey(key: string): boolean { +function shouldBlockWorkspaceRuntimeDotEnvKey(key: string): boolean { return isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key); } +function shouldBlockRuntimeDotEnvKey(key: string): boolean { + // The global ~/.openclaw/.env (or OPENCLAW_STATE_DIR/.env) is a trusted + // operator-controlled runtime surface. Workspace .env is untrusted and gets + // the strict blocklist, but the trusted global fallback is allowed to set + // runtime vars like proxy/base-url/auth values. + void key; + return false; +} + function shouldBlockWorkspaceDotEnvKey(key: string): boolean { const upper = key.toUpperCase(); return ( - shouldBlockRuntimeDotEnvKey(upper) || + shouldBlockWorkspaceRuntimeDotEnvKey(upper) || BLOCKED_WORKSPACE_DOTENV_KEYS.has(upper) || BLOCKED_WORKSPACE_DOTENV_PREFIXES.some((prefix) => upper.startsWith(prefix)) || BLOCKED_WORKSPACE_DOTENV_SUFFIXES.some((suffix) => upper.endsWith(suffix)) diff --git a/src/process/exec.ts b/src/process/exec.ts index ef979007f4d..9b853d09d89 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -234,13 +234,11 @@ export async function runCommandWithTimeout( signal: NodeJS.Signals | null; timedOut: boolean; noOutputTimedOut: boolean; - killed: boolean; }): boolean => usesWindowsExitCodeShim && params.signal == null && !params.timedOut && - !params.noOutputTimedOut && - !params.killed; + !params.noOutputTimedOut; const child = spawn( useCmdWrapper ? (process.env.ComSpec ?? "cmd.exe") : resolvedCommand, @@ -364,7 +362,6 @@ export async function runCommandWithTimeout( signal: resolvedSignal, timedOut, noOutputTimedOut, - killed: child.killed, }) ) { resolvedCode = 0; diff --git a/src/process/exec.windows.test.ts b/src/process/exec.windows.test.ts index bd78fd32956..cb88e866bdb 100644 --- a/src/process/exec.windows.test.ts +++ b/src/process/exec.windows.test.ts @@ -162,6 +162,26 @@ describe("windows command wrapper behavior", () => { } }); + it("treats shimmed Windows commands without a reported exit code as success even when child.killed is true", async () => { + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const child = createMockChild({ + closeCode: null, + exitCode: null, + }); + child.killed = true; + + spawnMock.mockImplementation(() => child); + + try { + const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.termination).toBe("exit"); + } finally { + platformSpy.mockRestore(); + } + }); + it("uses cmd.exe wrapper with windowsVerbatimArguments in runExec for .cmd shims", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const expectedComSpec = process.env.ComSpec ?? "cmd.exe";