fix(macos): LaunchAgent starts gateway on external home volumes#89967
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 3, 2026, 4:53 PM ET / 20:53 UTC. Summary PR surface: Source +30, Tests +59. Total +89 across 3 files. Reproducibility: yes. at source level: current main still puts the generated wrapper in ProgramArguments[0] when service env entries exist, and the linked issue plus PR proof cover the launchd failure path. I did not run a fresh live external-home reproduction in this read-only review. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the shell-first LaunchAgent env-wrapper fix after maintainer acceptance of the compatibility tradeoff, while keeping external-APFS plist-location work separate. Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main still puts the generated wrapper in ProgramArguments[0] when service env entries exist, and the linked issue plus PR proof cover the launchd failure path. I did not run a fresh live external-home reproduction in this read-only review. Is this the best way to solve the issue? Yes. Fixing the raw launchd executable shape at the LaunchAgent generation/readback boundary is narrower than changing gateway command construction or status parsing, while the external-APFS plist-location problem remains separate. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 010b61746379. Label changesLabel justifications:
Evidence reviewedPR surface: Source +30, Tests +59. Total +89 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
5d98e18 to
9a5d665
Compare
Maintainer live proofLand-ready review complete at
Known proof boundary: this VM does not have a literal migrated external-home volume. Making the generated wrapper non-executable exercises the same direct-wrapper |
9a5d665 to
cf2a74c
Compare
|
Merged via squash.
|
Fixes #87199
Summary
posix_spawnbefore the gateway starts.ProgramArguments(/bin/sh, wrapper path, env file path, then the gateway command) and keep legacy wrapper-first plists readable/rewriteable.src/daemon/launchd.ts,src/daemon/launchd-plist.ts, andsrc/daemon/launchd.test.tsnow share the shell-first env-wrapper contract.gateway --port <port>run path.Real behavior proof
Behavior or issue addressed: A generated macOS LaunchAgent must no longer directly spawn the generated env-wrapper from the user's home/state volume. The LaunchAgent should bootstrap and kickstart under macOS launchd, load the env file, start the gateway, and answer gateway health/status without
Permission denied,EX_CONFIG, orposix_spawnfailure.Real environment tested: macOS Darwin 25.5.0 arm64, Node v24.15.0, OpenClaw 2026.6.2 from PR worktree SHA
e977dc1fc177. The proof used a temporary HOME/OPENCLAW_HOME/OPENCLAW_STATE_DIR and unique labelai.openclaw.launchd-proof-fa9d9c1f, then changed the generated wrapper mode from0700to0600beforelaunchctl bootstrapto exercise the direct-wrapper-spawn permission surface.Exact steps or command run after this patch:
PROOF_REPO=/Users/zhangguiping/daily_pr_work/openclaw-87199-proof \ PROOF_PORT=19532 \ PROOF_PRINT_CLEANUP=1 \ node --import tsx /Users/zhangguiping/daily_pr_work/proofs/openclaw-issue-87199-evidence/macos-launchd-live-proof.mjs \ > /Users/zhangguiping/daily_pr_work/proofs/openclaw-issue-87199-evidence/macos-launchd-live-proof-2.out \ 2> /Users/zhangguiping/daily_pr_work/proofs/openclaw-issue-87199-evidence/macos-launchd-live-proof-2.err launchctl bootout gui/501/ai.openclaw.launchd-proof-fa9d9c1f launchctl bootstrap gui/501 /var/folders/76/w5rbbmt53d9f9wzqs3kzq3580000gn/T/openclaw-launchd-proof-fa9d9c1f-As3pt8/Library/LaunchAgents/ai.openclaw.launchd-proof-fa9d9c1f.plist launchctl enable gui/501/ai.openclaw.launchd-proof-fa9d9c1f launchctl kickstart -k gui/501/ai.openclaw.launchd-proof-fa9d9c1f launchctl print gui/501/ai.openclaw.launchd-proof-fa9d9c1f node --import tsx src/entry.ts gateway call health --json --url ws://127.0.0.1:19532 --token <redacted> --timeout 5000 node --import tsx src/entry.ts gateway status --json --url ws://127.0.0.1:19532 --token <redacted> launchctl bootout gui/501/ai.openclaw.launchd-proof-fa9d9c1f node scripts/run-vitest.mjs src/daemon/launchd.test.ts src/daemon/program-args.test.ts src/commands/daemon-install-helpers.test.ts pnpm format:check src/daemon/launchd.ts src/daemon/launchd-plist.ts src/daemon/launchd.test.ts node scripts/run-oxlint.mjs src/daemon/launchd.ts src/daemon/launchd-plist.ts src/daemon/launchd.test.ts git diff --checkEvidence after fix: Live macOS launchd proof output from
macos-launchd-live-proof-2.out:{ "platform": "Darwin 25.5.0", "arch": "arm64", "node": "v24.15.0", "head": "e977dc1fc177", "label": "ai.openclaw.launchd-proof-fa9d9c1f", "serviceTarget": "gui/501/ai.openclaw.launchd-proof-fa9d9c1f", "port": 19532, "programArguments": [ "/bin/sh", "/var/folders/.../.openclaw-state/service-env/ai.openclaw.launchd-proof-fa9d9c1f-env-wrapper.sh", "/var/folders/.../.openclaw-state/service-env/ai.openclaw.launchd-proof-fa9d9c1f.env", "/Users/zhangguiping/.nvm/versions/node/v24.15.0/bin/node", "--import", "tsx", "/Users/zhangguiping/daily_pr_work/openclaw-87199-proof/src/entry.ts", "gateway", "--port", "19532", "--allow-unconfigured" ], "plistExecutable": "/bin/sh", "secretInPlist": false, "envFileExists": true, "envFileContainsToken": true, "wrapperModeBefore": "0700", "wrapperModeAfter": "0600", "bootstrap": { "code": 0 }, "enable": { "code": 0 }, "kickstart": { "code": 0 }, "runtime": { "status": "running", "state": "active", "pid": 85513, "cachedLabel": false }, "gatewayHealth": { "code": 0, "stdout": { "ok": true } }, "gatewayStatus": { "code": 0, "rpc": { "ok": true } }, "gatewayStatusRpcOk": true, "noPermissionDenied": true }launchctl print gui/501/ai.openclaw.launchd-proof-fa9d9c1fshowed the real service running from/bin/sh:Gateway RPC proof from the launchd-started process:
Observed result after fix: macOS launchd successfully bootstrapped and kickstarted the generated LaunchAgent even after the generated wrapper was made non-executable (
0600). The service ran as/bin/sh, loaded environment values from the generated env file, started the gateway on port 19532, andgateway call healthplusgateway statusboth succeeded against the launchd-owned process. The live proof also confirms secrets stayed out of the plist (secretInPlist=false) while the env file contained the token (envFileContainsToken=true). The gateway command remains the existing supportedgateway --port <port>form; this PR does not change it to a literalgateway runsubcommand, and the launchd health/status proof shows that command starts the gateway correctly.What was not tested: I did not mount a separate external home volume. Instead, the proof used a temporary macOS HOME/state directory and changed the generated wrapper to
0600before launchd bootstrap to cover the same direct-spawn permission failure surface that this patch removes. No other known gaps.Fix classification: Root cause fix.
Review findings addressed
launchctl bootstrap,launchctl kickstart,launchctl print,gateway call health, andgateway statusagainst the generated service./bin/shis the program launchd starts, confirms owner-only env-file loading, and confirms there is noPermission denied,EX_CONFIG, orposix_spawnfailure in the launchd-started gateway path.Regression Test Plan
src/daemon/launchd.test.ts.ProgramArgumentsas/bin/sh, generated wrapper path, generated env file path, then the original gateway command; an old wrapper-first plist reads back and restart-rewrites into that same shell-first shape.src/daemon/program-args.test.tsremains green to confirm the existinggateway --port <port>command form is unchanged.src/commands/daemon-install-helpers.test.tsremains green to confirm service env planning still passes wrapper/env values through correctly.Root Cause
ProgramArgumentsas[wrapperPath, envFilePath, ...gatewayCommand]. On macOS setups where the generated wrapper lives on a permission-constrained home/state volume, launchd could fail atposix_spawnbefore the wrapper loaded the env file or execed the gateway. Status/readback code normalized the command for display, which hid the raw plist shape that launchd actually executes.launchctl bootstrap/kickstartagainst the generated LaunchAgent.Merge risk
EnvironmentVariables, Linux/Windows service behavior, or thegateway --port <port>command shape.prepareLaunchAgentProgramArgumentsplusreadLaunchAgentProgramArgumentsFromFile; generation and restart repair now share the same canonical shell-first wrapper contract while legacy wrapper-first plists are normalized only during read/rewrite.merge-risk:platform,merge-risk:compatibility.gateway --port <port>run path rather than changing CLI command construction in this PR.