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 }