diff --git a/CHANGELOG.md b/CHANGELOG.md index 378947ed5ea..0f4f7ff6edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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-` 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. diff --git a/extensions/whatsapp/src/media.test.ts b/extensions/whatsapp/src/media.test.ts index a03edff1a84..029c03baa33 100644 --- a/extensions/whatsapp/src/media.test.ts +++ b/extensions/whatsapp/src/media.test.ts @@ -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); diff --git a/src/infra/tmp-openclaw-dir.test.ts b/src/infra/tmp-openclaw-dir.test.ts index c427007d38a..b9c4260f43e 100644 --- a/src/infra/tmp-openclaw-dir.test.ts +++ b/src/infra/tmp-openclaw-dir.test.ts @@ -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); + }); }); diff --git a/src/infra/tmp-openclaw-dir.ts b/src/infra/tmp-openclaw-dir.ts index afd93a85d1c..a69a6eaf7a0 100644 --- a/src/infra/tmp-openclaw-dir.ts +++ b/src/infra/tmp-openclaw-dir.ts @@ -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;