diff --git a/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift b/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift index d4b01a2b19de..0733f0db245c 100644 --- a/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift +++ b/apps/macos/Sources/OpenClaw/OpenClawConfigFile.swift @@ -5,8 +5,8 @@ import OpenClawProtocol enum OpenClawConfigFile { private static let logger = Logger(subsystem: "ai.openclaw", category: "config") private static let configAuditFileName = "config-audit.jsonl" - private static let configHealthFileName = "config-health.json" private static let fileLock = NSRecursiveLock() + private nonisolated(unsafe) static var configHealthState: [String: Any] = [:] private static func withFileLock(_ body: () throws -> T) rethrows -> T { self.fileLock.lock() @@ -477,39 +477,6 @@ enum OpenClawConfigFile { .appendingPathComponent(self.configAuditFileName, isDirectory: false) } - private static func configHealthStateURL() -> URL { - self.stateDirURL() - .appendingPathComponent("logs", isDirectory: true) - .appendingPathComponent(self.configHealthFileName, isDirectory: false) - } - - private static func readConfigHealthState() -> [String: Any] { - let url = self.configHealthStateURL() - guard let data = try? Data(contentsOf: url), - let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] - else { - return [:] - } - return root - } - - private static func writeConfigHealthState(_ root: [String: Any]) { - guard JSONSerialization.isValidJSONObject(root), - let data = try? JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) - else { - return - } - let url = self.configHealthStateURL() - do { - try FileManager().createDirectory( - at: url.deletingLastPathComponent(), - withIntermediateDirectories: true) - try data.write(to: url, options: [.atomic]) - } catch { - // best-effort - } - } - private static func configHealthEntry(state: [String: Any], configPath: String) -> [String: Any] { let entries = state["entries"] as? [String: Any] return entries?[configPath] as? [String: Any] ?? [:] @@ -672,7 +639,7 @@ enum OpenClawConfigFile { private static func observeConfigRead(data: Data, root: [String: Any]?, configURL: URL, valid: Bool) { let observedAt = ISO8601DateFormatter().string(from: Date()) let current = self.configFingerprint(data: data, root: root, configURL: configURL, observedAt: observedAt) - var state = self.readConfigHealthState() + var state = self.configHealthState let entry = self.configHealthEntry(state: state, configPath: configURL.path) let lastKnownGood = entry["lastKnownGood"] as? [String: Any] let suspicious = self.observeSuspiciousReasons( @@ -688,7 +655,7 @@ enum OpenClawConfigFile { ] if !self.sameFingerprint(lastKnownGood, current) || entry["lastObservedSuspiciousSignature"] != nil { state = self.setConfigHealthEntry(state: state, configPath: configURL.path, entry: nextEntry) - self.writeConfigHealthState(state) + self.configHealthState = state } return } @@ -750,7 +717,7 @@ enum OpenClawConfigFile { var nextEntry = entry nextEntry["lastObservedSuspiciousSignature"] = signature state = self.setConfigHealthEntry(state: state, configPath: configURL.path, entry: nextEntry) - self.writeConfigHealthState(state) + self.configHealthState = state } private static func appendConfigWriteAudit(_ fields: [String: Any]) { diff --git a/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift b/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift index 1b384b37954a..600d8ed545a5 100644 --- a/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift @@ -266,6 +266,58 @@ struct OpenClawConfigFileTests { } } + @MainActor + @Test + func `load dict ignores legacy config health sidecar`() async throws { + let stateDir = FileManager().temporaryDirectory + .appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true) + let configPath = stateDir.appendingPathComponent("openclaw.json") + let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl") + let configHealthPath = stateDir.appendingPathComponent("logs/config-health.json") + + defer { try? FileManager().removeItem(at: stateDir) } + + try FileManager().createDirectory( + at: configHealthPath.deletingLastPathComponent(), + withIntermediateDirectories: true) + let legacyHealth = """ + { + "entries": { + "\(configPath.path)": { + "lastKnownGood": { + "bytes": 4096, + "gatewayMode": "local", + "hasMeta": true + } + } + } + } + """ + try legacyHealth.write(to: configHealthPath, atomically: true, encoding: .utf8) + let updateOnlyConfig = """ + { + "update": { + "channel": "beta" + } + } + """ + try updateOnlyConfig.write(to: configPath, atomically: true, encoding: .utf8) + + try await TestIsolation.withEnvValues([ + "OPENCLAW_STATE_DIR": stateDir.path, + "OPENCLAW_CONFIG_PATH": configPath.path, + ]) { + try OpenClawConfigFile.withTestingFileLock { + let loaded = OpenClawConfigFile.loadDict() + let update = loaded["update"] as? [String: Any] + #expect(update?["channel"] as? String == "beta") + #expect(!FileManager().fileExists(atPath: auditPath.path)) + let persistedHealth = try String(contentsOf: configHealthPath, encoding: .utf8) + #expect(persistedHealth == legacyHealth) + } + } + } + @MainActor @Test func `load dict audits suspicious out-of-band clobbers`() async throws { @@ -273,6 +325,7 @@ struct OpenClawConfigFileTests { .appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true) let configPath = stateDir.appendingPathComponent("openclaw.json") let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl") + let configHealthPath = stateDir.appendingPathComponent("logs/config-health.json") defer { try? FileManager().removeItem(at: stateDir) } @@ -293,6 +346,7 @@ struct OpenClawConfigFileTests { ], ]) _ = OpenClawConfigFile.loadDict() + #expect(!FileManager().fileExists(atPath: configHealthPath.path)) let clobbered = """ { @@ -305,6 +359,7 @@ struct OpenClawConfigFileTests { let loaded = OpenClawConfigFile.loadDict() #expect((loaded["gateway"] as? [String: Any]) == nil) + #expect(!FileManager().fileExists(atPath: configHealthPath.path)) let rawAudit = try String(contentsOf: auditPath, encoding: .utf8) let lines = rawAudit