From 69aa3df116d38141626fcdc29fc16b5f31f08d6c Mon Sep 17 00:00:00 2001 From: Josh Palmer Date: Sun, 8 Feb 2026 17:28:22 -0800 Subject: [PATCH] macOS: honor stable Nix defaults suite (#12205) * macOS: honor Nix defaults suite; auto launch in Nix mode Fixes repeated onboarding in Nix deployments by detecting nixMode from the stable defaults suite (ai.openclaw.mac) and bridging key settings into the current defaults domain. Also enables LaunchAgent autostart by default in Nix mode (escape hatch: openclaw.nixAutoLaunchAtLogin=false). * macOS: keep Nix mode fix focused Drop the automatic launch-at-login behavior from the Nix defaults patch; keep this PR scoped to reliable nixMode detection + defaults bridging. * macOS: simplify nixMode fix Remove the defaults-bridging helper and rely on a single, stable defaults suite (ai.openclaw.mac) for nixMode detection when running as an app bundle. This keeps the fix focused on onboarding suppression and rename churn resilience. * macOS: fix nixMode defaults suite churn (#12205) --- CHANGELOG.md | 1 + apps/macos/Sources/OpenClaw/Constants.swift | 2 + apps/macos/Sources/OpenClaw/MenuBar.swift | 1 + .../OpenClaw/ProcessInfo+OpenClaw.swift | 27 ++++++++++- .../NixModeStableSuiteTests.swift | 46 +++++++++++++++++++ .../WideAreaGatewayDiscoveryTests.swift | 1 + 6 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 apps/macos/Tests/OpenClawIPCTests/NixModeStableSuiteTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b879f2b9..e4a2d57c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - State dir: honor `OPENCLAW_STATE_DIR` for default device identity and canvas storage paths. (#4824) Thanks @kossoy. - Doctor/State dir: suppress repeated legacy migration warnings only for valid symlink mirrors, while keeping warnings for empty or invalid legacy trees. (#11709) Thanks @gumadeiras. - Tests: harden flaky hotspots by removing timer sleeps, consolidating onboarding provider-auth coverage, and improving memory test realism. (#11598) Thanks @gumadeiras. +- macOS: honor Nix-managed defaults suite (`ai.openclaw.mac`) for nixMode to prevent onboarding from reappearing after bundle-id churn. (#12205) Thanks @joshp123. ## 2026.2.6 diff --git a/apps/macos/Sources/OpenClaw/Constants.swift b/apps/macos/Sources/OpenClaw/Constants.swift index 40a0acbba..7065702d6 100644 --- a/apps/macos/Sources/OpenClaw/Constants.swift +++ b/apps/macos/Sources/OpenClaw/Constants.swift @@ -1,5 +1,7 @@ import Foundation +// Stable identifier used for both the macOS LaunchAgent label and Nix-managed defaults suite. +// nix-openclaw writes app defaults into this suite to survive app bundle identifier churn. let launchdLabel = "ai.openclaw.mac" let gatewayLaunchdLabel = "ai.openclaw.gateway" let onboardingVersionKey = "openclaw.onboardingVersion" diff --git a/apps/macos/Sources/OpenClaw/MenuBar.swift b/apps/macos/Sources/OpenClaw/MenuBar.swift index e2af09232..406d4e063 100644 --- a/apps/macos/Sources/OpenClaw/MenuBar.swift +++ b/apps/macos/Sources/OpenClaw/MenuBar.swift @@ -33,6 +33,7 @@ struct OpenClawApp: App { init() { OpenClawLogging.bootstrapIfNeeded() + Self.applyAttachOnlyOverrideIfNeeded() _state = State(initialValue: AppStateStore.shared) } diff --git a/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift b/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift index 65ea48e0f..d05e59338 100644 --- a/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift +++ b/apps/macos/Sources/OpenClaw/ProcessInfo+OpenClaw.swift @@ -6,9 +6,32 @@ extension ProcessInfo { return String(cString: raw) == "1" } + /// Nix deployments may write defaults into a stable suite (`ai.openclaw.mac`) even if the shipped + /// app bundle identifier changes (and therefore `UserDefaults.standard` domain changes). + static func resolveNixMode( + environment: [String: String], + standard: UserDefaults, + stableSuite: UserDefaults?, + isAppBundle: Bool + ) -> Bool { + if environment["OPENCLAW_NIX_MODE"] == "1" { return true } + if standard.bool(forKey: "openclaw.nixMode") { return true } + + // Only consult the stable suite when running as a .app bundle. + // This avoids local developer machines accidentally influencing unit tests. + if isAppBundle, let stableSuite, stableSuite.bool(forKey: "openclaw.nixMode") { return true } + + return false + } + var isNixMode: Bool { - if let raw = getenv("OPENCLAW_NIX_MODE"), String(cString: raw) == "1" { return true } - return UserDefaults.standard.bool(forKey: "openclaw.nixMode") + let isAppBundle = Bundle.main.bundleURL.pathExtension == "app" + let stableSuite = UserDefaults(suiteName: launchdLabel) + return Self.resolveNixMode( + environment: self.environment, + standard: .standard, + stableSuite: stableSuite, + isAppBundle: isAppBundle) } var isRunningTests: Bool { diff --git a/apps/macos/Tests/OpenClawIPCTests/NixModeStableSuiteTests.swift b/apps/macos/Tests/OpenClawIPCTests/NixModeStableSuiteTests.swift new file mode 100644 index 000000000..98f7b4c86 --- /dev/null +++ b/apps/macos/Tests/OpenClawIPCTests/NixModeStableSuiteTests.swift @@ -0,0 +1,46 @@ +import Foundation +import Testing +@testable import OpenClaw + +@Suite(.serialized) +struct NixModeStableSuiteTests { + @Test func resolvesFromStableSuiteForAppBundles() { + let suite = UserDefaults(suiteName: launchdLabel)! + let key = "openclaw.nixMode" + let prev = suite.object(forKey: key) + defer { + if let prev { suite.set(prev, forKey: key) } else { suite.removeObject(forKey: key) } + } + + suite.set(true, forKey: key) + + let standard = UserDefaults(suiteName: "NixModeStableSuiteTests.\(UUID().uuidString)")! + #expect(!standard.bool(forKey: key)) + + let resolved = ProcessInfo.resolveNixMode( + environment: [:], + standard: standard, + stableSuite: suite, + isAppBundle: true) + #expect(resolved) + } + + @Test func ignoresStableSuiteOutsideAppBundles() { + let suite = UserDefaults(suiteName: launchdLabel)! + let key = "openclaw.nixMode" + let prev = suite.object(forKey: key) + defer { + if let prev { suite.set(prev, forKey: key) } else { suite.removeObject(forKey: key) } + } + + suite.set(true, forKey: key) + let standard = UserDefaults(suiteName: "NixModeStableSuiteTests.\(UUID().uuidString)")! + + let resolved = ProcessInfo.resolveNixMode( + environment: [:], + standard: standard, + stableSuite: suite, + isAppBundle: false) + #expect(!resolved) + } +} diff --git a/apps/macos/Tests/OpenClawIPCTests/WideAreaGatewayDiscoveryTests.swift b/apps/macos/Tests/OpenClawIPCTests/WideAreaGatewayDiscoveryTests.swift index 4bea51890..24644a2f1 100644 --- a/apps/macos/Tests/OpenClawIPCTests/WideAreaGatewayDiscoveryTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/WideAreaGatewayDiscoveryTests.swift @@ -1,3 +1,4 @@ +import Darwin import Testing @testable import OpenClawDiscovery