diff --git a/contributors.svg b/contributors.svg index 37060fd26..4bd4276bc 100644 --- a/contributors.svg +++ b/contributors.svg @@ -10,27 +10,29 @@ + + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/packages/@ant/computer-use-swift/src/backends/darwin.ts b/packages/@ant/computer-use-swift/src/backends/darwin.ts index 620f162a9..6e3c24933 100644 --- a/packages/@ant/computer-use-swift/src/backends/darwin.ts +++ b/packages/@ant/computer-use-swift/src/backends/darwin.ts @@ -159,25 +159,33 @@ export const apps: AppsAPI = { async listInstalled() { try { - const result = await osascript(` - tell application "System Events" - set appList to "" - repeat with appFile in (every file of folder "Applications" of startup disk whose name ends with ".app") - set appPath to POSIX path of (appFile as alias) - set appName to name of appFile - set appList to appList & appPath & "|" & appName & "\\n" - end repeat - return appList - end tell - `) - return result.split('\n').filter(Boolean).map(line => { - const [path, name] = line.split('|', 2) - const displayName = (name ?? '').replace(/\.app$/, '') - return { - bundleId: `com.app.${displayName.toLowerCase().replace(/\s+/g, '-')}`, - displayName, - path: path ?? '', + // Use mdls to enumerate apps and get real bundle identifiers. + // The previous AppleScript approach generated fake bundle IDs + // (com.app.display-name) which prevented request_access from matching + // apps by their real bundle ID (e.g. com.google.Chrome). + const dirs = ['/Applications', '~/Applications', '/System/Applications'] + const allApps: InstalledApp[] = [] + for (const dir of dirs) { + const expanded = dir.startsWith('~') ? join(process.env.HOME ?? '~', dir.slice(1)) : dir + const proc = Bun.spawn( + ['bash', '-c', `for f in "${expanded}"/*.app; do [ -d "$f" ] || continue; bid=$(mdls -name kMDItemCFBundleIdentifier "$f" 2>/dev/null | sed 's/.*= "//;s/"//'); name=$(basename "$f" .app); echo "$f|$name|$bid"; done`], + { stdout: 'pipe', stderr: 'pipe' }, + ) + const text = await new Response(proc.stdout).text() + await proc.exited + for (const line of text.split('\n').filter(Boolean)) { + const [path, displayName, bundleId] = line.split('|', 3) + if (path && displayName && bundleId && bundleId !== '(null)') { + allApps.push({ bundleId, displayName, path }) + } } + } + // Deduplicate by bundleId (prefer /Applications over ~/Applications) + const seen = new Set() + return allApps.filter(app => { + if (seen.has(app.bundleId)) return false + seen.add(app.bundleId) + return true }) } catch { return [] diff --git a/src/utils/computerUse/escHotkey.ts b/src/utils/computerUse/escHotkey.ts index 24ba17cc4..f58bdff0f 100644 --- a/src/utils/computerUse/escHotkey.ts +++ b/src/utils/computerUse/escHotkey.ts @@ -26,7 +26,7 @@ export function registerEscHotkey(onEscape: () => void): boolean { if (process.platform !== 'darwin') return false if (registered) return true const cu = requireComputerUseSwift() - if (!(cu as any).hotkey.registerEscape(onEscape)) { + if (!(cu as any).hotkey?.registerEscape(onEscape)) { // CGEvent.tapCreate failed — typically missing Accessibility permission. // CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81. logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' }) @@ -41,7 +41,7 @@ export function registerEscHotkey(onEscape: () => void): boolean { export function unregisterEscHotkey(): void { if (!registered) return try { - (requireComputerUseSwift() as any).hotkey.unregister() + (requireComputerUseSwift() as any).hotkey?.unregister() } finally { releasePump() registered = false @@ -51,5 +51,5 @@ export function unregisterEscHotkey(): void { export function notifyExpectedEscape(): void { if (!registered) return - (requireComputerUseSwift() as any).hotkey.notifyExpectedEscape() + (requireComputerUseSwift() as any).hotkey?.notifyExpectedEscape() } diff --git a/src/utils/computerUse/hostAdapter.ts b/src/utils/computerUse/hostAdapter.ts index acefbaa3d..ae99d0b18 100644 --- a/src/utils/computerUse/hostAdapter.ts +++ b/src/utils/computerUse/hostAdapter.ts @@ -27,6 +27,38 @@ class DebugLogger implements Logger { } } +// --------------------------------------------------------------------------- +// JXA-based TCC permission probes (fallback when native .node module absent) +// --------------------------------------------------------------------------- + +/** Probe accessibility by asking System Events for a process list. */ +function checkAccessibilityJXA(): boolean { + try { + const result = Bun.spawnSync({ + cmd: ['osascript', '-e', 'tell application "System Events" to get name of every process whose background only is false'], + stdout: 'pipe', + stderr: 'pipe', + }) + return result.exitCode === 0 + } catch { + return false + } +} + +/** Probe screen recording by attempting a 1x1 screencapture. */ +function checkScreenRecordingJXA(): boolean { + try { + const result = Bun.spawnSync({ + cmd: ['screencapture', '-x', '-R', '0,0,1,1', '/dev/null'], + stdout: 'pipe', + stderr: 'pipe', + }) + return result.exitCode === 0 + } catch { + return false + } +} + let cached: ComputerUseHostAdapter | undefined /** @@ -47,8 +79,19 @@ export function getComputerUseHostAdapter(): ComputerUseHostAdapter { ensureOsPermissions: async () => { if (process.platform !== 'darwin') return { granted: true } const cu = requireComputerUseSwift() - const accessibility = (cu as any).tcc.checkAccessibility() - const screenRecording = (cu as any).tcc.checkScreenRecording() + const tcc = (cu as any).tcc + // Native Swift .node module provides tcc.checkAccessibility/checkScreenRecording. + // When absent (decompiled/reverse-engineered build), fall back to JXA probes. + if (tcc) { + const accessibility = tcc.checkAccessibility() + const screenRecording = tcc.checkScreenRecording() + return accessibility && screenRecording + ? { granted: true } + : { granted: false, accessibility, screenRecording } + } + // JXA fallback: try to query System Events (accessibility) and screencapture (screen recording). + const accessibility = checkAccessibilityJXA() + const screenRecording = checkScreenRecordingJXA() return accessibility && screenRecording ? { granted: true } : { granted: false, accessibility, screenRecording }