spb/zyquo-agent Public MIT
The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.
Swift 94.7%
Shell 4.1%
Python 0.7%
Makefile 0.5%
1//2// AppleScriptTool.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The agent's `osascript` tool: runs AppleScript to automate macOS apps9// (Finder, Notes, Mail, Calendar, System Events…). Single-line scripts run10// via `osascript -e`; multi-line scripts are written to a temp file under11// the workspace's `.zyquo/scripts/` dir and run as `osascript <file>`.12// Same policy → execute → audit path as the shell tool. When macOS TCC13// blocks Apple events (error -1743 / "Not authorized"), the result explains14// how to grant Automation access instead of leaving a cryptic error.15//1617import Foundation1819struct AppleScriptTool: Tool {20 private let executor: ExecutionService2122 init(executor: ExecutionService) {23 self.executor = executor24 }2526 let name = "osascript"2728 let description = """29 Run an AppleScript on this Mac via /usr/bin/osascript to automate \30 macOS applications: Finder, Notes, Reminders, Mail, Calendar, Safari, \31 Music, System Events, and any scriptable app. Use it for things the \32 shell cannot do cleanly — creating a Note or Reminder, reading \33 Calendar events, controlling app windows. AppleScript is powerful \34 and therefore gated: scripts are usually held for user approval, and \35 the first automation of each app triggers a one-time macOS \36 permission prompt the user must accept. If the result mentions \37 \"Not authorized\", the user needs to grant access in System \38 Settings > Privacy & Security > Automation — tell them so. Keep \39 scripts short and single-purpose; the script's `return` value is \40 returned to you as text.41 """4243 var parametersSchema: JSONValue {44 .object([45 "type": .string("object"),46 "properties": .object([47 "script": .object([48 "type": .string("string"),49 "description": .string("The complete AppleScript source to execute."),50 ]),51 "timeout_seconds": .object([52 "type": .string("integer"),53 "description": .string("Optional timeout in seconds for this script (default 120)."),54 ]),55 "explanation": .object([56 "type": .string("string"),57 "description": .string("One short sentence explaining what this script does and why — shown to the user on approval cards."),58 ]),59 ]),60 "required": .array([.string("script")]),61 ])62 }6364 func execute(arguments: JSONValue, context: ToolExecutionContext) async throws -> ToolExecutionResult {65 guard case .object(let object) = arguments,66 case .string(let script)? = object["script"],67 !script.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {68 return .failure("osascript: missing required parameter `script`.")69 }70 var timeout: TimeInterval?71 if case .number(let seconds)? = object["timeout_seconds"], seconds > 0 {72 timeout = seconds73 }74 var explanation: String?75 if case .string(let text)? = object["explanation"] {76 explanation = text77 }7879 // ---- Safety gate (never skipped) --------------------------------80 let cleared: ClearedAction81 do {82 cleared = try await context.policy.clear(ActionRequest(83 kind: .appleScript,84 payload: script,85 cwd: context.workspaceURL,86 explanation: explanation87 ))88 } catch let denial as PolicyDenied {89 await context.audit.append(AuditEntry(90 actionKind: name,91 payload: script,92 cwd: context.workspaceURL.path,93 ruling: PolicyDecisionRecord.Ruling.denied.rawValue,94 exitCode: nil,95 outputExcerpt: denial.reason96 ))97 return .failure("Script not run — denied by the safety policy: \(denial.reason)")98 }99100 // ---- Execute -----------------------------------------------------101 let effectiveScript = cleared.payload102 let isMultiline = effectiveScript.contains("\n")103 let result: ExecutionResult104 var scriptFileURL: URL?105 do {106 if isMultiline {107 // Multi-line: write to .zyquo/scripts/ and run the file.108 let scriptsDir = context.workspaceURL109 .appendingPathComponent(".zyquo")110 .appendingPathComponent("scripts")111 try FileManager.default.createDirectory(at: scriptsDir, withIntermediateDirectories: true)112 let fileURL = scriptsDir.appendingPathComponent("osascript-\(UUID().uuidString.prefix(8)).applescript")113 try effectiveScript.write(to: fileURL, atomically: true, encoding: .utf8)114 scriptFileURL = fileURL115 result = try await executor.runOSAScriptFile(116 scriptFile: fileURL,117 cwd: context.workspaceURL,118 timeout: timeout,119 onOutput: context.onOutput120 )121 } else {122 result = try await executor.runOSAScript(123 lines: [effectiveScript],124 cwd: context.workspaceURL,125 timeout: timeout,126 onOutput: context.onOutput127 )128 }129 } catch let launchError as ExecutionLaunchError {130 await context.audit.append(AuditEntry(131 actionKind: name,132 payload: effectiveScript,133 cwd: context.workspaceURL.path,134 ruling: cleared.decision.ruling.rawValue,135 exitCode: nil,136 outputExcerpt: launchError.reason137 ))138 return .failure("osascript failed to launch: \(launchError.reason)")139 }140 // Keep the script file for the audit trail (it lives in .zyquo/,141 // which is excluded from workspace file tracking).142 _ = scriptFileURL143144 // ---- Audit -------------------------------------------------------145 await context.audit.append(AuditEntry(146 actionKind: name,147 payload: effectiveScript,148 cwd: context.workspaceURL.path,149 ruling: cleared.decision.ruling.rawValue,150 exitCode: result.exitCode,151 outputExcerpt: result.combinedOutput152 ))153154 // ---- Result for the model ----------------------------------------155 var content = result.combinedOutput156 if result.isTimeout {157 content += "\n[timed out after \(Int(timeout ?? 120))s — osascript was killed]"158 }159 if result.isCancelled {160 content += "\n[cancelled by the user]"161 }162 content += "\n[exit code: \(result.exitCode.map(String.init) ?? "none")]"163164 // TCC / Automation permission guidance.165 let lowered = result.stderr.lowercased()166 if lowered.contains("not authorized") || lowered.contains("-1743") || result.stderr.contains("errAEEventNotPermitted") {167 content += """168 \n[Automation permission needed] macOS blocked Zyquo Agent from \169 controlling that application. Ask the user to open System \170 Settings > Privacy & Security > Automation, find "Zyquo Agent", \171 and enable the target app — then run the script again.172 """173 }174175 let failed = result.isTimeout || result.isCancelled || (result.exitCode ?? 1) != 0176 return ToolExecutionResult(content: content, isError: failed)177 }178}179