feat(privacy-host): add signed native calendar contacts and reminders host

Add the owner-only AF_UNIX Reyna CLI privacy host, strict signed-app installation, and typed native routing for Calendar, Contacts, and Reminders.\n\nAdd bounded system-status paths and config-only direct local-service wrappers. Preserve MacMiniMCP pending explicit cutover approval.\n\nApple Notes is intentionally deferred: no native Notes operations, Apple Events declaration, or Automation helper are included; legacy Notes handling remains untouched.
This commit is contained in:
Adolfo Reyna
2026-08-03 20:27:54 -04:00
parent 6e2117188e
commit 9fd04b0ce4
56 changed files with 14239 additions and 50 deletions
@@ -0,0 +1,292 @@
import Foundation
import Contacts
// MARK: - Contacts data models
struct ContactListItem: Codable, Equatable, Sendable {
let id: String
let name: String
let organization: String
let modifiedAt: String
}
struct ContactEmailLabelValue: Codable, Equatable, Sendable {
let label: String
let value: String
}
struct ContactPhoneLabelValue: Codable, Equatable, Sendable {
let label: String
let value: String
}
struct ContactDetailItem: Codable, Equatable, Sendable {
let id: String
let name: String
let firstName: String
let lastName: String
let organization: String
let jobTitle: String
let emails: [ContactEmailLabelValue]
let phones: [ContactPhoneLabelValue]
let modifiedAt: String
}
struct ContactCreateResult: Codable, Equatable, Sendable {
let id: String
let name: String
let organization: String
}
// MARK: - Contacts provider errors
enum ContactsProviderError: Error, Equatable, Sendable {
case permissionRequired
case permissionDenied
case unavailable(String)
case invalidRequest(String)
case notFound(String)
}
// MARK: - Contact provider protocols
protocol ContactsAuthorizationProviding: Sendable {
func authorizationStatus() -> ContactsAuthorizationStatus
func requestAccess() throws -> Bool
}
enum ContactsAuthorizationStatus: String, Equatable, Sendable {
case authorized
case notDetermined
case denied
case restricted
case unknown
}
protocol ContactsSearchProviding: Sendable {
func searchContacts(query: String?, limit: Int) throws -> [ContactListItem]
}
protocol ContactsReadProviding: Sendable {
func readContact(id: String) throws -> ContactDetailItem
}
protocol ContactsCreateProviding: Sendable {
func createContact(
firstName: String?,
lastName: String?,
organization: String?,
jobTitle: String?,
note: String?,
email: ContactEmailLabelValue?,
phone: ContactPhoneLabelValue?
) throws -> ContactCreateResult
}
protocol FullContactsProviding: ContactsSearchProviding, ContactsReadProviding, ContactsCreateProviding {}
// MARK: - Production Contacts providers
private func displayNameFromFetchedParts(givenName: String, familyName: String) -> String {
let combined = "\(givenName) \(familyName)".trimmingCharacters(in: .whitespacesAndNewlines)
let squashed = combined.components(separatedBy: .whitespaces).filter { !$0.isEmpty }.joined(separator: " ")
return squashed
}
struct ContactsSearchProvider: ContactsSearchProviding {
private func requireAuthorized() throws {
let status = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case .authorized:
return
case .denied, .restricted, .notDetermined:
throw ContactsProviderError.permissionRequired
@unknown default:
throw ContactsProviderError.permissionRequired
}
}
func searchContacts(query: String?, limit: Int) throws -> [ContactListItem] {
try requireAuthorized()
let store = CNContactStore()
let keys: [CNKeyDescriptor] = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor
]
let fetchRequest = CNContactFetchRequest(keysToFetch: keys)
var items: [ContactListItem] = []
let q = query?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let shouldFilter = !(q?.isEmpty ?? true)
do {
try store.enumerateContacts(with: fetchRequest) { contact, stop in
// Production crash fix: the formatter accesses unfetched
// properties like middleName (CNPropertyNotFetchedException is ObjC exception
// and is uncatchable in Swift). Construct deterministically from only fetched keys.
let fullName = displayNameFromFetchedParts(givenName: contact.givenName, familyName: contact.familyName)
let org = contact.organizationName
if shouldFilter, let lowerQ = q {
let haystack = "\(fullName)\n\(org)".lowercased()
if !haystack.contains(lowerQ) {
return
}
}
// Deterministic: do not leak current time; nil modification date yields stable empty string
let modifiedStr: String = ""
items.append(ContactListItem(
id: contact.identifier,
name: fullName,
organization: org,
modifiedAt: modifiedStr
))
if items.count >= limit {
stop.pointee = true
}
}
} catch let err as NSError {
// Permission or other failure
if err.domain == CNErrorDomain {
throw ContactsProviderError.unavailable(err.localizedDescription)
}
throw ContactsProviderError.unavailable(err.localizedDescription)
}
// Deterministic: sorted by name then org then id
items.sort {
if $0.name != $1.name { return $0.name < $1.name }
if $0.organization != $1.organization { return $0.organization < $1.organization }
return $0.id < $1.id
}
if items.count > limit {
return Array(items.prefix(limit))
}
return items
}
}
struct ContactsReadProvider: ContactsReadProviding {
private func requireAuthorized() throws {
let status = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case .authorized:
return
case .denied, .restricted, .notDetermined:
throw ContactsProviderError.permissionRequired
@unknown default:
throw ContactsProviderError.permissionRequired
}
}
func readContact(id: String) throws -> ContactDetailItem {
guard !id.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw ContactsProviderError.invalidRequest("Missing required argument: id")
}
try requireAuthorized()
let store = CNContactStore()
let keys: [CNKeyDescriptor] = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactJobTitleKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
do {
let contact = try store.unifiedContact(withIdentifier: id, keysToFetch: keys)
// Same crash root cause as search: the system formatter can touch unfetched keys.
// Use only the keys we fetched to avoid ObjC CNPropertyNotFetchedException.
let fullName = displayNameFromFetchedParts(givenName: contact.givenName, familyName: contact.familyName)
// Deterministic: nil modification date yields stable empty string, not current time
let modifiedStr = ""
let emails = contact.emailAddresses.map { labeled in
ContactEmailLabelValue(
label: CNLabeledValue<NSString>.localizedString(forLabel: labeled.label ?? ""),
value: labeled.value as String
)
}
let phones = contact.phoneNumbers.map { labeled in
ContactPhoneLabelValue(
label: CNLabeledValue<CNPhoneNumber>.localizedString(forLabel: labeled.label ?? ""),
value: labeled.value.stringValue
)
}
return ContactDetailItem(
id: contact.identifier,
name: fullName,
firstName: contact.givenName,
lastName: contact.familyName,
organization: contact.organizationName,
jobTitle: contact.jobTitle,
emails: emails,
phones: phones,
modifiedAt: modifiedStr
)
} catch let err as CNError {
if err.code == .recordDoesNotExist {
throw ContactsProviderError.notFound("Contact not found: \(id)")
}
throw ContactsProviderError.unavailable(err.localizedDescription)
} catch let err as ContactsProviderError {
throw err
} catch {
throw ContactsProviderError.unavailable(error.localizedDescription)
}
}
}
struct ContactsCreateProvider: ContactsCreateProviding {
private func requireAuthorized() throws {
let status = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case .authorized:
return
case .denied, .restricted, .notDetermined:
throw ContactsProviderError.permissionRequired
@unknown default:
throw ContactsProviderError.permissionRequired
}
}
func createContact(
firstName: String?,
lastName: String?,
organization: String?,
jobTitle: String?,
note: String?,
email: ContactEmailLabelValue?,
phone: ContactPhoneLabelValue?
) throws -> ContactCreateResult {
try requireAuthorized()
let mutable = CNMutableContact()
mutable.givenName = firstName ?? ""
mutable.familyName = lastName ?? ""
mutable.organizationName = organization ?? ""
mutable.jobTitle = jobTitle ?? ""
if let n = note {
mutable.note = n
}
if let em = email {
mutable.emailAddresses = [CNLabeledValue(label: em.label.isEmpty ? CNLabelWork : em.label, value: em.value as NSString)]
}
if let ph = phone {
mutable.phoneNumbers = [CNLabeledValue(label: ph.label.isEmpty ? CNLabelPhoneNumberMobile : ph.label, value: CNPhoneNumber(stringValue: ph.value))]
}
let store = CNContactStore()
let saveRequest = CNSaveRequest()
saveRequest.add(mutable, toContainerWithIdentifier: nil)
do {
try store.execute(saveRequest)
} catch let err as NSError {
throw ContactsProviderError.unavailable(err.localizedDescription)
}
// CNMutableContact doesn't trigger key-fetch checks; safe deterministic construction still.
// Prefer only locally available strings – avoids future formatter regressions.
let fullName = displayNameFromFetchedParts(givenName: mutable.givenName, familyName: mutable.familyName)
return ContactCreateResult(
id: mutable.identifier,
name: fullName,
organization: organization ?? ""
)
}
}