File size: 3,862 Bytes
7b2dfc5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import Foundation

/// Sensitive system data is opt-in per capability. Keeping the future
/// capabilities in the persisted vocabulary lets Settings evolve without
/// changing the policy contract used by tools.
enum AgentSystemCapability: String, CaseIterable, Sendable, Codable, Hashable {
  case calendarRead = "calendar_read"
  case contactsLookup = "contacts_lookup"
  case locationSnapshot = "location_snapshot"
}

enum CalendarQueryWindow: String, CaseIterable, Sendable, Codable, Equatable {
  case today
  case tomorrow
  case nextSevenDays = "next_7_days"

  func dateInterval(containing now: Date, calendar: Calendar) -> DateInterval {
    let startOfToday = calendar.startOfDay(for: now)
    let startOffset: Int
    let endOffset: Int
    switch self {
    case .today:
      startOffset = 0
      endOffset = 1
    case .tomorrow:
      startOffset = 1
      endOffset = 2
    case .nextSevenDays:
      startOffset = 0
      endOffset = 7
    }
    let start = calendar.date(
      byAdding: .day,
      value: startOffset,
      to: startOfToday
    ) ?? startOfToday
    let end = calendar.date(
      byAdding: .day,
      value: endOffset,
      to: startOfToday
    ) ?? start.addingTimeInterval(TimeInterval(endOffset - startOffset) * 86_400)
    return DateInterval(start: start, end: end)
  }

  var displayLabel: String {
    switch self {
    case .today: "today"
    case .tomorrow: "tomorrow"
    case .nextSevenDays: "for the next 7 days"
    }
  }
}

/// Deliberately contains only the fields the assistant may disclose. EventKit
/// identifiers, notes, attendees, URLs, locations, and recurrence metadata
/// never cross this boundary.
struct SystemCalendarEvent: Sendable, Equatable {
  let title: String
  let startsAt: Date
  let endsAt: Date
  let isAllDay: Bool
}

struct SystemCalendarEventPage: Sendable, Equatable {
  let events: [SystemCalendarEvent]
  let hasMore: Bool
}

enum SystemCalendarAuthorization: String, Sendable, Equatable {
  case fullAccess
  case notDetermined
  case denied
  case restricted
  case writeOnly
}

enum SystemCalendarReaderError: LocalizedError, Sendable, Equatable {
  case fullAccessRequired(SystemCalendarAuthorization)
  case unavailable(String)

  var errorDescription: String? {
    switch self {
    case .fullAccessRequired(let status):
      switch status {
      case .notDetermined:
        "Calendar full access has not been granted. Grant access from Dolphin Settings before trying again."
      case .denied:
        "Calendar full access is denied. Enable it in iOS Settings before trying again."
      case .restricted:
        "Calendar full access is restricted on this iPhone."
      case .writeOnly:
        "Calendar access is write-only. Full access is required to read events."
      case .fullAccess:
        "Calendar events are temporarily unavailable."
      }
    case .unavailable(let reason):
      "Calendar events are unavailable: \(reason)"
    }
  }
}

protocol SystemCalendarReading: Sendable {
  func authorizationStatus() async -> SystemCalendarAuthorization

  /// Implementations return at most `limit` events and indicate whether more
  /// matched. This method must never request permission.
  func readEvents(
    in interval: DateInterval,
    limit: Int
  ) async throws -> SystemCalendarEventPage
}

/// Pure fallback used by host tests and non-iOS consumers. The production app
/// injects EventKit explicitly, keeping platform APIs out of the agent core.
struct UnavailableSystemCalendarReader: SystemCalendarReading {
  func authorizationStatus() async -> SystemCalendarAuthorization {
    .denied
  }

  func readEvents(
    in interval: DateInterval,
    limit: Int
  ) async throws -> SystemCalendarEventPage {
    throw SystemCalendarReaderError.unavailable(
      "No system calendar adapter is available in this environment."
    )
  }
}