mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(infra): block Windows system path env vars from workspace .env injection (#74456)
* fix: address issue * fix: address PR review feedback * fix: address codex review feedback * fix: address codex review feedback * fix: address codex review feedback * docs: add changelog entry for PR merge * Update CHANGELOG.md
This commit is contained in:
committed by
GitHub
parent
3b75898bee
commit
cba0a348dc
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- fix(infra): block ambient Homebrew env vars from brew resolution. (#74463) Thanks @pgondhi987.
|
||||
- Thinking/providers: resolve bundled provider thinking profiles through lightweight provider policy artifacts when startup-lazy providers are not active, so OpenAI Codex GPT-5.x keeps xhigh available in Gateway session validation. Fixes #74796. Thanks @maxschachere.
|
||||
- Security/Windows: ignore workspace `.env` system-path variables and resolve stale-process `taskkill.exe` from the validated Windows install root, preventing repository-local env files from redirecting cleanup helpers. Thanks @pgondhi987.
|
||||
- Plugins/TTS: keep bundled speech-provider discovery available on cold package Gateway paths and add bundled plugin matrix runtime probes for health, readiness, RPC, TTS discovery, and post-ready runtime-deps watchdog coverage. Refs #75283. Thanks @vincentkoc.
|
||||
- Google Meet/Twilio: show delegated voice call ID, DTMF, and intro-greeting state in `googlemeet doctor`, and avoid claiming DTMF was sent when no Meet PIN sequence was configured. Refs #72478. Thanks @DougButdorf.
|
||||
- Voice Call/Twilio: send notify-mode initial TwiML directly in the outbound create-call request while keeping conversation and pre-connect DTMF calls webhook-driven, so one-shot notify calls do not depend on a first-answer webhook fetch. Supersedes #72758. Thanks @tyshepps.
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildEmbeddedPiSettingsSnapshot,
|
||||
DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY,
|
||||
resolveEmbeddedPiProjectSettingsPolicy,
|
||||
} from "./pi-project-settings-snapshot.js";
|
||||
import { createPreparedEmbeddedPiSettingsManager } from "./pi-project-settings.js";
|
||||
|
||||
type EmbeddedPiSettingsArgs = Parameters<typeof buildEmbeddedPiSettingsSnapshot>[0];
|
||||
|
||||
@@ -126,3 +130,48 @@ describe("buildEmbeddedPiSettingsSnapshot", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPreparedEmbeddedPiSettingsManager", () => {
|
||||
it("keeps trusted file-backed settings runtime-scoped after preparation", async () => {
|
||||
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-settings-"));
|
||||
try {
|
||||
const cwd = path.join(baseDir, "workspace");
|
||||
const agentDir = path.join(baseDir, "agent");
|
||||
const projectSettingsDir = path.join(cwd, ".pi");
|
||||
const agentSettingsPath = path.join(agentDir, "settings.json");
|
||||
await fs.mkdir(projectSettingsDir, { recursive: true });
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
agentSettingsPath,
|
||||
JSON.stringify({ retry: { enabled: true } }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(projectSettingsDir, "settings.json"),
|
||||
JSON.stringify({ shellCommandPrefix: "echo trusted &&" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const settingsManager = createPreparedEmbeddedPiSettingsManager({
|
||||
cwd,
|
||||
agentDir,
|
||||
cfg: {
|
||||
agents: { defaults: { embeddedPi: { projectSettingsPolicy: "trusted" } } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(settingsManager.getShellCommandPrefix()).toBe("echo trusted &&");
|
||||
expect(settingsManager.getRetryEnabled()).toBe(true);
|
||||
|
||||
settingsManager.setRetryEnabled(false);
|
||||
await settingsManager.flush();
|
||||
|
||||
const diskSettings = JSON.parse(await fs.readFile(agentSettingsPath, "utf8")) as {
|
||||
retry?: { enabled?: boolean };
|
||||
};
|
||||
expect(diskSettings.retry?.enabled).toBe(true);
|
||||
} finally {
|
||||
await fs.rm(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,17 @@ export function createEmbeddedPiSettingsManager(params: {
|
||||
return SettingsManager.inMemory(settings);
|
||||
}
|
||||
|
||||
function createRuntimeEmbeddedPiSettingsManager(settingsManager: SettingsManager): SettingsManager {
|
||||
return SettingsManager.inMemory(
|
||||
buildEmbeddedPiSettingsSnapshot({
|
||||
globalSettings: settingsManager.getGlobalSettings(),
|
||||
pluginSettings: {},
|
||||
projectSettings: settingsManager.getProjectSettings(),
|
||||
policy: "trusted",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function createPreparedEmbeddedPiSettingsManager(params: {
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
@@ -44,7 +55,9 @@ export function createPreparedEmbeddedPiSettingsManager(params: {
|
||||
/** Resolved context window budget so reserve-token floor can be capped for small models. */
|
||||
contextTokenBudget?: number;
|
||||
}): SettingsManager {
|
||||
const settingsManager = createEmbeddedPiSettingsManager(params);
|
||||
const settingsManager = createRuntimeEmbeddedPiSettingsManager(
|
||||
createEmbeddedPiSettingsManager(params),
|
||||
);
|
||||
applyPiCompactionSettingsFromConfig({
|
||||
settingsManager,
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -139,6 +139,18 @@ describe("brew helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not resolve brew from PATH entries", async () => {
|
||||
await withTempDir({ prefix: "openclaw-brew-" }, async (tmp) => {
|
||||
const pathBin = path.join(tmp, "path-bin");
|
||||
const pathBrew = path.join(pathBin, "brew");
|
||||
await writeExecutable(pathBrew);
|
||||
|
||||
const env: NodeJS.ProcessEnv = { PATH: pathBin };
|
||||
|
||||
expect(resolveBrewExecutable({ homeDir: path.join(tmp, "home"), env })).not.toBe(pathBrew);
|
||||
});
|
||||
});
|
||||
|
||||
it("always includes the standard macOS brew dirs after linuxbrew candidates", () => {
|
||||
const dirs = resolveBrewPathDirs({ homeDir: "/home/test" });
|
||||
|
||||
|
||||
@@ -680,6 +680,14 @@ describe("workspace .env blocklist completeness", () => {
|
||||
"OPENCLAW_NODE_EXEC_HOST",
|
||||
"OPENCLAW_NODE_EXEC_FALLBACK",
|
||||
"OPENCLAW_ALLOW_PROJECT_LOCAL_BIN",
|
||||
"PATH",
|
||||
"HOMEBREW_BREW_FILE",
|
||||
"HOMEBREW_PREFIX",
|
||||
"SystemRoot",
|
||||
"WINDIR",
|
||||
"ProgramFiles",
|
||||
"ProgramFiles(x86)",
|
||||
"ProgramW6432",
|
||||
"SYNOLOGY_CHAT_INCOMING_URL",
|
||||
"SYNOLOGY_NAS_HOST",
|
||||
];
|
||||
|
||||
@@ -74,10 +74,16 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_TEST_TAILSCALE_BINARY",
|
||||
"PI_CODING_AGENT_DIR",
|
||||
"PATH",
|
||||
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
|
||||
"PROGRAMFILES",
|
||||
"PROGRAMFILES(X86)",
|
||||
"PROGRAMW6432",
|
||||
"SYNOLOGY_CHAT_INCOMING_URL",
|
||||
"SYNOLOGY_NAS_HOST",
|
||||
"SYSTEMROOT",
|
||||
"UV_PYTHON",
|
||||
"WINDIR",
|
||||
]);
|
||||
|
||||
// Block endpoint redirection for any service without overfitting per-provider names.
|
||||
|
||||
@@ -90,6 +90,15 @@ vi.mock("./windows-port-pids.js", () => ({
|
||||
mockReadWindowsProcessArgsResult(pid, timeoutMs),
|
||||
}));
|
||||
|
||||
vi.mock("./windows-install-roots.js", () => ({
|
||||
getWindowsInstallRoots: () => ({
|
||||
systemRoot: "C:\\Windows",
|
||||
programFiles: "C:\\Program Files",
|
||||
programFilesX86: "C:\\Program Files (x86)",
|
||||
programW6432: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { resolveLsofCommandSync } from "./ports-lsof.js";
|
||||
let __testing: typeof import("./restart-stale-pids.js").__testing;
|
||||
let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
|
||||
@@ -980,8 +989,10 @@ describe.skipIf(isWindows)("restart-stale-pids", () => {
|
||||
|
||||
it("does not report Windows pids as killed when taskkill fails", () => {
|
||||
const origDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
const originalSystemRoot = process.env.SystemRoot;
|
||||
const stalePid = process.pid + 911;
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
process.env.SystemRoot = "C:\\PoisonedWindows";
|
||||
try {
|
||||
let fakeNow = 0;
|
||||
__testing.setDateNowOverride(() => fakeNow);
|
||||
@@ -1012,12 +1023,17 @@ describe.skipIf(isWindows)("restart-stale-pids", () => {
|
||||
|
||||
expect(cleanStaleGatewayProcessesSync()).toEqual([]);
|
||||
expect(mockSpawnSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("taskkill.exe"),
|
||||
"C:\\Windows\\System32\\taskkill.exe",
|
||||
["/T", "/PID", String(stalePid)],
|
||||
expect.objectContaining({ timeout: 5000 }),
|
||||
);
|
||||
} finally {
|
||||
__testing.setDateNowOverride(null);
|
||||
if (originalSystemRoot === undefined) {
|
||||
delete process.env.SystemRoot;
|
||||
} else {
|
||||
process.env.SystemRoot = originalSystemRoot;
|
||||
}
|
||||
if (origDescriptor) {
|
||||
Object.defineProperty(process, "platform", origDescriptor);
|
||||
}
|
||||
@@ -1063,13 +1079,13 @@ describe.skipIf(isWindows)("restart-stale-pids", () => {
|
||||
expect(cleanStaleGatewayProcessesSync()).toEqual([]);
|
||||
expect(mockSpawnSync).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.stringContaining("taskkill.exe"),
|
||||
"C:\\Windows\\System32\\taskkill.exe",
|
||||
["/T", "/PID", String(stalePid)],
|
||||
expect.objectContaining({ timeout: 5000 }),
|
||||
);
|
||||
expect(mockSpawnSync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.stringContaining("taskkill.exe"),
|
||||
"C:\\Windows\\System32\\taskkill.exe",
|
||||
["/F", "/T", "/PID", String(stalePid)],
|
||||
expect.objectContaining({ timeout: 5000 }),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { isGatewayArgv } from "./gateway-process-argv.js";
|
||||
import { resolveLsofCommandSync } from "./ports-lsof.js";
|
||||
import { getWindowsInstallRoots } from "./windows-install-roots.js";
|
||||
import {
|
||||
readWindowsListeningPidsOnPortSync,
|
||||
readWindowsListeningPidsResultSync,
|
||||
@@ -424,8 +425,8 @@ function terminateStaleProcessesSync(pids: number[]): number[] {
|
||||
* Sends a graceful taskkill first (/T for tree), waits, then escalates to /F.
|
||||
*/
|
||||
function terminateStaleProcessesWindows(pids: number[]): number[] {
|
||||
const taskkillPath = path.join(
|
||||
process.env.SystemRoot ?? "C:\\Windows",
|
||||
const taskkillPath = path.win32.join(
|
||||
getWindowsInstallRoots().systemRoot,
|
||||
"System32",
|
||||
"taskkill.exe",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user