惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
腾讯CDC
Jina AI
Jina AI
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
小众软件
小众软件
M
MIT News - Artificial intelligence
MyScale Blog
MyScale Blog
D
Docker
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
月光博客
月光博客
L
LangChain Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - Franky
C
Check Point Blog
U
Unit 42
人人都是产品经理
人人都是产品经理

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(gateway): harden macOS launchd service startup · open...
vincentkoc · 2026-04-27 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai

66
77

### Fixes

88
9+

- macOS Gateway: write launchd services with a state-dir `WorkingDirectory`, use a durable state-dir temp path instead of freezing macOS session `TMPDIR`, create that temp directory before bootstrap, and label abort-shaped launchd exits as `SIGABRT/abort` in status output. Fixes #53679 and #70223; refs #71848. Thanks @dlturock, @stammi922, and @palladius.

910

- Memory/QMD: prefer QMD's `--mask` collection pattern flag so root memory indexing stays scoped to `MEMORY.md` instead of widening to every markdown file in the workspace. Thanks @codex.

1011

- Codex harness: normalize cached input tokens before session/context accounting so prompt cache reads are not double-counted in `/status`, `session_status`, or persisted `sessionEntry.totalTokens`. Fixes #69298. Thanks @richardmqq.

1112

- Hooks/session-memory: use the host local timezone for memory filenames, fallback timestamp slugs, and markdown headers instead of UTC dates. Fixes #46703. (#46721) Thanks @Astro-Han.

Original file line numberDiff line numberDiff line change

@@ -66,12 +66,14 @@ function mockNodeGatewayPlanFixture(

6666

} = {},

