diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f4be2309..f7d924954f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/Daemon: forward `TMPDIR` into installed service environments so macOS LaunchAgent gateway runs can open SQLite temp/journal files reliably instead of failing with `SQLITE_CANTOPEN`. (#20512) Thanks @Clawborn. - iOS/Screen: move `WKWebView` lifecycle ownership into `ScreenWebView` coordinator and explicit attach/detach flow to reduce gesture/lifecycle crash risk (`__NSArrayM insertObject:atIndex:` paths) during screen tab updates. (#20366) Thanks @ngutman. - Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor. - Protocol/Apple: regenerate Swift gateway models for `push.test` so `pnpm protocol:check` stays green on main. Thanks @mbelinky. diff --git a/src/daemon/launchd.test.ts b/src/daemon/launchd.test.ts index 43222a0e29..8365879b28 100644 --- a/src/daemon/launchd.test.ts +++ b/src/daemon/launchd.test.ts @@ -151,6 +151,26 @@ describe("launchd install", () => { expect(bootstrapIndex).toBeGreaterThanOrEqual(0); expect(enableIndex).toBeLessThan(bootstrapIndex); }); + + it("writes TMPDIR to LaunchAgent environment when provided", async () => { + const env: Record = { + HOME: "/Users/test", + OPENCLAW_PROFILE: "default", + }; + const tmpDir = "/var/folders/xy/abc123/T/"; + await installLaunchAgent({ + env, + stdout: new PassThrough(), + programArguments: ["node", "-e", "process.exit(0)"], + environment: { TMPDIR: tmpDir }, + }); + + const plistPath = resolveLaunchAgentPlistPath(env); + const plist = state.files.get(plistPath) ?? ""; + expect(plist).toContain("EnvironmentVariables"); + expect(plist).toContain("TMPDIR"); + expect(plist).toContain(`${tmpDir}`); + }); }); describe("resolveLaunchAgentPlistPath", () => { diff --git a/src/daemon/service-env.test.ts b/src/daemon/service-env.test.ts index c9e86f7cf1..31a46c4990 100644 --- a/src/daemon/service-env.test.ts +++ b/src/daemon/service-env.test.ts @@ -1,3 +1,4 @@ +import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { resolveGatewayStateDir } from "./paths.js"; @@ -282,6 +283,22 @@ describe("buildServiceEnvironment", () => { } }); + it("forwards TMPDIR from the host environment", () => { + const env = buildServiceEnvironment({ + env: { HOME: "/home/user", TMPDIR: "/var/folders/xw/abc123/T/" }, + port: 18789, + }); + expect(env.TMPDIR).toBe("/var/folders/xw/abc123/T/"); + }); + + it("falls back to os.tmpdir when TMPDIR is not set", () => { + const env = buildServiceEnvironment({ + env: { HOME: "/home/user" }, + port: 18789, + }); + expect(env.TMPDIR).toBe(os.tmpdir()); + }); + it("uses profile-specific unit and label", () => { const env = buildServiceEnvironment({ env: { HOME: "/home/user", OPENCLAW_PROFILE: "work" }, @@ -301,6 +318,20 @@ describe("buildNodeServiceEnvironment", () => { }); expect(env.HOME).toBe("/home/user"); }); + + it("forwards TMPDIR for node services", () => { + const env = buildNodeServiceEnvironment({ + env: { HOME: "/home/user", TMPDIR: "/tmp/custom" }, + }); + expect(env.TMPDIR).toBe("/tmp/custom"); + }); + + it("falls back to os.tmpdir for node services when TMPDIR is not set", () => { + const env = buildNodeServiceEnvironment({ + env: { HOME: "/home/user" }, + }); + expect(env.TMPDIR).toBe(os.tmpdir()); + }); }); describe("resolveGatewayStateDir", () => { diff --git a/src/daemon/service-env.ts b/src/daemon/service-env.ts index ec6e1a7f3b..10fd4223c8 100644 --- a/src/daemon/service-env.ts +++ b/src/daemon/service-env.ts @@ -1,3 +1,4 @@ +import os from "node:os"; import path from "node:path"; import { VERSION } from "../version.js"; import { @@ -212,8 +213,11 @@ export function buildServiceEnvironment(params: { const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`; const stateDir = env.OPENCLAW_STATE_DIR; const configPath = env.OPENCLAW_CONFIG_PATH; + // Keep a usable temp directory for supervised services even when the host env omits TMPDIR. + const tmpDir = env.TMPDIR?.trim() || os.tmpdir(); return { HOME: env.HOME, + TMPDIR: tmpDir, PATH: buildMinimalServicePath({ env }), OPENCLAW_PROFILE: profile, OPENCLAW_STATE_DIR: stateDir, @@ -234,8 +238,10 @@ export function buildNodeServiceEnvironment(params: { const { env } = params; const stateDir = env.OPENCLAW_STATE_DIR; const configPath = env.OPENCLAW_CONFIG_PATH; + const tmpDir = env.TMPDIR?.trim() || os.tmpdir(); return { HOME: env.HOME, + TMPDIR: tmpDir, PATH: buildMinimalServicePath({ env }), OPENCLAW_STATE_DIR: stateDir, OPENCLAW_CONFIG_PATH: configPath,