fix(infra): skip POSIX tmp path on Windows (#73533)

Skip the POSIX `/tmp/openclaw` preferred path on Windows so temp files land under the trusted `os.tmpdir()`/`%TEMP%`-based `openclaw-<uid>` path instead of `C:\tmp\openclaw`.

Add regression coverage for Windows path selection and the WhatsApp media temp directory integration, plus a changelog entry.

Fixes #60713.

Tests:
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/infra/tmp-openclaw-dir.ts src/infra/tmp-openclaw-dir.test.ts extensions/whatsapp/src/media.test.ts
- pnpm test src/infra/tmp-openclaw-dir.test.ts extensions/whatsapp/src/media.test.ts
- pnpm check:changed

Thanks @juan-flores077.

Co-authored-by: Juan Flores <112629487+juan-flores077@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
This commit is contained in:
Juan Flores
2026-05-05 03:32:36 +02:00
committed by GitHub
parent 04b7e4894d
commit 46a04099a4
4 changed files with 78 additions and 3 deletions

View File

@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Infra/Windows: skip the POSIX `/tmp/openclaw` preferred path on Windows in `resolvePreferredOpenClawTmpDir` so log files, TTS temp files, and other writes land in `%TEMP%\openclaw-<uid>` instead of `C:\tmp\openclaw`. Fixes #60713. Thanks @juan-flores077.
- Media/Windows: open saved attachment temp files read/write before fsync so Windows WebChat and `chat.send` media offloads no longer fail with EPERM during durability flush. (#76593) Thanks @qq230849622-a11y.
- Agents/tools: honor narrow runtime tool allowlists when constructing embedded-runner tool families and bundled MCP/LSP runtimes, so cron/subagent runs that request tools such as `update_plan`, `browser`, `x_search`, channel login tools, or `group:plugins` no longer start with missing tools or unrelated bootstrap work. (#77519, #77532)
- Codex plugin: mirror the experimental upstream app-server protocol and format generated TypeScript before drift checks, keeping OpenClaw's `experimentalApi` bridge compatible with latest Codex while preserving formatter gates.

View File

@@ -352,6 +352,10 @@ describe("local media root guard", () => {
const actualLstat = await fs.lstat(tinyPngFile);
const actualStat = await fs.stat(tinyPngFile);
const zeroDev = typeof actualLstat.dev === "bigint" ? 0n : 0;
// Resolve before mocking platform: under `win32` the helper returns the
// os.tmpdir() fallback rather than the POSIX `/tmp/openclaw` root that
// actually holds `tinyPngFile` on this Linux test runner (#60713).
const realTmpRoot = resolvePreferredOpenClawTmpDir();
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const lstatSpy = vi
@@ -361,7 +365,7 @@ describe("local media root guard", () => {
try {
const result = await loadWebMedia(tinyPngFile, 1024 * 1024, {
localRoots: [resolvePreferredOpenClawTmpDir()],
localRoots: [realTmpRoot],
});
expect(result.kind).toBe("image");
expect(result.buffer.length).toBeGreaterThan(0);

View File

@@ -512,4 +512,53 @@ describe("resolvePreferredOpenClawTmpDir", () => {
}),
).toThrow(/Unable to create fallback OpenClaw temp dir/);
});
it("skips the POSIX preferred path on Windows even when /tmp is accessible (#60713)", () => {
// Node on Windows resolves the POSIX path `/tmp` to `C:\tmp` against the
// current drive root. If `C:\tmp` happens to exist (Git, MSYS2, etc.
// create it), the previous code path returned `/tmp/openclaw` and routed
// log files / TTS temp files there instead of `%TEMP%\openclaw`. The
// platform: "win32" branch must skip the POSIX path entirely.
const winFallback = path.win32.join("C:\\Users\\u\\AppData\\Local\\Temp", "openclaw-501");
const accessSync = vi.fn();
const lstatSync = vi.fn((target: string) => {
if (target === POSIX_OPENCLAW_TMP_DIR || target === winFallback) {
return secureDirStat();
}
throw nodeErrorWithCode("ENOENT");
});
const mkdirSync = vi.fn();
const chmodSync = vi.fn();
const tmpdir = vi.fn(() => "C:\\Users\\u\\AppData\\Local\\Temp");
const result = resolvePreferredOpenClawTmpDir({
platform: "win32",
accessSync,
lstatSync,
mkdirSync,
chmodSync,
getuid: vi.fn(() => 501),
tmpdir,
warn: vi.fn(),
});
expect(result).toBe(winFallback);
expect(result).not.toBe(POSIX_OPENCLAW_TMP_DIR);
expect(tmpdir).toHaveBeenCalled();
});
it("still uses the POSIX preferred path on non-Windows platforms when available", () => {
const result = resolvePreferredOpenClawTmpDir({
platform: "linux",
accessSync: vi.fn(),
lstatSync: vi.fn(() => secureDirStat()),
mkdirSync: vi.fn(),
chmodSync: vi.fn(),
getuid: vi.fn(() => 501),
tmpdir: vi.fn(() => "/var/fallback"),
warn: vi.fn(),
});
expect(result).toBe(POSIX_OPENCLAW_TMP_DIR);
});
});

View File

@@ -30,8 +30,13 @@ function isNodeErrorWithCode(err: unknown, code: string): err is MaybeNodeError
);
}
type ResolvePreferredOpenClawTmpDirInternalOptions = ResolvePreferredOpenClawTmpDirOptions & {
/** Test seam for the host platform; defaults to `process.platform`. */
platform?: NodeJS.Platform;
};
export function resolvePreferredOpenClawTmpDir(
options: ResolvePreferredOpenClawTmpDirOptions = {},
options: ResolvePreferredOpenClawTmpDirInternalOptions = {},
): string {
// Evaluated here (not at module load) so this file is safe to import in browser bundles.
const TMP_DIR_ACCESS_MODE = fs.constants.W_OK | fs.constants.X_OK;
@@ -50,6 +55,7 @@ export function resolvePreferredOpenClawTmpDir(
}
});
const tmpdir = typeof options.tmpdir === "function" ? options.tmpdir : getOsTmpDir;
const platform = options.platform ?? process.platform;
const uid = getuid();
const isSecureDirForUser = (st: { mode?: number; uid?: number }): boolean => {
@@ -69,7 +75,11 @@ export function resolvePreferredOpenClawTmpDir(
const fallback = (): string => {
const base = tmpdir();
const suffix = uid === undefined ? "openclaw" : `openclaw-${uid}`;
return path.join(base, suffix);
// Use the platform-specific joiner so Windows fallbacks stay in pure
// backslash form even when the host process is non-Windows (e.g. when
// tests inject `platform: "win32"` on a Linux runner).
const joiner = platform === "win32" ? path.win32.join : path.join;
return joiner(base, suffix);
};
const isTrustedTmpDir = (st: {
@@ -155,6 +165,17 @@ export function resolvePreferredOpenClawTmpDir(
return fallbackPath;
};
// On Windows, Node resolves the POSIX path `/tmp` to `C:\tmp` (relative to
// the current drive root). Many Windows hosts have `C:\tmp` because Git,
// MSYS2, and other Unix-compat tools create it; the existing logic then
// happily writes logs and TTS files to `C:\tmp\openclaw\` while every
// other code path expects `%TEMP%\openclaw\`. Skip the POSIX preferred
// path entirely on Windows so the function falls through to the
// os.tmpdir() fallback (#60713).
if (platform === "win32") {
return ensureTrustedFallbackDir();
}
const existingPreferredState = resolveDirState(POSIX_OPENCLAW_TMP_DIR);
if (existingPreferredState === "available") {
return POSIX_OPENCLAW_TMP_DIR;