Files
openclaw/apps/macos/Sources/OpenClaw/GatewayConnectionSupport.swift
Peter Steinberger 0dbdf994b3 feat(macos): isolate named app profiles (#121136)
* feat(macos): isolate named app profiles

* refactor(macos): isolate profile launch ownership

* fix(macos): avoid overlapping approvals socket access

* fix(macos): declare profile defaults concurrency ownership

* fix(macos): return profiled node launch arguments

* chore(i18n): refresh macOS profile source inventory

* fix(macos): gate profile startup before services

* test(macos): evaluate profile state before assertions

* fix(daemon): skip absent launchd deactivation

* fix(macos): fail closed on profile port conflicts

* chore(i18n): refresh profile conflict inventory

* fix(macos): ignore non-gateway launch agent claims

* test(macos): stabilize profile lifecycle timing

* fix(macos): remove stale dashboard URL

* chore(macos): refresh native source baseline
2026-08-09 14:50:15 -07:00

78 lines
2.9 KiB
Swift

import CryptoKit
import Foundation
import Security
struct GatewayRouteChangedAfterDispatchError: LocalizedError, Sendable {
let method: String
var errorDescription: String? {
"The Gateway route changed after \(self.method) was sent. Its result is unknown; refresh before retrying."
}
}
enum GatewayActivationBindingKeyStore {
// Dev builds carry a different code signature; creating the release item
// would poison its Keychain ACL and make the shipped app demand the login
// keychain password on every read. DEBUG is a config heuristic, not a
// signing check — same accepted tradeoff as MacGatewayProfileStore.service.
#if DEBUG
private static let baseService = "ai.openclaw.onboarding-route-binding.debug"
#else
private static let baseService = "ai.openclaw.onboarding-route-binding"
#endif
static var service: String {
AppProfile.current.keychainService(base: self.baseService)
}
private static let account = "credential-binding-v1"
private static let byteCount = 32
static func loadOrCreate() -> SymmetricKey? {
if let data = load() {
return SymmetricKey(data: data)
}
var data = Data(count: byteCount)
let randomStatus = data.withUnsafeMutableBytes { bytes in
guard let baseAddress = bytes.baseAddress else { return errSecAllocate }
return SecRandomCopyBytes(kSecRandomDefault, self.byteCount, baseAddress)
}
guard randomStatus == errSecSuccess else { return nil }
var query = self.baseQuery
query[kSecValueData as String] = data
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
let addStatus = SecItemAdd(query as CFDictionary, nil)
if addStatus == errSecSuccess {
return SymmetricKey(data: data)
}
// Another process can win the first-launch create race. Only accept the
// secret after reading the Keychain item back through normal ACL checks.
if addStatus == errSecDuplicateItem, let existing = load() {
return SymmetricKey(data: existing)
}
return nil
}
private static func load() -> Data? {
var query = self.baseQuery
query[kSecReturnData as String] = true
query[kSecMatchLimit as String] = kSecMatchLimitOne
var result: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
let data = result as? Data,
data.count == byteCount
else { return nil }
return data
}
private static var baseQuery: [String: Any] {
[
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrSynchronizable as String: false,
]
}
}