Files
Adolfo Reyna 9fd04b0ce4 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.
2026-08-03 20:27:54 -04:00

1084 lines
50 KiB
Swift

import Foundation
// MARK: - JSONValue safe representation
enum JSONValue: Codable, Equatable, Sendable {
case object([String: JSONValue])
case array([JSONValue])
case string(String)
case bool(Bool)
case number(Double)
case null
init() {
self = .object([:])
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
return
}
if let b = try? container.decode(Bool.self) {
self = .bool(b)
return
}
if let obj = try? container.decode([String: JSONValue].self) {
self = .object(obj)
return
}
if let arr = try? container.decode([JSONValue].self) {
self = .array(arr)
return
}
if let s = try? container.decode(String.self) {
self = .string(s)
return
}
if let intVal = try? container.decode(Int.self) {
self = .number(Double(intVal))
return
}
if let doubleVal = try? container.decode(Double.self) {
self = .number(doubleVal)
return
}
throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unrecognized JSON value"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .null:
try container.encodeNil()
case .bool(let b):
try container.encode(b)
case .string(let s):
try container.encode(s)
case .number(let d):
if d.truncatingRemainder(dividingBy: 1) == 0, d >= Double(Int.min), d <= Double(Int.max) {
try container.encode(Int(d))
} else {
try container.encode(d)
}
case .object(let o):
try container.encode(o)
case .array(let a):
try container.encode(a)
}
}
}
typealias Args = JSONValue
struct CalendarListItem: Codable, Equatable, Sendable {
let id: String
let title: String
let source: String
let type: String
}
struct CalendarEventItem: Codable, Equatable, Sendable {
let id: String
let title: String
let start: String
let end: String
let all_day: Bool
let calendar_id: String
let calendar_title: String
let notes: String?
let location: String?
enum CodingKeys: String, CodingKey {
case id
case title
case start
case end
case all_day
case calendar_id
case calendar_title
case notes
case location
}
}
enum CalendarProviderError: Error, Equatable, Sendable {
case permissionRequired
case unavailable(String)
case invalidRequest(String)
var isPermission: Bool {
if case .permissionRequired = self { return true }
return false
}
}
protocol CalendarListProviding: Sendable {
func listCalendars() throws -> [CalendarListItem]
}
protocol CalendarEventsListProviding: Sendable {
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem]
}
protocol CalendarEventCreateProviding: Sendable {
func createEvent(
title: String,
start: Date,
end: Date,
allDay: Bool,
notes: String?,
location: String?,
calendarId: String?,
calendarTitle: String?
) throws -> CalendarEventItem
}
protocol FullCalendarProviding: CalendarListProviding, CalendarEventsListProviding, CalendarEventCreateProviding {}
struct DefaultCalendarListProvider: CalendarListProviding {
func listCalendars() throws -> [CalendarListItem] {
throw CalendarProviderError.unavailable("no provider configured")
}
}
struct Request: Codable, Sendable {
let id: String
let operation: String
let arguments: JSONValue
}
struct ResultPayload: Codable, Equatable, Sendable {
let protocol_version: String
let operation: String
let status: String?
let calendars: [CalendarListItem]?
let events: [CalendarEventItem]?
let event: CalendarEventItem?
let contacts: [ContactListItem]?
let contact: ContactDetailItem?
let created_contact: ContactCreateResult?
let reminder_lists: [ReminderListItem]?
let reminders: [ReminderItem]?
let reminder: ReminderItem?
let created_reminder: ReminderCreateResult?
let system_info: SystemInfoItem?
let speech_api_status: SpeechApiStatusItem?
init(
protocol_version: String,
operation: String,
status: String? = nil,
calendars: [CalendarListItem]? = nil,
events: [CalendarEventItem]? = nil,
event: CalendarEventItem? = nil,
contacts: [ContactListItem]? = nil,
contact: ContactDetailItem? = nil,
created_contact: ContactCreateResult? = nil,
reminder_lists: [ReminderListItem]? = nil,
reminders: [ReminderItem]? = nil,
reminder: ReminderItem? = nil,
created_reminder: ReminderCreateResult? = nil,
system_info: SystemInfoItem? = nil,
speech_api_status: SpeechApiStatusItem? = nil
) {
self.protocol_version = protocol_version
self.operation = operation
self.status = status
self.calendars = calendars
self.events = events
self.event = event
self.contacts = contacts
self.contact = contact
self.created_contact = created_contact
self.reminder_lists = reminder_lists
self.reminders = reminders
self.reminder = reminder
self.created_reminder = created_reminder
self.system_info = system_info
self.speech_api_status = speech_api_status
}
}
struct ErrorPayload: Codable, Equatable, Sendable {
let code: String
let message: String
}
struct Response: Codable, Equatable, Sendable {
let id: String
let ok: Bool
let result: ResultPayload?
let error: ErrorPayload?
}
let PROTOCOL_VERSION = "1.0.0"
// MARK: - Shared ISO8601 parsing & validation helpers
enum CalendarArgsValidationError: Error, Equatable, Sendable {
case invalid(String)
}
nonisolated(unsafe) private let iso8601WithFractional: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
nonisolated(unsafe) private let iso8601Plain: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime]
return f
}()
func parseISO8601DateStrict(_ s: String) throws -> Date {
if let d = iso8601WithFractional.date(from: s) { return d }
if let d = iso8601Plain.date(from: s) { return d }
throw CalendarProviderError.invalidRequest("Invalid ISO-8601 date: \(s)")
}
func extractStringArg(_ args: JSONValue, key: String) -> String? {
guard case .object(let obj) = args, let val = obj[key] else { return nil }
switch val {
case .string(let s): return s
case .null: return nil
default: return nil
}
}
func extractBoolArg(_ args: JSONValue, key: String, defaultValue: Bool) -> Bool {
guard case .object(let obj) = args, let val = obj[key] else { return defaultValue }
switch val {
case .bool(let b): return b
case .object(_), .array(_), .string(_), .number(_), .null: return defaultValue
}
}
func extractIntArg(_ args: JSONValue, key: String) -> Int? {
guard case .object(let obj) = args, let val = obj[key] else { return nil }
switch val {
case .number(let n): return Int(n)
case .string(let s): return Int(s)
default: return nil
}
}
func iso8601StringFromDate(_ d: Date) -> String {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime]
return f.string(from: d)
}
func dispatch(
request: Request,
calendarProvider: CalendarListProviding = ProductionCalendarProvider(),
authProvider: CalendarAuthorizationProviding = ProductionAuthProvider()
) -> Response {
return dispatch(
request: request,
calendarProvider: calendarProvider,
eventsProvider: ProductionCalendarEventsProvider(),
createProvider: ProductionCalendarEventCreateProvider(),
authProvider: authProvider,
contactsAuthProvider: ProductionContactsAuthProvider(),
contactsSearchProvider: ProductionContactsSearchProvider(),
contactsReadProvider: ProductionContactsReadProvider(),
contactsCreateProvider: ProductionContactsCreateProvider(),
remindersAuthProvider: ProductionRemindersAuthProvider(),
remindersListsProvider: ProductionRemindersListsProvider(),
remindersListProvider: ProductionRemindersListProvider(),
remindersCreateProvider: ProductionRemindersCreateProvider()
)
}
func dispatch(
request: Request,
calendarProvider: CalendarListProviding,
eventsProvider: CalendarEventsListProviding,
createProvider: CalendarEventCreateProviding,
authProvider: CalendarAuthorizationProviding,
contactsAuthProvider: ContactsAuthorizationProviding = ProductionContactsAuthProvider(),
contactsSearchProvider: ContactsSearchProviding = ProductionContactsSearchProvider(),
contactsReadProvider: ContactsReadProviding = ProductionContactsReadProvider(),
contactsCreateProvider: ContactsCreateProviding = ProductionContactsCreateProvider(),
remindersAuthProvider: RemindersAuthorizationProviding = ProductionRemindersAuthProvider(),
remindersListsProvider: RemindersListsProviding = ProductionRemindersListsProvider(),
remindersListProvider: RemindersListProviding = ProductionRemindersListProvider(),
remindersCreateProvider: RemindersCreateProviding = ProductionRemindersCreateProvider()
) -> Response {
switch request.operation {
case "service.health":
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "service.health",
status: "ok",
calendars: nil
)
return Response(id: request.id, ok: true, result: result, error: nil)
case "calendar.list":
do {
let items = try calendarProvider.listCalendars()
let sorted = items.sorted {
if $0.source != $1.source { return $0.source < $1.source }
if $0.title != $1.title { return $0.title < $1.title }
return $0.id < $1.id
}
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "calendar.list",
status: "ok",
calendars: sorted
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as CalendarProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "calendar.request_full_access":
do {
let status = authProvider.authorizationStatus()
if status == .authorized {
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "calendar.request_full_access",
status: "authorized",
calendars: nil
)
return Response(id: request.id, ok: true, result: result, error: nil)
}
let granted: Bool
do {
granted = try authProvider.requestFullAccess()
} catch let err as CalendarProviderError {
switch err {
case .unavailable(let msg):
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionRequired:
let e = ErrorPayload(code: "permission_denied", message: "Calendar access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
if granted {
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "calendar.request_full_access",
status: "authorized",
calendars: nil
)
return Response(id: request.id, ok: true, result: result, error: nil)
} else {
let e = ErrorPayload(code: "permission_denied", message: "Calendar access denied. Grant permission in System Settings > Privacy & Security > Calendars.")
return Response(id: request.id, ok: false, result: nil, error: e)
}
}
case "calendar.events.list":
do {
guard let startStr = extractStringArg(request.arguments, key: "start"), !startStr.isEmpty else {
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: start")
return Response(id: request.id, ok: false, result: nil, error: e)
}
guard let endStr = extractStringArg(request.arguments, key: "end"), !endStr.isEmpty else {
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: end")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let startDate: Date
let endDate: Date
do {
startDate = try parseISO8601DateStrict(startStr)
} catch let err as CalendarProviderError {
if case .invalidRequest(let msg) = err {
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg))
}
throw err
}
do {
endDate = try parseISO8601DateStrict(endStr)
} catch let err as CalendarProviderError {
if case .invalidRequest(let msg) = err {
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg))
}
throw err
}
if startDate >= endDate {
let e = ErrorPayload(code: "invalid_request", message: "start must occur before end")
return Response(id: request.id, ok: false, result: nil, error: e)
}
var limit = 50
if let extracted = extractIntArg(request.arguments, key: "limit") {
limit = extracted
}
if limit < 1 || limit > 250 {
let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 250")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let calId = extractStringArg(request.arguments, key: "calendar_id")
?? extractStringArg(request.arguments, key: "calendarId")
let calTitle = extractStringArg(request.arguments, key: "calendar")
?? extractStringArg(request.arguments, key: "calendar_title")
?? extractStringArg(request.arguments, key: "calendarTitle")
let items = try eventsProvider.listEvents(start: startDate, end: endDate, calendarId: calId, calendarTitle: calTitle, limit: limit)
let sorted = items.sorted {
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
}
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "calendar.events.list",
status: "ok",
events: sorted
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as CalendarProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "calendar.event.create":
do {
guard let title = extractStringArg(request.arguments, key: "title"), !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: title")
return Response(id: request.id, ok: false, result: nil, error: e)
}
guard let startStr = extractStringArg(request.arguments, key: "start"), !startStr.isEmpty else {
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: start")
return Response(id: request.id, ok: false, result: nil, error: e)
}
guard let endStr = extractStringArg(request.arguments, key: "end"), !endStr.isEmpty else {
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: end")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let startDate: Date
let endDate: Date
do { startDate = try parseISO8601DateStrict(startStr) } catch let err as CalendarProviderError {
if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) }
throw err
}
do { endDate = try parseISO8601DateStrict(endStr) } catch let err as CalendarProviderError {
if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) }
throw err
}
if startDate >= endDate {
let e = ErrorPayload(code: "invalid_request", message: "start must occur before end")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let allDay = extractBoolArg(request.arguments, key: "all_day", defaultValue: extractBoolArg(request.arguments, key: "allDay", defaultValue: false))
let notes = extractStringArg(request.arguments, key: "notes")
let location = extractStringArg(request.arguments, key: "location")
let calId = extractStringArg(request.arguments, key: "calendar_id") ?? extractStringArg(request.arguments, key: "calendarId")
let calTitle = extractStringArg(request.arguments, key: "calendar") ?? extractStringArg(request.arguments, key: "calendar_title") ?? extractStringArg(request.arguments, key: "calendarTitle")
let created = try createProvider.createEvent(
title: title,
start: startDate,
end: endDate,
allDay: allDay,
notes: notes,
location: location,
calendarId: calId,
calendarTitle: calTitle
)
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "calendar.event.create",
status: "ok",
event: created
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as CalendarProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "contacts.request_access":
do {
let status = contactsAuthProvider.authorizationStatus()
if status == .authorized {
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "contacts.request_access",
status: "authorized"
)
return Response(id: request.id, ok: true, result: result, error: nil)
}
let granted: Bool
do {
granted = try contactsAuthProvider.requestAccess()
} catch let err as ContactsProviderError {
switch err {
case .unavailable(let msg):
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionRequired, .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .notFound(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
if granted {
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "contacts.request_access",
status: "authorized"
)
return Response(id: request.id, ok: true, result: result, error: nil)
} else {
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied. Grant permission in System Settings > Privacy & Security > Contacts.")
return Response(id: request.id, ok: false, result: nil, error: e)
}
}
case "contacts.search":
do {
var limit = 50
if let extracted = extractIntArg(request.arguments, key: "limit") {
limit = extracted
}
if limit < 1 || limit > 100 {
let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 100")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let queryObj: String?
if case .object(let obj) = request.arguments {
if let q = obj["query"] {
switch q {
case .string(let s): queryObj = s
case .null: queryObj = nil
default: queryObj = nil
}
} else {
queryObj = nil
}
} else {
queryObj = nil
}
// Also accept empty string? legacy allowed query nil
let items = try contactsSearchProvider.searchContacts(query: queryObj, limit: limit)
let sorted = items.sorted {
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
}
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "contacts.search",
status: "ok",
contacts: sorted
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as ContactsProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .notFound(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "contacts.read":
do {
guard let idVal = extractStringArg(request.arguments, key: "id"), !idVal.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: id")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let item = try contactsReadProvider.readContact(id: idVal)
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "contacts.read",
status: "ok",
contact: item
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as ContactsProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .notFound(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "contacts.create":
do {
// Legacy keys: firstName/lastName/organization/jobTitle/note + email/phone objects
var firstName: String? = extractStringArg(request.arguments, key: "firstName") ?? extractStringArg(request.arguments, key: "first_name")
var lastName: String? = extractStringArg(request.arguments, key: "lastName") ?? extractStringArg(request.arguments, key: "last_name")
var organization: String? = extractStringArg(request.arguments, key: "organization")
var jobTitle: String? = extractStringArg(request.arguments, key: "jobTitle") ?? extractStringArg(request.arguments, key: "job_title")
var note: String? = extractStringArg(request.arguments, key: "note")
// At least one of firstName/lastName/organization required to create meaningful contact
let hasName = !(firstName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) || !(lastName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) || !(organization?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
if !hasName {
// allow empty still? per legacy it allowed empty, but we require at least something
// However preserve compat: if nothing, invalid_request
let e = ErrorPayload(code: "invalid_request", message: "Missing required field: at least firstName, lastName, or organization required")
return Response(id: request.id, ok: false, result: nil, error: e)
}
var emailLV: ContactEmailLabelValue? = nil
var phoneLV: ContactPhoneLabelValue? = nil
if case .object(let outer) = request.arguments {
if let ev = outer["email"] {
if case .object(let eObj) = ev {
let label: String
let value: String
switch eObj["label"] {
case .string(let s): label = s
default: label = "work"
}
switch eObj["value"] {
case .string(let s): value = s
default: value = ""
}
if !value.isEmpty {
emailLV = ContactEmailLabelValue(label: label, value: value)
}
}
}
if let pv = outer["phone"] {
if case .object(let pObj) = pv {
let label: String
let value: String
switch pObj["label"] {
case .string(let s): label = s
default: label = "mobile"
}
switch pObj["value"] {
case .string(let s): value = s
default: value = ""
}
if !value.isEmpty {
phoneLV = ContactPhoneLabelValue(label: label, value: value)
}
}
}
}
let created = try contactsCreateProvider.createContact(
firstName: firstName,
lastName: lastName,
organization: organization,
jobTitle: jobTitle,
note: note,
email: emailLV,
phone: phoneLV
)
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "contacts.create",
status: "ok",
created_contact: created
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as ContactsProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .notFound(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "reminders.request_full_access":
do {
let status = remindersAuthProvider.authorizationStatus()
if status == .authorized {
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "reminders.request_full_access",
status: "authorized"
)
return Response(id: request.id, ok: true, result: result, error: nil)
}
let granted: Bool
do {
granted = try remindersAuthProvider.requestFullAccess()
} catch let err as RemindersProviderError {
switch err {
case .unavailable(let msg):
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionRequired, .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
if granted {
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "reminders.request_full_access",
status: "authorized"
)
return Response(id: request.id, ok: true, result: result, error: nil)
} else {
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied. Grant permission in System Settings > Privacy & Security > Reminders.")
return Response(id: request.id, ok: false, result: nil, error: e)
}
}
case "reminders.lists":
do {
let items = try remindersListsProvider.listReminderLists()
let sorted = items.sorted {
if $0.source != $1.source { return $0.source < $1.source }
if $0.title != $1.title { return $0.title < $1.title }
return $0.id < $1.id
}
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "reminders.lists",
status: "ok",
reminder_lists: sorted
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as RemindersProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "reminders.list":
do {
var limit = 50
if let extracted = extractIntArg(request.arguments, key: "limit") {
limit = extracted
}
if limit < 1 || limit > 250 {
let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 250")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let listId = extractStringArg(request.arguments, key: "list_id") ?? extractStringArg(request.arguments, key: "listId") ?? extractStringArg(request.arguments, key: "id")
let listTitle = extractStringArg(request.arguments, key: "list") ?? extractStringArg(request.arguments, key: "list_title") ?? extractStringArg(request.arguments, key: "listTitle")
var completedFilter: Bool? = nil
if case .object(let obj) = request.arguments {
if let compVal = obj["completed"] {
switch compVal {
case .bool(let b): completedFilter = b
case .null: completedFilter = nil
default: break
}
}
}
let items = try remindersListProvider.listReminders(listId: listId, listTitle: listTitle, completed: completedFilter, limit: limit)
let sorted = items.sorted {
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
}
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "reminders.list",
status: "ok",
reminders: sorted
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as RemindersProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "reminders.create":
do {
guard let title = extractStringArg(request.arguments, key: "title"), !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: title")
return Response(id: request.id, ok: false, result: nil, error: e)
}
if title.count > 1024 {
let e = ErrorPayload(code: "invalid_request", message: "title exceeds maximum length 1024")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let notes = extractStringArg(request.arguments, key: "notes") ?? extractStringArg(request.arguments, key: "body")
if let n = notes, n.count > 4096 {
let e = ErrorPayload(code: "invalid_request", message: "notes exceeds maximum length 4096")
return Response(id: request.id, ok: false, result: nil, error: e)
}
var dueDate: Date? = nil
if let dueStr = extractStringArg(request.arguments, key: "due") ?? extractStringArg(request.arguments, key: "dueDate") ?? extractStringArg(request.arguments, key: "due_date") {
if !dueStr.isEmpty {
do {
dueDate = try parseISO8601DateStrict(dueStr)
} catch let err as CalendarProviderError {
if case .invalidRequest(let msg) = err {
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg))
}
throw err
}
}
}
var priority: Int? = nil
if let p = extractIntArg(request.arguments, key: "priority") {
if p < 0 || p > 9 {
let e = ErrorPayload(code: "invalid_request", message: "priority must be between 0 and 9")
return Response(id: request.id, ok: false, result: nil, error: e)
}
priority = p
}
let listId = extractStringArg(request.arguments, key: "list_id") ?? extractStringArg(request.arguments, key: "listId")
let listTitle = extractStringArg(request.arguments, key: "list") ?? extractStringArg(request.arguments, key: "list_title") ?? extractStringArg(request.arguments, key: "listTitle")
if (listId == nil || listId?.isEmpty == true) && (listTitle == nil || listTitle?.isEmpty == true) {
let e = ErrorPayload(code: "invalid_request", message: "Reminders list must be specified by id or exact unique title")
return Response(id: request.id, ok: false, result: nil, error: e)
}
let created = try remindersCreateProvider.createReminder(title: title, listId: listId, listTitle: listTitle, notes: notes, due: dueDate, priority: priority)
let result = ResultPayload(
protocol_version: PROTOCOL_VERSION,
operation: "reminders.create",
status: "ok",
created_reminder: created
)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch let err as RemindersProviderError {
switch err {
case .permissionRequired:
let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .permissionDenied:
let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.")
return Response(id: request.id, ok: false, result: nil, error: e)
case .unavailable(let msg):
let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg)
return Response(id: request.id, ok: false, result: nil, error: e)
case .invalidRequest(let msg):
let e = ErrorPayload(code: "invalid_request", message: msg)
return Response(id: request.id, ok: false, result: nil, error: e)
}
} catch {
let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable")
return Response(id: request.id, ok: false, result: nil, error: e)
}
case "system.get_info":
do {
let info = try ProductionSystemInfoProvider().getSystemInfo()
let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "system.get_info", status: "ok", system_info: info)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch {
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "system_unavailable", message: "System info unavailable"))
}
case "system.speech_api_status":
do {
let st = try ProductionSystemInfoProvider().getSpeechApiStatus()
let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "system.speech_api_status", status: "ok", speech_api_status: st)
return Response(id: request.id, ok: true, result: result, error: nil)
} catch {
return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "system_unavailable", message: "Speech API status unavailable"))
}
case "apple_llm.check":
// Minimal health probe — returns status ok with notes that full ANE session is owned separately
let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "apple_llm.check", status: "ok")
return Response(id: request.id, ok: true, result: result, error: nil)
default:
let err = ErrorPayload(
code: "unknown_operation",
message: "Unknown operation: \(request.operation)"
)
return Response(id: request.id, ok: false, result: nil, error: err)
}
}
func dispatch(request: Request) -> Response {
return dispatch(request: request, calendarProvider: ProductionCalendarProvider(), authProvider: ProductionAuthProvider())
}
func ProductionCalendarProvider() -> CalendarListProviding {
return EventKitCalendarProvider()
}
func ProductionAuthProvider() -> CalendarAuthorizationProviding {
return EventKitCalendarAuthorizationProvider()
}
func ProductionCalendarEventsProvider() -> CalendarEventsListProviding {
return EventKitCalendarProvider()
}
func ProductionCalendarEventCreateProvider() -> CalendarEventCreateProviding {
return EventKitCalendarProvider()
}
func ProductionContactsAuthProvider() -> ContactsAuthorizationProviding {
return ContactsAuthorizationProvider()
}
func ProductionContactsSearchProvider() -> ContactsSearchProviding {
return ContactsSearchProvider()
}
func ProductionContactsReadProvider() -> ContactsReadProviding {
return ContactsReadProvider()
}
func ProductionContactsCreateProvider() -> ContactsCreateProviding {
return ContactsCreateProvider()
}
func ProductionRemindersAuthProvider() -> RemindersAuthorizationProviding {
return RemindersAuthorizationProvider()
}
func ProductionRemindersListsProvider() -> RemindersListsProviding {
return EventKitRemindersProvider()
}
func ProductionRemindersListProvider() -> RemindersListProviding {
return EventKitRemindersProvider()
}
func ProductionRemindersCreateProvider() -> RemindersCreateProviding {
return EventKitRemindersProvider()
}