From 4507a64d22522af3143671a67a34f4d9fd3afa80 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 18 Feb 2026 21:04:55 -0500 Subject: [PATCH] fix(daemon): scope TMPDIR forwarding to macOS services --- src/daemon/launchd.test.ts | 20 +++++++++++++++++++ src/daemon/service-env.test.ts | 35 +++++++++++++++++++++++++++++++--- src/daemon/service-env.ts | 8 ++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) 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 ea67a0c31d..be226a4f32 100644 --- a/src/daemon/service-env.test.ts +++ b/src/daemon/service-env.test.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { resolveGatewayStateDir } from "./paths.js"; import { buildMinimalServicePath, @@ -9,6 +9,16 @@ import { getMinimalServicePathPartsFromEnv, } from "./service-env.js"; +const originalPlatform = process.platform; + +function setPlatform(platform: NodeJS.Platform): void { + Object.defineProperty(process, "platform", { value: platform, configurable: true }); +} + +afterEach(() => { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }); +}); + describe("getMinimalServicePathParts - Linux user directories", () => { it("includes user bin directories when HOME is set on Linux", () => { const result = getMinimalServicePathParts({ @@ -282,7 +292,8 @@ describe("buildServiceEnvironment", () => { } }); - it("forwards TMPDIR from the host environment", () => { + it("forwards TMPDIR from the host environment on macOS", () => { + setPlatform("darwin"); const env = buildServiceEnvironment({ env: { HOME: "/home/user", TMPDIR: "/var/folders/xw/abc123/T/" }, port: 18789, @@ -290,6 +301,15 @@ describe("buildServiceEnvironment", () => { expect(env.TMPDIR).toBe("/var/folders/xw/abc123/T/"); }); + it("does not forward TMPDIR on non-macOS services", () => { + setPlatform("linux"); + const env = buildServiceEnvironment({ + env: { HOME: "/home/user", TMPDIR: "/tmp/custom" }, + port: 18789, + }); + expect(env.TMPDIR).toBeUndefined(); + }); + it("omits TMPDIR when not set in host environment", () => { const env = buildServiceEnvironment({ env: { HOME: "/home/user" }, @@ -318,12 +338,21 @@ describe("buildNodeServiceEnvironment", () => { expect(env.HOME).toBe("/home/user"); }); - it("forwards TMPDIR for node services", () => { + it("forwards TMPDIR for node services on macOS", () => { + setPlatform("darwin"); const env = buildNodeServiceEnvironment({ env: { HOME: "/home/user", TMPDIR: "/tmp/custom" }, }); expect(env.TMPDIR).toBe("/tmp/custom"); }); + + it("does not forward TMPDIR for node services on non-macOS platforms", () => { + setPlatform("linux"); + const env = buildNodeServiceEnvironment({ + env: { HOME: "/home/user", TMPDIR: "/tmp/custom" }, + }); + expect(env.TMPDIR).toBeUndefined(); + }); }); describe("resolveGatewayStateDir", () => { diff --git a/src/daemon/service-env.ts b/src/daemon/service-env.ts index dab858dc05..b9590841c2 100644 --- a/src/daemon/service-env.ts +++ b/src/daemon/service-env.ts @@ -212,9 +212,11 @@ export function buildServiceEnvironment(params: { const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`; const stateDir = env.OPENCLAW_STATE_DIR; const configPath = env.OPENCLAW_CONFIG_PATH; + // launchd on macOS does not inherit shell TMPDIR by default; forward it explicitly there. + const tmpDir = process.platform === "darwin" ? env.TMPDIR : undefined; return { HOME: env.HOME, - TMPDIR: env.TMPDIR, + TMPDIR: tmpDir, PATH: buildMinimalServicePath({ env }), OPENCLAW_PROFILE: profile, OPENCLAW_STATE_DIR: stateDir, @@ -235,9 +237,11 @@ export function buildNodeServiceEnvironment(params: { const { env } = params; const stateDir = env.OPENCLAW_STATE_DIR; const configPath = env.OPENCLAW_CONFIG_PATH; + // Keep TMPDIR propagation scoped to macOS launchd installs. + const tmpDir = process.platform === "darwin" ? env.TMPDIR : undefined; return { HOME: env.HOME, - TMPDIR: env.TMPDIR, + TMPDIR: tmpDir, PATH: buildMinimalServicePath({ env }), OPENCLAW_STATE_DIR: stateDir, OPENCLAW_CONFIG_PATH: configPath,