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,190 @@
import XCTest
@testable import ReynaCLIHostCore
import Foundation
final class ContactsAuthorizationTests: XCTestCase {
struct AlreadyAuthorizedProvider: ContactsAuthorizationProviding {
func authorizationStatus() -> ContactsAuthorizationStatus { .authorized }
func requestAccess() throws -> Bool {
XCTFail("must not call requestAccess when already authorized")
return false
}
}
struct NotDeterminedGranted: ContactsAuthorizationProviding {
func authorizationStatus() -> ContactsAuthorizationStatus { .notDetermined }
func requestAccess() throws -> Bool { true }
}
struct NotDeterminedDenied: ContactsAuthorizationProviding {
func authorizationStatus() -> ContactsAuthorizationStatus { .notDetermined }
func requestAccess() throws -> Bool { false }
}
struct DeniedProvider: ContactsAuthorizationProviding {
func authorizationStatus() -> ContactsAuthorizationStatus { .denied }
func requestAccess() throws -> Bool { false }
}
struct ErrorProvider: ContactsAuthorizationProviding {
func authorizationStatus() -> ContactsAuthorizationStatus { .notDetermined }
func requestAccess() throws -> Bool {
throw ContactsProviderError.unavailable("disk error")
}
}
struct EmptyContactsSearch: ContactsSearchProviding {
func searchContacts(query: String?, limit: Int) throws -> [ContactListItem] { [] }
}
struct EmptyContactsRead: ContactsReadProviding {
func readContact(id: String) throws -> ContactDetailItem {
throw ContactsProviderError.notFound("not found")
}
}
struct EmptyContactsCreate: ContactsCreateProviding {
func createContact(firstName: String?, lastName: String?, organization: String?, jobTitle: String?, note: String?, email: ContactEmailLabelValue?, phone: ContactPhoneLabelValue?) throws -> ContactCreateResult {
throw ContactsProviderError.unavailable("no create")
}
}
struct EmptyCalendarList: CalendarListProviding {
func listCalendars() throws -> [CalendarListItem] { [] }
}
func testAlreadyAuthorizedNoRequest() {
let auth = AlreadyAuthorizedProvider()
let req = Request(id: "c-a1", operation: "contacts.request_access", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: EmptyCalendarList(), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockCalAuth(status: .authorized), contactsAuthProvider: auth, contactsSearchProvider: EmptyContactsSearch(), contactsReadProvider: EmptyContactsRead(), contactsCreateProvider: EmptyContactsCreate())
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.status, "authorized")
XCTAssertNil(resp.result?.contacts, "auth response must not leak contacts")
XCTAssertNil(resp.result?.contact)
}
func testNotDeterminedGrantedReturnsAuthorized() {
let auth = NotDeterminedGranted()
let req = Request(id: "c-a2", operation: "contacts.request_access", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: EmptyCalendarList(), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockCalAuth(status: .authorized), contactsAuthProvider: auth, contactsSearchProvider: EmptyContactsSearch(), contactsReadProvider: EmptyContactsRead(), contactsCreateProvider: EmptyContactsCreate())
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.status, "authorized")
XCTAssertEqual(resp.result?.operation, "contacts.request_access")
}
func testDeniedReturnsPermissionDenied() {
let auth = NotDeterminedDenied()
let req = Request(id: "c-a3", operation: "contacts.request_access", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: EmptyCalendarList(), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockCalAuth(status: .authorized), contactsAuthProvider: auth, contactsSearchProvider: EmptyContactsSearch(), contactsReadProvider: EmptyContactsRead(), contactsCreateProvider: EmptyContactsCreate())
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "permission_denied")
}
func testErrorReturnsContactsUnavailable() {
let auth = ErrorProvider()
let req = Request(id: "c-a4", operation: "contacts.request_access", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: EmptyCalendarList(), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockCalAuth(status: .authorized), contactsAuthProvider: auth, contactsSearchProvider: EmptyContactsSearch(), contactsReadProvider: EmptyContactsRead(), contactsCreateProvider: EmptyContactsCreate())
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "contacts_unavailable")
}
func testContactsSearchDoesNotCallAuthRequest() {
final class SpySearch: ContactsSearchProviding, @unchecked Sendable {
var called = false
func searchContacts(query: String?, limit: Int) throws -> [ContactListItem] {
called = true
return []
}
}
final class SpyAuth: ContactsAuthorizationProviding, @unchecked Sendable {
var didRequest = false
func authorizationStatus() -> ContactsAuthorizationStatus { .authorized }
func requestAccess() throws -> Bool {
didRequest = true
return false
}
}
let search = SpySearch()
let cAuth = SpyAuth()
let req = Request(id: "cs-1", operation: "contacts.search", arguments: .object(["query": .string("john"), "limit": .number(10)]))
let resp = dispatch(request: req, calendarProvider: EmptyCalendarList(), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockCalAuth(status: .authorized), contactsAuthProvider: cAuth, contactsSearchProvider: search, contactsReadProvider: EmptyContactsRead(), contactsCreateProvider: EmptyContactsCreate())
XCTAssertTrue(resp.ok)
XCTAssertFalse(cAuth.didRequest, "contacts.search must never trigger authorization request")
XCTAssertTrue(search.called)
}
// Bridging tests reuse same bridge pattern – verify contacts bridge pumps run loop
func testContactsBridgePumpsMainRunLoop() {
let exp = expectation(description: "contacts bridge")
final class Box: @unchecked Sendable { var granted = false; var error: Error? = nil }
let box = Box()
DispatchQueue.main.async {
let bridge = ContactsMainRunLoopBridge()
do {
box.granted = try bridge.requestAccess(timeout: 2) { completion in
Timer.scheduledTimer(withTimeInterval: 0.02, repeats: false) { _ in
completion(true, nil)
}
}
} catch {
box.error = error
}
exp.fulfill()
}
wait(for: [exp], timeout: 5)
XCTAssertNil(box.error)
XCTAssertTrue(box.granted)
}
// MARK: - Helpers shared
struct FakeEventsProvider: CalendarEventsListProviding {
var events: [CalendarEventItem]
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] { events }
}
struct FakeCreateProvider: CalendarEventCreateProviding {
var willReturn: CalendarEventItem
func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem { willReturn }
}
func sampleEvent() -> CalendarEventItem {
CalendarEventItem(id: "sample", title: "Sample", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: nil, location: nil)
}
struct MockCalAuth: CalendarAuthorizationProviding {
var status: CalendarAuthorizationStatus
func authorizationStatus() -> CalendarAuthorizationStatus { status }
func requestFullAccess() throws -> Bool { false }
}
// Isolation: only ContactsAuthorizationProvider.swift should call requestAccess(for: .contacts)
func testOnlyContactsAuthorizationProviderCallsRequestAccessForContacts() throws {
let fm = FileManager.default
var cur = URL(fileURLWithPath: fm.currentDirectoryPath)
var dirs: [URL] = []
for _ in 0..<10 {
let candCore = cur.appendingPathComponent("Sources/ReynaCLIHostCore")
if fm.fileExists(atPath: candCore.path) { dirs.append(candCore); break }
let cand = cur.appendingPathComponent("native/ReynaCLIHost/Sources/ReynaCLIHostCore")
if fm.fileExists(atPath: cand.path) { dirs.append(cand); break }
cur = cur.deletingLastPathComponent()
}
guard let srcDir = dirs.first else {
XCTFail("Could not find ReynaCLIHostCore sources"); return
}
let files = (try? fm.contentsOfDirectory(at: srcDir, includingPropertiesForKeys: nil)) ?? []
var hits: [String] = []
for file in files where file.pathExtension == "swift" {
guard let content = try? String(contentsOf: file) else { continue }
if content.contains("requestAccess(for:") && file.lastPathComponent != "ContactsAuthorizationProvider.swift" {
// Calendar provider calls requestFullAccessToEvents – not contacts
if content.contains(".contacts") || content.contains("CNContact") {
hits.append(file.lastPathComponent)
}
}
}
XCTAssertTrue(hits.isEmpty, "Only ContactsAuthorizationProvider.swift should call requestAccess(for: .contacts), found extras: \(hits)")
}
}