fix(daemon): write LaunchAgent plist to boot volume when home is on external APFS#90923
fix(daemon): write LaunchAgent plist to boot volume when home is on external APFS#90923849261680 wants to merge 7 commits into
Conversation
|
Codex review: found issues before merge. Reviewed June 25, 2026, 9:36 PM ET / 01:36 UTC. Summary PR surface: Source +81, Tests +292, Other +86. Total +459 across 14 files. Reproducibility: yes. at source level. Current main and v2026.6.10 still derive the LaunchAgent plist from HOME, and the PR body includes macOS terminal proof showing launchctl error 5 from an external APFS plist and success from the boot-volume path. Review metrics: 2 noteworthy metrics.
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. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land one canonical boot-volume LaunchAgent path across macOS install, restart, uninstall, repair, inspection, update recovery, app lookup, and cleanup instructions, with the dirty branch state resolved before merge. Do we have a high-confidence way to reproduce the issue? Yes, at source level. Current main and v2026.6.10 still derive the LaunchAgent plist from HOME, and the PR body includes macOS terminal proof showing launchctl error 5 from an external APFS plist and success from the boot-volume path. Is this the best way to solve the issue? Yes, with one polish gap. A single LaunchAgent-home resolver reused by lifecycle and recovery paths is the maintainable fix; the safer finishing touch is to route Darwin cleanup hints through that same path instead of leaving a HOME-derived command. Full review comments:
Overall correctness: patch is correct AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 6830aa39eaa1. Label changesLabel justifications:
Evidence reviewedPR surface: Source +81, Tests +292, Other +86. Total +459 across 14 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
|
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
3e08860 to
43d7cac
Compare
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
…me resolver The detached restart handoff recomputed the LaunchAgent plist path directly from $HOME, so an external-APFS-home gateway could still bootstrap the external plist on an in-band reload/kickstart fallback and re-hit `Bootstrap failed: 5` after the install path was fixed. Resolve it through resolveLaunchAgentHomeDir, the same boot-volume-aware source the rest of the lifecycle uses, and add an external-HOME regression test for the handoff path.
…e resolver The supervisor restart fallback (infra/restart.ts) and the update restart helper script (cli/update-cli/restart-helper.ts) both recomputed the LaunchAgent plist path directly from $HOME and then `launchctl bootstrap`-ed it. For an external-APFS $HOME these re-hit `Bootstrap failed: 5` even after install was fixed. Route both through resolveLaunchAgentHomeDir so every launchd bootstrap path uses the same boot-volume plist location; logs and state stay under the user's real HOME. Adds an external-HOME regression test for the generated update restart script.
…ename Issue openclaw#60398's reported $HOME is the external volume root itself (/Volumes/MainDataDrive), whose last path segment is the volume name, not the user. Deriving the boot-volume account from path.basename($HOME) produced /Users/MainDataDrive — the wrong account. Resolve the account from the login identity instead (USER/LOGNAME, falling back to the process owner via os.userInfo() when a launchd-sanitized env drops them), case-preserved. Keep $HOME only when the login user is undeterminable. Adds a volume-root external HOME regression test.
d0ed6cc to
6931c23
Compare
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Current head
Fresh verification from this worktree: @clawsweeper re-review |
|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing due to inactivity. |
Summary
macOS
launchdrefuses to bootstrap a LaunchAgent plist that lives on an external APFS volume (Bootstrap failed: 5: Input/output error). When a user's$HOMEis on an external drive,openclaw gateway installalways fails.This PR makes the macOS LaunchAgent path resolution boot-volume-aware. A single helper,
resolveLaunchAgentHomeDir(env)(insrc/daemon/paths.ts), returns$HOMEnormally but falls back to the boot-volume home/Users/<user>when$HOMEis on an external volume (detected by comparingstatSync("/").devvsstatSync(home).dev). Every launchd lifecycle path derives from this one source, so install, repair, restart, uninstall, status/audit, and doctor service detection all converge on the same path launchd can actually bootstrap.Why this shape (vs. an install-only copy)
An earlier revision wrote a second boot-volume plist only at install time. That left
repair,restart, anduninstallstill bootstrapping from the external path (which fails with the same error 5), and split state across two plists. Making the resolver canonical avoids a second path and keeps every lifecycle command consistent. Correctness details handled:/Users/<user>/.Trash(same volume as the plist) instead of the external$HOME/.Trash; a cross-volumerenamewouldEXDEV-fail and leak the plist.src/daemon/inspect.ts) scans the boot-volumeLibrary/LaunchAgentsso an external-home install is actually detected.src/daemon/launchd-restart-handoff.ts) routes through the same resolver, so an in-band reload/kickstart fallback bootstraps the boot-volume plist instead of re-hitting error 5 against the external volume.src/infra/restart.ts) and the update restart helper script (src/cli/update-cli/restart-helper.ts) alsolaunchctl bootstrapthe plist; both now resolve through the same boot-volume-aware home. Logs and state stay under the user's real$HOME.USER/LOGNAME, else the process owner viaos.userInfo()), not from$HOME's structure — the reported$HOMEis the external volume root itself (/Volumes/MainDataDrive), whose last path segment is the volume name, not the user. Case is preserved.staton every poll.Behavior on a normal Mac (HOME on the boot volume): unchanged.
/and$HOMEreport the samest_dev(macOS firmlinks), so the resolver returns$HOMEexactly as before — no extra plist, no path change.Changed files:
src/daemon/paths.ts—resolveLaunchAgentHomeDir()+ memoized external-volume detection.src/daemon/launchd.ts— plist path, plist write, and uninstall trash all derive from the boot-volume-aware home (removed the install-only double-write helpers).src/daemon/inspect.ts— darwin user-scope service scan uses the boot-volume-aware home.src/daemon/launchd-restart-handoff.ts— detached restart plist path uses the boot-volume-aware home.src/infra/restart.ts/src/cli/update-cli/restart-helper.ts— supervisor and update restart bootstrap fallbacks use the boot-volume-aware home./Volumes/.../Users/testHOME, plus uninstall-trash, restart-handoff, supervisor-restart, and update-restart-script external-HOME regression tests.Rebased onto current
main.Verification
Real behavior proof (live macOS launchd + real APFS volume)
openclaw gateway installfails withBootstrap failed: 5: Input/output errorwhen$HOMEis on an external APFS volume, because launchd cannot bootstrap a plist from that volume. The fix resolves the plist (and its lifecycle siblings) to the boot volume.hdiutil), reallaunchctlin thegui/<uid>domain, and the realresolveLaunchAgentPlistPathfromsrc/daemon/launchd.ts. A harmless plist running/usr/bin/truewas used so no real gateway was started; everything was booted out and the image detached afterward.st_devdiffers from/.launchctl bootstrap gui/$UID <plist-on-external-volume>→ observe error 5.launchctl bootstrap gui/$UID <plist-on-boot-volume>→ observe success +launchctl print.resolveLaunchAgentPlistPathwithHOMEon the external volume and assert it returns the boot-volume path.Bootstrap failed: 5: Input/output error; the identical plist on the boot volume bootstraps with exit code 0 and is visible vialaunchctl print; and the patchedresolveLaunchAgentPlistPathredirects an external$HOMEto the boot-volume path while leaving a normal boot-volume$HOMEunchanged.openclaw gateway installof the real gateway service on an external home (avoided to not register a real launchd gateway on the test machine); a physically separate USB/Thunderbolt enclosure (an attached APFS disk image was used, which is a real, distinct APFS volume with a distinctst_dev).Fixes #60398