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