6767

) {

6868

const {

69-

workingDirectory = "/Users/me",

7069

version = "22.0.0",

7170

supported = true,

7271

warning,

7372

serviceEnvironment = { OPENCLAW_PORT: "3000" },

7473

} = params;

74+

const workingDirectory = Object.hasOwn(params, "workingDirectory")

75+

? params.workingDirectory

76+

: "/Users/me";

7577

mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");

7678

mocks.resolveGatewayProgramArguments.mockResolvedValue({

7779

programArguments: ["node", "gateway"],

@@ -166,6 +168,43 @@ describe("buildGatewayInstallPlan", () => {

166168

expect(mocks.resolvePreferredNodePath).toHaveBeenCalled();

167169

});

168170
171+

it("uses the state dir as the default macOS launchd working directory", async () => {

172+

mockNodeGatewayPlanFixture({

173+

workingDirectory: undefined,

174+

serviceEnvironment: {},

175+

});

176+
177+

const plan = await buildGatewayInstallPlan({

178+

env: isolatedPlanEnv(),

179+

port: 3000,

180+

runtime: "node",

181+

platform: "darwin",

182+

});

183+
184+

expect(plan.workingDirectory).toBe(path.join(isolatedHome, ".openclaw"));

185+

expect(mocks.buildServiceEnvironment).toHaveBeenCalledWith(

186+

expect.objectContaining({

187+

platform: "darwin",

188+

}),

189+

);

190+

});

191+
192+

it("does not invent a working directory for non-macOS service installs", async () => {

193+

mockNodeGatewayPlanFixture({

194+

workingDirectory: undefined,

195+

serviceEnvironment: {},

196+

});

197+
198+

const plan = await buildGatewayInstallPlan({

199+

env: isolatedPlanEnv(),

200+

port: 3000,

201+

runtime: "node",

202+

platform: "linux",

203+

});

204+
205+

expect(plan.workingDirectory).toBeUndefined();

206+

});

207+
169208

it("merges safe config env while dropping unsafe values and keeping service precedence", async () => {

170209

mockNodeGatewayPlanFixture({

171210

serviceEnvironment: {

Original file line numberDiff line numberDiff line change

@@ -5,6 +5,7 @@ import { formatCliCommand } from "../cli/command-format.js";

55

import { collectDurableServiceEnvVars } from "../config/state-dir-dotenv.js";

66

import type { OpenClawConfig } from "../config/types.js";

77

import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";

8+

import { resolveGatewayStateDir } from "../daemon/paths.js";

89

import { resolveGatewayProgramArguments } from "../daemon/program-args.js";

910

import { buildServiceEnvironment } from "../daemon/service-env.js";

1011

import {

@@ -212,6 +213,20 @@ function collectPreservedExistingServiceEnvVars(

212213

return preserved;

213214

}

214215
216+

function resolveGatewayInstallWorkingDirectory(params: {

217+

env: Record<string, string | undefined>;

218+

platform: NodeJS.Platform;

219+

workingDirectory: string | undefined;

220+

}): string | undefined {

221+

if (params.workingDirectory) {

222+

return params.workingDirectory;

223+

}

224+

if (params.platform !== "darwin") {

225+

return undefined;

226+

}

227+

return resolveGatewayStateDir(params.env);

228+

}

229+
215230

async function buildGatewayInstallEnvironment(params: {

216231

env: Record<string, string | undefined>;

217232

config?: OpenClawConfig;

@@ -261,11 +276,13 @@ export async function buildGatewayInstallPlan(params: {

261276

existingEnvironment?: Record<string, string | undefined>;

262277

devMode?: boolean;

263278

nodePath?: string;

279+

platform?: NodeJS.Platform;

264280

warn?: DaemonInstallWarnFn;

265281

/** Full config to extract env vars from (env vars + inline env keys). */

266282

config?: OpenClawConfig;

267283

authStore?: AuthProfileStore;

268284

}): Promise<GatewayInstallPlan> {

285+

const platform = params.platform ?? process.platform;

269286

const { devMode, nodePath } = await resolveDaemonInstallRuntimeInputs({

270287

env: params.env,

271288

runtime: params.runtime,

@@ -289,16 +306,21 @@ export async function buildGatewayInstallPlan(params: {

289306

env: params.env,

290307

port: params.port,

291308

launchdLabel:

292-

process.platform === "darwin"

309+

platform === "darwin"

293310

? resolveGatewayLaunchAgentLabel(params.env.OPENCLAW_PROFILE)

294311

: undefined,

312+

platform,

295313

extraPathDirs: resolveDaemonNodeBinDir(nodePath),

296314

});

297315
298316

// Lowest to highest: preserved custom vars, durable config, auth env refs, generated service env.

299317

return {

300318

programArguments,

301-

workingDirectory,

319+

workingDirectory: resolveGatewayInstallWorkingDirectory({

320+

env: params.env,

321+

platform,

322+

workingDirectory,

323+

}),

302324

environment: await buildGatewayInstallEnvironment({

303325

env: params.env,

304326

config: params.config,

Original file line numberDiff line numberDiff line change

@@ -451,7 +451,7 @@ describe("launchd install", () => {

451451
452452

it("writes TMPDIR to LaunchAgent environment when provided", async () => {

453453

const env = createDefaultLaunchdEnv();

454-

const tmpDir = "/var/folders/xy/abc123/T/";

454+

const tmpDir = "/Users/test/.openclaw/tmp";

455455

await installLaunchAgent({

456456

env,

457457

stdout: new PassThrough(),

@@ -466,6 +466,20 @@ describe("launchd install", () => {

466466

expect(plist).toContain(`<string>${tmpDir}</string>`);

467467

});

468468
469+

it("creates the LaunchAgent TMPDIR before bootstrap", async () => {

470+

const env = createDefaultLaunchdEnv();

471+

const tmpDir = "/Users/test/.openclaw/tmp";

472+

await installLaunchAgent({

473+

env,

474+

stdout: new PassThrough(),

475+

programArguments: defaultProgramArguments,

476+

environment: { TMPDIR: tmpDir },

477+

});

478+
479+

expect(state.dirs.has(tmpDir)).toBe(true);

480+

expect(state.dirModes.get(tmpDir)).toBe(0o700);

481+

});

482+
469483

it("writes KeepAlive=true policy with restrictive umask", async () => {

470484

const env = createDefaultLaunchdEnv();

471485

await installLaunchAgent({

Original file line numberDiff line numberDiff line change

@@ -36,6 +36,7 @@ import type {

3636
3737

const LAUNCH_AGENT_DIR_MODE = 0o755;

3838

const LAUNCH_AGENT_PLIST_MODE = 0o644;

39+

const LAUNCH_AGENT_PRIVATE_DIR_MODE = 0o700;

3940
4041

function assertValidLaunchAgentLabel(label: string): string {

4142

const trimmed = label.trim();

@@ -209,12 +210,16 @@ async function bootstrapLaunchAgentOrThrow(params: {

209210

throw new Error(`launchctl bootstrap failed: ${detail}`);

210211

}

211212
212-

async function ensureSecureDirectory(targetPath: string): Promise<void> {

213-

await fs.mkdir(targetPath, { recursive: true, mode: LAUNCH_AGENT_DIR_MODE });

213+

async function ensureSecureDirectory(

214+

targetPath: string,

215+

dirMode = LAUNCH_AGENT_DIR_MODE,

216+

): Promise<void> {

217+

await fs.mkdir(targetPath, { recursive: true, mode: dirMode });

214218

try {

215219

const stat = await fs.stat(targetPath);

216220

const mode = stat.mode & 0o777;

217-

const tightenedMode = mode & ~0o022;

221+

const forbiddenMode = dirMode === LAUNCH_AGENT_PRIVATE_DIR_MODE ? 0o077 : 0o022;

222+

const tightenedMode = mode & ~forbiddenMode;

218223

if (tightenedMode !== mode) {

219224

await fs.chmod(targetPath, tightenedMode);

220225

}

@@ -223,6 +228,15 @@ async function ensureSecureDirectory(targetPath: string): Promise<void> {

223228

}

224229

}

225230
231+

async function ensureLaunchAgentEnvironmentDirectories(

232+

environment: Record<string, string | undefined> | undefined,

233+

): Promise<void> {

234+

const tmpDir = environment?.TMPDIR?.trim();

235+

if (tmpDir) {

236+

await ensureSecureDirectory(tmpDir, LAUNCH_AGENT_PRIVATE_DIR_MODE);

237+

}

238+

}

239+
226240

export type LaunchctlPrintInfo = {

227241

state?: string;

228242

pid?: number;

@@ -535,6 +549,7 @@ async function writeLaunchAgentPlist({

535549

await ensureSecureDirectory(home);

536550

await ensureSecureDirectory(libraryDir);

537551

await ensureSecureDirectory(path.dirname(plistPath));

552+

await ensureLaunchAgentEnvironmentDirectories(environment);

538553
539554

const serviceDescription = resolveGatewayServiceDescription({ env, environment, description });

540555

const plist = buildLaunchAgentPlist({

Original file line numberDiff line numberDiff line change

@@ -0,0 +1,10 @@

1+

import { describe, expect, it } from "vitest";

2+

import { formatRuntimeStatus } from "./runtime-format.js";

3+
4+

describe("formatRuntimeStatus", () => {

5+

it("labels abort-shaped launchd exit statuses", () => {

6+

expect(formatRuntimeStatus({ status: "stopped", lastExitStatus: 134 })).toContain(

7+

"last exit 134 (SIGABRT/abort)",

8+

);

9+

});

10+

});

Original file line numberDiff line numberDiff line change

@@ -12,6 +12,20 @@ export type ServiceRuntimeLike = {

1212

detail?: string;

1313

};

1414
15+

const SIGNAL_NAMES_BY_STATUS = new Map<number, string>([

16+

[129, "SIGHUP"],

17+

[130, "SIGINT"],

18+

[131, "SIGQUIT"],

19+

[134, "SIGABRT/abort"],

20+

[137, "SIGKILL"],

21+

[143, "SIGTERM"],

22+

]);

23+
24+

function formatLastExitStatus(status: number): string {

25+

const signalName = SIGNAL_NAMES_BY_STATUS.get(status);

26+

return signalName ? `last exit ${status} (${signalName})` : `last exit ${status}`;

27+

}

28+
1529

export function formatRuntimeStatus(runtime: ServiceRuntimeLike | undefined): string | null {

1630

if (!runtime) {

1731

return null;

@@ -21,7 +35,7 @@ export function formatRuntimeStatus(runtime: ServiceRuntimeLike | undefined): st

2135

details.push(`sub ${runtime.subState}`);

2236

}

2337

if (runtime.lastExitStatus !== undefined) {

24-

details.push(`last exit ${runtime.lastExitStatus}`);

38+

details.push(formatLastExitStatus(runtime.lastExitStatus));

2539

}

2640

if (runtime.lastExitReason) {

2741

details.push(`reason ${runtime.lastExitReason}`);

Original file line numberDiff line numberDiff line change

@@ -398,18 +398,29 @@ describe("buildServiceEnvironment", () => {

398398

}

399399

});

400400
401-

it("forwards TMPDIR from the host environment", () => {

401+

it("forwards TMPDIR from the host environment on Linux", () => {

402402

const env = buildServiceEnvironment({

403403

env: { HOME: "/home/user", TMPDIR: "/var/folders/xw/abc123/T/" },

404404

port: 18789,

405+

platform: "linux",

405406

});

406407

expect(env.TMPDIR).toBe("/var/folders/xw/abc123/T/");

407408

});

408409
409-

it("falls back to os.tmpdir when TMPDIR is not set", () => {

410+

it("uses a durable state temp directory for macOS LaunchAgents", () => {

411+

const env = buildServiceEnvironment({

412+

env: { HOME: "/Users/user", TMPDIR: "/var/folders/xw/abc123/T/" },

413+

port: 18789,

414+

platform: "darwin",

415+

});

416+

expect(env.TMPDIR).toBe(path.join("/Users/user", ".openclaw", "tmp"));

417+

});

418+
419+

it("falls back to os.tmpdir when TMPDIR is not set on Linux", () => {

410420

const env = buildServiceEnvironment({

411421

env: { HOME: "/home/user" },

412422

port: 18789,

423+

platform: "linux",

413424

});

414425

expect(env.TMPDIR).toBe(os.tmpdir());

415426

});

@@ -519,16 +530,26 @@ describe("buildNodeServiceEnvironment", () => {

519530

expect(env.no_proxy).toBe("localhost,127.0.0.1");

520531

});

521532
522-

it("forwards TMPDIR for node services", () => {

533+

it("forwards TMPDIR for node services on Linux", () => {

523534

const env = buildNodeServiceEnvironment({

524535

env: { HOME: "/home/user", TMPDIR: "/tmp/custom" },

536+

platform: "linux",

525537

});

526538

expect(env.TMPDIR).toBe("/tmp/custom");

527539

});

528540
529-

it("falls back to os.tmpdir for node services when TMPDIR is not set", () => {

541+

it("uses a durable state temp directory for macOS node services", () => {

542+

const env = buildNodeServiceEnvironment({

543+

env: { HOME: "/Users/user", TMPDIR: "/var/folders/xw/abc123/T/" },

544+

platform: "darwin",

545+

});

546+

expect(env.TMPDIR).toBe(path.join("/Users/user", ".openclaw", "tmp"));

547+

});

548+
549+

it("falls back to os.tmpdir for node services when TMPDIR is not set on Linux", () => {

530550

const env = buildNodeServiceEnvironment({

531551

env: { HOME: "/home/user" },

552+

platform: "linux",

532553

});

533554

expect(env.TMPDIR).toBe(os.tmpdir());

534555

});

Original file line numberDiff line numberDiff line change

@@ -20,6 +20,7 @@ import {

2020

resolveNodeSystemdServiceName,

2121

resolveNodeWindowsTaskName,

2222

} from "./constants.js";

23+

import { resolveGatewayStateDir } from "./paths.js";

2324
2425

export { isNodeVersionManagerRuntime, resolveLinuxSystemCaBundle };

2526

@@ -360,6 +361,20 @@ function buildCommonServiceEnvironment(

360361

return serviceEnv;

361362

}

362363
364+

function resolveServiceTmpDir(

365+

env: Record<string, string | undefined>,

366+

platform: NodeJS.Platform,

367+

): string {

368+

if (platform === "darwin") {

369+

try {

370+

return path.join(resolveGatewayStateDir(env), "tmp");

371+

} catch {

372+

return env.TMPDIR?.trim() || os.tmpdir();

373+

}

374+

}

375+

return env.TMPDIR?.trim() || os.tmpdir();

376+

}

377+
363378

function resolveSharedServiceEnvironmentFields(

364379

env: Record<string, string | undefined>,

365380

platform: NodeJS.Platform,

@@ -368,8 +383,7 @@ function resolveSharedServiceEnvironmentFields(

368383

): SharedServiceEnvironmentFields {

369384

const stateDir = env.OPENCLAW_STATE_DIR;

370385

const configPath = env.OPENCLAW_CONFIG_PATH;

371-

// Keep a usable temp directory for supervised services even when the host env omits TMPDIR.

372-

const tmpDir = env.TMPDIR?.trim() || os.tmpdir();

386+

const tmpDir = resolveServiceTmpDir(env, platform);

373387

const proxyEnv = readServiceProxyEnvironment(env);

374388

// On macOS, launchd services don't inherit the shell environment, so Node's undici/fetch

375389

// cannot locate the system CA bundle. Default to /etc/ssl/cert.pem so TLS verification