import EventKit import Foundation /// The production adapter is intentionally read-only and non-interactive. /// Permission prompts belong to an explicit Settings flow, never tool /// execution, so an agent run cannot surprise the user with a system prompt. actor EventKitCalendarReader: SystemCalendarReading { private let eventStore: EKEventStore init(eventStore: EKEventStore = EKEventStore()) { self.eventStore = eventStore } func authorizationStatus() -> SystemCalendarAuthorization { Self.authorizationStatus( from: EKEventStore.authorizationStatus(for: .event) ) } /// Called only from the explicit Settings button. Tool execution uses /// `readEvents` and therefore can never trigger an iOS permission prompt. func requestFullAccess() async throws -> SystemCalendarAuthorization { _ = try await eventStore.requestFullAccessToEvents() return authorizationStatus() } func readEvents( in interval: DateInterval, limit: Int ) throws -> SystemCalendarEventPage { let authorization = authorizationStatus() guard authorization == .fullAccess else { throw SystemCalendarReaderError.fullAccessRequired(authorization) } let safeLimit = min(max(limit, 1), 11) let predicate = eventStore.predicateForEvents( withStart: interval.start, end: interval.end, calendars: nil ) let matchingEvents = eventStore.events(matching: predicate) .filter { event in event.startDate < interval.end && event.endDate > interval.start } .sorted { first, second in if first.startDate != second.startDate { return first.startDate < second.startDate } if first.endDate != second.endDate { return first.endDate < second.endDate } return (first.title ?? "").localizedStandardCompare( second.title ?? "" ) == .orderedAscending } let visibleEvents = matchingEvents.prefix(safeLimit).map { event in SystemCalendarEvent( title: event.title ?? "Untitled event", startsAt: event.startDate, endsAt: max(event.startDate, event.endDate), isAllDay: event.isAllDay ) } return SystemCalendarEventPage( events: Array(visibleEvents), hasMore: matchingEvents.count > safeLimit ) } private static func authorizationStatus( from status: EKAuthorizationStatus ) -> SystemCalendarAuthorization { switch status { case .fullAccess: .fullAccess case .notDetermined: .notDetermined case .denied: .denied case .restricted: .restricted case .writeOnly: .writeOnly @unknown default: .restricted } } }