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,290 @@
import Foundation
import EventKit
// MARK: - Reminders data models
struct ReminderListItem: Codable, Equatable, Sendable {
let id: String
let title: String
let source: String
let type: String // local/caldav/exchange/etc
}
struct ReminderItem: Codable, Equatable, Sendable {
let id: String
let list_id: String
let list_title: String
let title: String
let notes: String?
let completed: Bool
let due: String? // ISO8601 or nil
let priority: Int // 0-9 (EKReminderPriority mapped)
enum CodingKeys: String, CodingKey {
case id
case list_id
case list_title
case title
case notes
case completed
case due
case priority
}
}
struct ReminderCreateResult: Codable, Equatable, Sendable {
let id: String
let list_id: String
let list_title: String
let title: String
}
enum RemindersProviderError: Error, Equatable, Sendable {
case permissionRequired
case permissionDenied
case unavailable(String)
case invalidRequest(String)
var isPermission: Bool {
if case .permissionRequired = self { return true }
return false
}
}
// MARK: - Provider protocols
protocol RemindersListsProviding: Sendable {
func listReminderLists() throws -> [ReminderListItem]
}
protocol RemindersListProviding: Sendable {
func listReminders(listId: String?, listTitle: String?, completed: Bool?, limit: Int) throws -> [ReminderItem]
}
protocol RemindersCreateProviding: Sendable {
func createReminder(title: String, listId: String?, listTitle: String?, notes: String?, due: Date?, priority: Int?) throws -> ReminderCreateResult
}
protocol FullRemindersProviding: RemindersListsProviding, RemindersListProviding, RemindersCreateProviding {}
// MARK: - Production EventKit provider (scoped read non-prompt; create requires explicit list)
struct EventKitRemindersProvider: RemindersListsProviding, RemindersListProviding, RemindersCreateProviding {
// Shared auth check – only checks status, never prompts
private func requireAuthorizedForRead() throws {
let status = EKEventStore.authorizationStatus(for: .reminder)
switch status {
case .fullAccess, .authorized:
return
case .writeOnly:
// writeOnly does not permit listing reminders per EventKit; treat as permission_required for list operations
throw RemindersProviderError.permissionRequired
case .denied, .restricted, .notDetermined:
throw RemindersProviderError.permissionRequired
@unknown default:
throw RemindersProviderError.permissionRequired
}
}
private func requireAuthorizedForCreate() throws {
let status = EKEventStore.authorizationStatus(for: .reminder)
switch status {
case .fullAccess, .authorized, .writeOnly:
return
case .denied, .restricted, .notDetermined:
throw RemindersProviderError.permissionRequired
@unknown default:
throw RemindersProviderError.permissionRequired
}
}
private func allReminderCalendars(store: EKEventStore) -> [EKCalendar] {
store.calendars(for: .reminder)
}
private func resolveForList(store: EKEventStore, listId: String?, listTitle: String?) throws -> [EKCalendar] {
let calendars = allReminderCalendars(store: store)
if let lid = listId, !lid.isEmpty {
if let found = calendars.first(where: { $0.calendarIdentifier == lid }) {
return [found]
} else {
throw RemindersProviderError.invalidRequest("Reminders list not found: \(lid)")
}
}
if let title = listTitle, !title.isEmpty {
let matched = calendars.filter { $0.title == title }
if matched.isEmpty {
throw RemindersProviderError.invalidRequest("Reminders list not found: \(title)")
}
if matched.count > 1 {
throw RemindersProviderError.invalidRequest("Ambiguous reminders list title: \(title) matches \(matched.count) lists")
}
return matched
}
return calendars
}
private func resolveForCreate(store: EKEventStore, listId: String?, listTitle: String?) throws -> EKCalendar {
let calendars = allReminderCalendars(store: store)
if let lid = listId, !lid.isEmpty {
guard let found = calendars.first(where: { $0.calendarIdentifier == lid }) else {
throw RemindersProviderError.invalidRequest("Reminders list not found: \(lid)")
}
if !found.allowsContentModifications {
throw RemindersProviderError.invalidRequest("Reminders list is read-only: \(found.title)")
}
return found
}
if let title = listTitle, !title.isEmpty {
let matched = calendars.filter { $0.title == title }
if matched.isEmpty {
throw RemindersProviderError.invalidRequest("Reminders list not found: \(title)")
}
if matched.count > 1 {
throw RemindersProviderError.invalidRequest("Ambiguous reminders list title: \(title) matches \(matched.count) lists")
}
let found = matched[0]
if !found.allowsContentModifications {
throw RemindersProviderError.invalidRequest("Reminders list is read-only: \(found.title)")
}
return found
}
throw RemindersProviderError.invalidRequest("Reminders list must be specified by id or exact unique title")
}
// MARK: - lists
func listReminderLists() throws -> [ReminderListItem] {
try requireAuthorizedForRead()
let store = EKEventStore()
let calendars = allReminderCalendars(store: store)
return calendars.map { cal in
let sourceTitle = cal.source.title
let typeString: String
switch cal.type {
case .local: typeString = "local"
case .calDAV: typeString = "caldav"
case .exchange: typeString = "exchange"
case .subscription: typeString = "subscription"
case .birthday: typeString = "birthday"
@unknown default: typeString = "unknown"
}
return ReminderListItem(id: cal.calendarIdentifier, title: cal.title, source: sourceTitle, type: typeString)
}
}
// MARK: - list reminders
func listReminders(listId: String?, listTitle: String?, completed: Bool?, limit: Int) throws -> [ReminderItem] {
try requireAuthorizedForRead()
let store = EKEventStore()
let targetCalendars: [EKCalendar]
do {
targetCalendars = try resolveForList(store: store, listId: listId, listTitle: listTitle)
} catch let e as RemindersProviderError {
throw e
} catch {
throw RemindersProviderError.unavailable("Reminders lookup failed")
}
if targetCalendars.isEmpty { return [] }
let predicate = store.predicateForReminders(in: targetCalendars)
var fetched: [EKReminder] = []
let sem = DispatchSemaphore(value: 0)
var fetchError: Error? = nil
store.fetchReminders(matching: predicate) { rems in
fetched = rems ?? []
sem.signal()
}
// fetchReminders is async on newer APIs? In EventKit even on macOS 13 fetchReminders matching is async via completion.
// Wait bounded 10s
let waitRes = sem.wait(timeout: .now() + 10)
if waitRes == .timedOut {
throw RemindersProviderError.unavailable("Reminders fetch timed out")
}
if let err = fetchError {
throw RemindersProviderError.unavailable(err.localizedDescription)
}
var items: [ReminderItem] = []
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime]
for rem in fetched {
let isCompleted = rem.isCompleted
if let filterCompleted = completed, filterCompleted != isCompleted { continue }
guard let cal = rem.calendar else { continue }
let dueStr: String?
if let comps = rem.dueDateComponents, let d = Calendar.current.date(from: comps) {
dueStr = iso.string(from: d)
} else {
dueStr = nil
}
let item = ReminderItem(
id: rem.calendarItemIdentifier,
list_id: cal.calendarIdentifier,
list_title: cal.title,
title: rem.title ?? "",
notes: rem.notes,
completed: isCompleted,
due: dueStr,
priority: rem.priority
)
items.append(item)
}
// Deterministic sort: due, title, id
items.sort {
let due0 = $0.due ?? ""
let due1 = $1.due ?? ""
if due0 != due1 { return due0 < due1 }
if $0.title != $1.title { return $0.title < $1.title }
return $0.id < $1.id
}
if items.count > limit {
return Array(items.prefix(limit))
}
return items
}
// MARK: - create
func createReminder(title: String, listId: String?, listTitle: String?, notes: String?, due: Date?, priority: Int?) throws -> ReminderCreateResult {
try requireAuthorizedForCreate()
guard !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw RemindersProviderError.invalidRequest("Missing required argument: title")
}
let store = EKEventStore()
let destination: EKCalendar
do {
destination = try resolveForCreate(store: store, listId: listId, listTitle: listTitle)
} catch let e as RemindersProviderError {
throw e
} catch {
throw RemindersProviderError.unavailable("Reminders list lookup failed")
}
let rem = EKReminder(eventStore: store)
rem.title = title
rem.calendar = destination
rem.notes = notes
if let p = priority {
rem.priority = p
}
if let dueDate = due {
let comps = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: dueDate)
rem.dueDateComponents = comps
}
do {
try store.save(rem, commit: true)
} catch {
throw RemindersProviderError.unavailable("Failed to save reminder: \(error.localizedDescription)")
}
return ReminderCreateResult(
id: rem.calendarItemIdentifier,
list_id: destination.calendarIdentifier,
list_title: destination.title,
title: rem.title ?? title
)
}
}