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,288 @@
import Foundation
import EventKit
// Production EventKit provider – read-only for list, mutating only for create.
// Requirements:
// - Only checks authorization status (never triggers prompt) for list/create.
// - Lists calendars when status permits.
// - Never calls prompting APIs except in auth provider.
// - If permission absent/denied/restricted, throw permissionRequired.
struct EventKitCalendarProvider: CalendarListProviding, CalendarEventsListProviding, CalendarEventCreateProviding {
// MARK: - Auth check shared
private func requireAuthorizedOrThrow() throws {
let status = EKEventStore.authorizationStatus(for: .event)
switch status {
case .fullAccess, .authorized:
break
case .writeOnly:
// writeOnly does not allow reading calendars/events; but for create we could allow? Task says already-authorized access but no permission request; map denied/not-determined to permission_required for all.
// Simpler: for events list, writeOnly -> permissionRequired; for create, writeOnly should also require check but writeOnly actually allows writing. However to keep deterministic, attempt to respect writeOnly for create?
// Spec: map denied/not-determined to permission_required, provider failures to calendar_unavailable.
// To be safe: for list -> permissionRequired, for create we will check below differently? But shared throw would block create with writeOnly unnecessarily.
// We differentiate inside methods. For this helper, allow writeOnly as authorized for mutating path? Caller should call specific check.
throw CalendarProviderError.permissionRequired
case .denied, .restricted, .notDetermined:
throw CalendarProviderError.permissionRequired
@unknown default:
throw CalendarProviderError.permissionRequired
}
}
private func requireAuthorizedForReadOrWrite(allowsWriteOnlyRead: Bool = false) throws {
let status = EKEventStore.authorizationStatus(for: .event)
switch status {
case .fullAccess, .authorized:
return
case .writeOnly:
if allowsWriteOnlyRead {
return
}
// For read paths (list calendars, list events), writeOnly does NOT permit read -> permission_required
throw CalendarProviderError.permissionRequired
case .denied, .restricted, .notDetermined:
throw CalendarProviderError.permissionRequired
@unknown default:
throw CalendarProviderError.permissionRequired
}
}
// MARK: - Calendar list (existing)
func listCalendars() throws -> [CalendarListItem] {
try requireAuthorizedForReadOrWrite(allowsWriteOnlyRead: false)
let store = EKEventStore()
let ekCalendars = store.calendars(for: .event)
let items: [CalendarListItem] = ekCalendars.map { cal in
let sourceTitle = cal.source.title
let typeString: String
switch cal.type {
case .birthday:
typeString = "birthday"
case .calDAV:
typeString = "caldav"
case .exchange:
typeString = "exchange"
case .local:
typeString = "local"
case .subscription:
typeString = "subscription"
@unknown default:
typeString = "unknown"
}
return CalendarListItem(
id: cal.calendarIdentifier,
title: cal.title,
source: sourceTitle,
type: typeString
)
}
return items
}
// MARK: - Calendar helpers for events
private func allCalendars(from store: EKEventStore) -> [EKCalendar] {
store.calendars(for: .event)
}
private func resolveCalendarForList(store: EKEventStore, calendarId: String?, calendarTitle: String?) throws -> [EKCalendar] {
// Return array of matching calendars (filtered)
// Rules:
// - if calendarId provided (stable ID), exact match only one; if unknown -> invalidRequest
// - else if calendarTitle provided, exact title match; must be unique else invalidRequest (ambiguous) or unknown => invalidRequest
// - else all calendars
let calendars = allCalendars(from: store)
if let cid = calendarId, !cid.isEmpty {
// ID wins
if let found = calendars.first(where: { $0.calendarIdentifier == cid }) {
return [found]
} else {
throw CalendarProviderError.invalidRequest("Calendar not found: \(cid)")
}
}
if let title = calendarTitle, !title.isEmpty {
let matched = calendars.filter { $0.title == title }
if matched.isEmpty {
throw CalendarProviderError.invalidRequest("Calendar not found: \(title)")
}
if matched.count > 1 {
throw CalendarProviderError.invalidRequest("Ambiguous calendar title: \(title) matches \(matched.count) calendars")
}
return matched
}
// no filter -> all
return calendars
}
private func resolveCalendarForCreate(store: EKEventStore, calendarId: String?, calendarTitle: String?) throws -> EKCalendar {
// Create must not default to arbitrary.
// If neither id nor title -> invalidRequest
let calendars = allCalendars(from: store)
if let cid = calendarId, !cid.isEmpty {
guard let found = calendars.first(where: { $0.calendarIdentifier == cid }) else {
throw CalendarProviderError.invalidRequest("Calendar not found: \(cid)")
}
if !found.allowsContentModifications {
throw CalendarProviderError.invalidRequest("Calendar is read-only: \(found.title)")
}
// also check writable via allowsContentModifications; isImmutable also relevant but we use allowsContentModifications
return found
}
if let title = calendarTitle, !title.isEmpty {
let matched = calendars.filter { $0.title == title }
if matched.isEmpty {
throw CalendarProviderError.invalidRequest("Calendar not found: \(title)")
}
if matched.count > 1 {
throw CalendarProviderError.invalidRequest("Ambiguous calendar title: \(title) matches \(matched.count) calendars")
}
let found = matched[0]
if !found.allowsContentModifications {
throw CalendarProviderError.invalidRequest("Calendar is read-only: \(found.title)")
}
return found
}
// No calendar specified – per spec "no default first arbitrary calendar"
throw CalendarProviderError.invalidRequest("Calendar must be specified by id or exact unique title")
}
// MARK: - Events list
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] {
// Permission: read requires full access
try requireAuthorizedForReadOrWrite(allowsWriteOnlyRead: false)
let store = EKEventStore()
let targetCalendars: [EKCalendar]
do {
targetCalendars = try resolveCalendarForList(store: store, calendarId: calendarId, calendarTitle: calendarTitle)
} catch let err as CalendarProviderError {
throw err
} catch {
throw CalendarProviderError.unavailable("Calendar lookup failed")
}
if targetCalendars.isEmpty {
return []
}
// EK predicate
let predicate = store.predicateForEvents(withStart: start, end: end, calendars: targetCalendars)
let ekEvents: [EKEvent] = store.events(matching: predicate)
// Map and sort deterministically, enforce overlap check (predicate already does but ensure)
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime]
var items: [CalendarEventItem] = []
items.reserveCapacity(min(ekEvents.count, limit))
for ev in ekEvents {
guard let evStart = ev.startDate, let evEnd = ev.endDate else { continue }
// Enforce overlap (EventKit predicate should already overlap but safe)
if evEnd < start || evStart > end { continue }
guard let cal = ev.calendar else { continue }
let item = CalendarEventItem(
id: ev.eventIdentifier ?? ev.calendarItemIdentifier,
title: ev.title ?? "",
start: iso.string(from: evStart),
end: iso.string(from: evEnd),
all_day: ev.isAllDay,
calendar_id: cal.calendarIdentifier,
calendar_title: cal.title,
notes: ev.notes,
location: ev.location
)
items.append(item)
}
// Deterministic sort by start, then title, then id, then truncate to limit
items.sort {
if $0.start != $1.start { return $0.start < $1.start }
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: - Event create
func createEvent(
title: String,
start: Date,
end: Date,
allDay: Bool,
notes: String?,
location: String?,
calendarId: String?,
calendarTitle: String?
) throws -> CalendarEventItem {
// For create, we allow fullAccess, authorized, and writeOnly (since writeOnly permits creation)
let status = EKEventStore.authorizationStatus(for: .event)
switch status {
case .fullAccess, .authorized, .writeOnly:
break
case .denied, .restricted, .notDetermined:
throw CalendarProviderError.permissionRequired
@unknown default:
throw CalendarProviderError.permissionRequired
}
let store = EKEventStore()
let destination: EKCalendar
do {
destination = try resolveCalendarForCreate(store: store, calendarId: calendarId, calendarTitle: calendarTitle)
} catch let err as CalendarProviderError {
throw err
} catch {
throw CalendarProviderError.unavailable("Calendar lookup failed")
}
// Validate start < end already done in dispatch, but double check
if start >= end {
throw CalendarProviderError.invalidRequest("start must occur before end")
}
let ekEvent = EKEvent(eventStore: store)
ekEvent.title = title
ekEvent.startDate = start
ekEvent.endDate = end
ekEvent.isAllDay = allDay
ekEvent.notes = notes
ekEvent.location = location
ekEvent.calendar = destination
do {
try store.save(ekEvent, span: .thisEvent, commit: true)
} catch {
throw CalendarProviderError.unavailable("Failed to save event: \(error.localizedDescription)")
}
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime]
return CalendarEventItem(
id: ekEvent.eventIdentifier ?? ekEvent.calendarItemIdentifier,
title: ekEvent.title ?? title,
start: iso.string(from: ekEvent.startDate ?? start),
end: iso.string(from: ekEvent.endDate ?? end),
all_day: ekEvent.isAllDay,
calendar_id: destination.calendarIdentifier,
calendar_title: destination.title,
notes: ekEvent.notes,
location: ekEvent.location
)
}
}