From d03b9e30463e09356cd4420d04f0ef46d4c07aea Mon Sep 17 00:00:00 2001 From: Natalie Date: Tue, 30 Jun 2026 03:54:02 -0400 Subject: [PATCH] fix(inotes): read Notes via in-process NSAppleScript so the TCC grant applies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit osascript runs out-of-process, so TCC attributes the Apple event to osascript rather than MacSync — every Notes read was denied even after the user granted MacSync → Notes Automation (the script works fine from Terminal). Send the event in-process via NSAppleScript on the main actor (tell-application events need a live run loop for their reply); the grant is then honored and notes sync. The read is infrequent (600s cycle) and brief enough for a menu agent. Co-Authored-By: Claude Opus 4.8 --- .../inotes/Sources/INoteSync/Reader.swift | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/@packages/inotes/Sources/INoteSync/Reader.swift b/@packages/inotes/Sources/INoteSync/Reader.swift index 05c0cab..be39bee 100644 --- a/@packages/inotes/Sources/INoteSync/Reader.swift +++ b/@packages/inotes/Sources/INoteSync/Reader.swift @@ -140,34 +140,28 @@ public final class NotesReader { return out } - /// Run AppleScript via /usr/bin/osascript subprocess. Returns - /// `(success, stdout-or-stderr)`. We deliberately do NOT use - /// `NSAppleScript` because it blocks the calling thread; `osascript` - /// runs out-of-process and is awaitable. + /// Run AppleScript IN-PROCESS via `NSAppleScript`. Returns + /// `(success, output-or-error)`. + /// + /// We must NOT shell out to `/usr/bin/osascript`: TCC attributes the Apple + /// event a subprocess sends to that subprocess (`osascript`), not to MacSync, + /// so the `MacSync → Notes` Automation grant doesn't apply and every read is + /// denied even after the user allows it. NSAppleScript sends the event from + /// MacSync itself, so the grant is honored. + /// + /// Run on the main actor: `tell application` events need a live run loop to + /// receive their reply, so a detached thread can hang. The Notes read is + /// infrequent (the 600s sync cycle) and brief enough that a menu-bar agent + /// can absorb it on the main thread. + @MainActor private func runAppleScript(_ source: String) async -> (Bool, String) { - await Task.detached { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript") - process.arguments = ["-e", source] - let outPipe = Pipe() - let errPipe = Pipe() - process.standardOutput = outPipe - process.standardError = errPipe - do { - try process.run() - process.waitUntilExit() - let outData = outPipe.fileHandleForReading.readDataToEndOfFile() - let errData = errPipe.fileHandleForReading.readDataToEndOfFile() - let outStr = String(data: outData, encoding: .utf8) ?? "" - let errStr = String(data: errData, encoding: .utf8) ?? "" - if process.terminationStatus == 0 { - return (true, outStr) - } else { - return (false, errStr.isEmpty ? outStr : errStr) - } - } catch { - return (false, error.localizedDescription) - } - }.value + var errorInfo: NSDictionary? + let script = NSAppleScript(source: source) + let result = script?.executeAndReturnError(&errorInfo) + if let errorInfo { + let msg = (errorInfo[NSAppleScript.errorMessage] as? String) ?? "\(errorInfo)" + return (false, msg) + } + return (true, result?.stringValue ?? "") } }