fix(inotes): read Notes via in-process NSAppleScript so the TCC grant applies

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 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-30 03:54:02 -04:00
parent 8597406898
commit d03b9e3046

View file

@ -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 ?? "")
}
}