源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)

This commit is contained in:
agent-radar
2026-09-02 01:05:41 +08:00
commit 04bac1f6e2
2200 changed files with 489415 additions and 0 deletions
@@ -0,0 +1,2 @@
.build/
.swiftpm/
@@ -0,0 +1,13 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "HandsFree",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "HandsFreeComputerUse",
path: "Sources/ComputerUse"
),
]
)
@@ -0,0 +1,451 @@
import AppKit
import ApplicationServices
import ScreenCaptureKit
final class AccessibilityService: @unchecked Sendable {
private let screenshotImageWidth: CGFloat = 768
private let maxElements = 250
private let maxDepth = 22
private let importantRoles: Set<String> = [
"AXButton", "AXCheckBox", "AXRadioButton", "AXPopUpButton", "AXMenuButton",
"AXComboBox", "AXTextField", "AXTextArea", "AXSearchField", "AXLink",
"AXSlider", "AXIncrementor", "AXScrollArea", "AXScrollBar", "AXTabGroup",
"AXTab", "AXMenuItem", "AXCell", "AXRow", "AXStaticText", "AXImage",
"AXOutline", "AXTable", "AXList", "AXGroup",
]
private let enhancedLock = NSLock()
private var enhancedPIDs = Set<pid_t>()
func resolveTarget(appName: String?, pid requestedPID: pid_t? = nil, windowTitle: String? = nil) throws -> WindowTarget {
guard AXIsProcessTrusted() else { throw ComputerUseError.accessibilityDenied }
let app = try resolveApp(appName: appName, pid: requestedPID)
let pid = app.processIdentifier
let axApp = AXUIElementCreateApplication(pid)
if enableEnhancedAccessibility(axApp: axApp, pid: pid) {
// Chromium and Electron build the renderer AX tree lazily; give it a moment.
Thread.sleep(forTimeInterval: 0.35)
}
let axWindow = firstAXWindow(axApp: axApp, title: windowTitle)
let title = axWindow.flatMap { axString($0, kAXTitleAttribute) }
let info = firstCGWindowInfo(pid: pid, title: title)
let bounds = axWindow.flatMap(axFrame) ?? info?.bounds
guard let bounds, bounds.width > 20, bounds.height > 20 else {
throw ComputerUseError.noWindow(app.localizedName ?? appName ?? "frontmost app")
}
return WindowTarget(
appName: app.localizedName ?? "Unknown",
pid: pid,
windowNumber: info?.number,
windowTitle: title ?? info?.title,
bounds: bounds,
isFrontmost: NSWorkspace.shared.frontmostApplication?.processIdentifier == pid,
axWindow: axWindow
)
}
func snapshot(target: WindowTarget, strictMode: Bool, backgroundActivated: Bool) async throws -> AppSnapshot {
let records = records(target: target)
let (data, meta) = try await captureScreenshot(target: target)
return AppSnapshot(
id: "",
observation: 0,
appName: target.appName,
pid: target.pid,
windowNumber: target.windowNumber,
windowTitle: target.windowTitle,
screenshotData: data,
screenshotMimeType: "image/jpeg",
screenshotMeta: meta,
records: records,
strictMode: strictMode,
backgroundActivated: backgroundActivated,
recentActions: [],
addedLabels: [],
removedLabels: []
)
}
func records(target: WindowTarget) -> [AXElementRecord] {
target.axWindow.map(semanticRecords(window:)) ?? []
}
func press(record: AXElementRecord) -> Bool {
AXUIElementPerformAction(record.element, kAXPressAction as CFString) == .success
}
func focus(record: AXElementRecord) -> Bool {
AXUIElementSetAttributeValue(record.element, kAXFocusedAttribute as CFString, true as CFBoolean) == .success
}
func setValue(record: AXElementRecord, value: String) -> Bool {
var settable = DarwinBoolean(false)
guard AXUIElementIsAttributeSettable(record.element, kAXValueAttribute as CFString, &settable) == .success,
settable.boolValue else {
return false
}
return AXUIElementSetAttributeValue(record.element, kAXValueAttribute as CFString, value as CFString) == .success
}
func performAction(record: AXElementRecord, action: String) -> Bool {
AXUIElementPerformAction(record.element, action as CFString) == .success
}
func value(record: AXElementRecord) -> String? {
axString(record.element, kAXValueAttribute)
}
/// Chromium browsers and Electron apps only expose web content to AX after an
/// assistive client announces itself. VoiceOver does this by setting
/// AXEnhancedUserInterface; Electron additionally supports AXManualAccessibility.
/// Returns true when the attribute was newly applied to this pid.
private func enableEnhancedAccessibility(axApp: AXUIElement, pid: pid_t) -> Bool {
enhancedLock.lock()
let isNew = enhancedPIDs.insert(pid).inserted
enhancedLock.unlock()
guard isNew else { return false }
// Chromium reacts to the set attempt even though it reports an AXError
// (.notImplemented / .attributeUnsupported), so the return codes are
// logged for diagnostics but intentionally not treated as failure.
let enhanced = AXUIElementSetAttributeValue(axApp, "AXEnhancedUserInterface" as CFString, kCFBooleanTrue)
let manual = AXUIElementSetAttributeValue(axApp, "AXManualAccessibility" as CFString, kCFBooleanTrue)
fputs("[ComputerUse] enhanced-ax pid=\(pid) AXEnhancedUserInterface=\(enhanced.rawValue) AXManualAccessibility=\(manual.rawValue)\n", stderr)
return true
}
private func resolveApp(appName: String?, pid: pid_t?) throws -> NSRunningApplication {
if let pid {
guard let app = NSRunningApplication(processIdentifier: pid) else {
throw ComputerUseError.appNotFound("pid \(pid)")
}
return app
}
guard let rawName = appName?.trimmingCharacters(in: .whitespacesAndNewlines), !rawName.isEmpty else {
guard let frontmost = NSWorkspace.shared.frontmostApplication else {
throw ComputerUseError.noFrontmostApplication
}
return frontmost
}
let needle = rawName.lowercased()
let regularApps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
if let exact = regularApps.first(where: { $0.localizedName?.lowercased() == needle }) {
return exact
}
if let contains = regularApps.first(where: { $0.localizedName?.lowercased().contains(needle) == true }) {
return contains
}
throw ComputerUseError.appNotFound(rawName)
}
private func firstAXWindow(axApp: AXUIElement, title preferredTitle: String?) -> AXUIElement? {
var windowValue: CFTypeRef?
guard AXUIElementCopyAttributeValue(axApp, kAXWindowsAttribute as CFString, &windowValue) == .success,
let windows = windowValue as? [AXUIElement] else {
return nil
}
let usable = windows.filter { window in
guard let frame = axFrame(window) else { return false }
return frame.width > 20 && frame.height > 20
}
if let preferredTitle, let match = usable.first(where: { axString($0, kAXTitleAttribute) == preferredTitle }) {
return match
}
return usable.first
}
private func semanticRecords(window: AXUIElement) -> [AXElementRecord] {
var records: [AXElementRecord] = []
collect(element: window, depth: 0, records: &records)
return records
}
private func collect(element: AXUIElement, depth: Int, records: inout [AXElementRecord]) {
guard depth <= maxDepth, records.count < maxElements else { return }
let rawRole = axString(element, kAXRoleAttribute) ?? "AXUnknown"
let role = normalizedRole(rawRole)
let value = axString(element, kAXValueAttribute).map { String($0.prefix(120)) }
let label = semanticLabel(element: element, role: role, value: value)
let actions = axActions(element)
let frame = axFrame(element)
let capabilities = capabilitiesFor(element: element, rawRole: rawRole, actions: actions)
let shouldSurface = shouldSurfaceElement(rawRole: rawRole, label: label, value: value, frame: frame, capabilities: capabilities)
if shouldSurface, let frame {
let id = records.count + 1
let semantic = SemanticAXElement(
id: id,
ref: "{e\(id)}",
role: role,
label: label,
value: value,
frame: ElementFrame(
x: Int(frame.origin.x),
y: Int(frame.origin.y),
width: Int(frame.width),
height: Int(frame.height)
),
state: stateFor(element: element, rawRole: rawRole),
capabilities: capabilities
)
records.append(AXElementRecord(element: element, semantic: semantic))
}
for child in axChildren(element) {
collect(element: child, depth: depth + 1, records: &records)
if records.count >= maxElements { break }
}
}
private func shouldSurfaceElement(rawRole: String, label: String, value: String?, frame: CGRect?, capabilities: AXElementCapabilities) -> Bool {
guard let frame, frame.width > 1, frame.height > 1 else { return false }
let hasSemanticText = !label.isEmpty || value?.isEmpty == false
let interactive = capabilities.canPress || capabilities.canFocus || capabilities.canScroll || capabilities.canAdjust || capabilities.canSetValue
if interactive { return true }
if !importantRoles.contains(rawRole) { return false }
if rawRole == "AXGroup" { return hasSemanticText && frame.width < 900 && frame.height < 700 }
return hasSemanticText
}
private func semanticLabel(element: AXUIElement, role: String, value: String?) -> String {
let candidates = [
axString(element, kAXTitleAttribute),
axString(element, kAXDescriptionAttribute),
axString(element, kAXHelpAttribute),
axString(element, kAXIdentifierAttribute),
value,
]
for candidate in candidates {
if let trimmed = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty {
return String(trimmed.prefix(120))
}
}
return role
}
private func capabilitiesFor(element: AXUIElement, rawRole: String, actions: [String]) -> AXElementCapabilities {
var valueSettable = DarwinBoolean(false)
let canSetValue = AXUIElementIsAttributeSettable(element, kAXValueAttribute as CFString, &valueSettable) == .success && valueSettable.boolValue
var focusSettable = DarwinBoolean(false)
let canFocus = AXUIElementIsAttributeSettable(element, kAXFocusedAttribute as CFString, &focusSettable) == .success && focusSettable.boolValue
let canAdjust = actions.contains(kAXIncrementAction) || actions.contains(kAXDecrementAction) || rawRole == "AXSlider" || rawRole == "AXIncrementor"
let canScroll = actions.contains("AXScrollToVisible") || rawRole == "AXScrollArea" || rawRole == "AXScrollBar"
let canPress = actions.contains(kAXPressAction) || ["AXButton", "AXCheckBox", "AXRadioButton", "AXLink", "AXMenuItem", "AXPopUpButton", "AXMenuButton", "AXCell"].contains(rawRole)
return AXElementCapabilities(
canPress: canPress,
canFocus: canFocus,
canScroll: canScroll,
canAdjust: canAdjust,
canSetValue: canSetValue,
actions: actions
)
}
private func stateFor(element: AXUIElement, rawRole: String) -> AXElementState {
AXElementState(
enabled: axBool(element, kAXEnabledAttribute),
focused: axBool(element, kAXFocusedAttribute),
selected: axBool(element, kAXSelectedAttribute),
expanded: axBool(element, kAXExpandedAttribute),
checked: rawRole == "AXCheckBox" || rawRole == "AXRadioButton" ? axBool(element, kAXValueAttribute) : nil
)
}
private func axChildren(_ element: AXUIElement) -> [AXUIElement] {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value) == .success,
let children = value as? [AXUIElement] else {
return []
}
return children
}
private func axActions(_ element: AXUIElement) -> [String] {
var actionNames: CFArray?
guard AXUIElementCopyActionNames(element, &actionNames) == .success,
let names = actionNames as? [String] else {
return []
}
return names
}
private func axString(_ element: AXUIElement, _ attribute: String) -> String? {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
let value else {
return nil
}
if let string = value as? String, !string.isEmpty { return string }
if let attributed = value as? NSAttributedString, !attributed.string.isEmpty { return attributed.string }
if let number = value as? NSNumber { return number.stringValue }
return nil
}
private func axBool(_ element: AXUIElement, _ attribute: String) -> Bool? {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
let value else {
return nil
}
if let bool = value as? Bool { return bool }
if let number = value as? NSNumber { return number.boolValue }
return nil
}
private func axFrame(_ element: AXUIElement) -> CGRect? {
guard let position = axPoint(element, kAXPositionAttribute), let size = axSize(element, kAXSizeAttribute) else {
return nil
}
return CGRect(origin: position, size: size)
}
private func axPoint(_ element: AXUIElement, _ attribute: String) -> CGPoint? {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
let value,
CFGetTypeID(value) == AXValueGetTypeID() else {
return nil
}
let axValue = value as! AXValue
guard AXValueGetType(axValue) == .cgPoint else { return nil }
var point = CGPoint.zero
guard AXValueGetValue(axValue, .cgPoint, &point) else { return nil }
return point
}
private func axSize(_ element: AXUIElement, _ attribute: String) -> CGSize? {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
let value,
CFGetTypeID(value) == AXValueGetTypeID() else {
return nil
}
let axValue = value as! AXValue
guard AXValueGetType(axValue) == .cgSize else { return nil }
var size = CGSize.zero
guard AXValueGetValue(axValue, .cgSize, &size) else { return nil }
return size
}
private func normalizedRole(_ rawRole: String) -> String {
rawRole.hasPrefix("AX") ? String(rawRole.dropFirst(2)) : rawRole
}
private func firstCGWindowInfo(pid: pid_t, title: String?) -> (number: Int, title: String?, bounds: CGRect)? {
guard let list = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
return nil
}
let candidates = list.compactMap { info -> (number: Int, title: String?, bounds: CGRect)? in
guard let ownerPID = info[kCGWindowOwnerPID as String] as? Int32,
ownerPID == pid,
let layer = info[kCGWindowLayer as String] as? Int,
layer == 0,
let number = info[kCGWindowNumber as String] as? Int,
let boundsDict = info[kCGWindowBounds as String] as? [String: Any] else {
return nil
}
let bounds = CGRect(
x: cgFloat(boundsDict["X"]),
y: cgFloat(boundsDict["Y"]),
width: cgFloat(boundsDict["Width"]),
height: cgFloat(boundsDict["Height"])
)
guard bounds.width > 20, bounds.height > 20 else { return nil }
return (number, info[kCGWindowName as String] as? String, bounds)
}
if let title, let exact = candidates.first(where: { $0.title == title }) {
return exact
}
return candidates.first
}
private func captureScreenshot(target: WindowTarget) async throws -> (Data, ScreenshotMetadata) {
let cgImage = await screenCaptureKitImage(target: target) ?? legacyCaptureImage(target: target)
guard let cgImage else { throw ComputerUseError.screenshotFailed }
return try encodeScreenshot(cgImage, capturedBounds: target.bounds)
}
private func legacyCaptureImage(target: WindowTarget) -> CGImage? {
if let windowNumber = target.windowNumber {
return CGWindowListCreateImage(
CGRect.null,
.optionIncludingWindow,
CGWindowID(windowNumber),
[.bestResolution, .boundsIgnoreFraming]
)
}
return CGWindowListCreateImage(target.bounds, .optionOnScreenOnly, kCGNullWindowID, [.bestResolution])
}
private func screenCaptureKitImage(target: WindowTarget) async -> CGImage? {
guard let windowNumber = target.windowNumber else { return nil }
do {
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
guard let window = content.windows.first(where: { Int($0.windowID) == windowNumber }) else { return nil }
let configuration = SCStreamConfiguration()
let scale = screenScale(for: target.bounds)
configuration.width = max(1, Int(target.bounds.width * scale))
configuration.height = max(1, Int(target.bounds.height * scale))
configuration.showsCursor = false
let filter = SCContentFilter(desktopIndependentWindow: window)
return try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
} catch {
return nil
}
}
private func screenScale(for bounds: CGRect) -> CGFloat {
NSScreen.screens.first(where: { $0.frame.intersects(bounds) })?.backingScaleFactor
?? NSScreen.main?.backingScaleFactor
?? 2
}
private func encodeScreenshot(_ cgImage: CGImage, capturedBounds: CGRect) throws -> (Data, ScreenshotMetadata) {
let rawWidth = CGFloat(cgImage.width)
let rawHeight = CGFloat(cgImage.height)
let targetWidth = min(screenshotImageWidth, rawWidth)
let targetHeight = rawHeight * (targetWidth / rawWidth)
let source = NSImage(cgImage: cgImage, size: NSSize(width: rawWidth, height: rawHeight))
let resized = NSImage(size: NSSize(width: targetWidth, height: targetHeight))
resized.lockFocus()
source.draw(in: NSRect(x: 0, y: 0, width: targetWidth, height: targetHeight))
resized.unlockFocus()
guard let tiff = resized.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff),
let jpeg = rep.representation(using: .jpeg, properties: [.compressionFactor: 0.45]) else {
throw ComputerUseError.screenshotFailed
}
return (
jpeg,
ScreenshotMetadata(
imageWidth: Int(targetWidth),
imageHeight: Int(targetHeight),
capturedBounds: capturedBounds
)
)
}
private func cgFloat(_ value: Any?) -> CGFloat {
if let value = value as? CGFloat { return value }
if let value = value as? Double { return CGFloat(value) }
if let value = value as? Int { return CGFloat(value) }
if let value = value as? NSNumber { return CGFloat(truncating: value) }
return 0
}
}
@@ -0,0 +1,104 @@
import AppKit
final class AgentCursorOverlay: @unchecked Sendable {
static let shared = AgentCursorOverlay()
private var panel: AgentCursorPanel?
private var hideWorkItem: DispatchWorkItem?
private let size = CGSize(width: 30, height: 34)
func show(at point: CGPoint) {
DispatchQueue.main.async { [weak self] in
self?.showOnMain(at: point)
}
}
private func showOnMain(at point: CGPoint) {
let panel = ensurePanel()
panel.setFrame(frame(for: point), display: true)
panel.orderFrontRegardless()
hideWorkItem?.cancel()
let nextHide = DispatchWorkItem { [weak panel] in
panel?.orderOut(nil)
}
hideWorkItem = nextHide
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: nextHide)
}
private func ensurePanel() -> AgentCursorPanel {
if let panel { return panel }
let panel = AgentCursorPanel(
contentRect: NSRect(origin: .zero, size: size),
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
panel.backgroundColor = .clear
panel.isOpaque = false
panel.hasShadow = false
panel.ignoresMouseEvents = true
panel.level = .floating
panel.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary]
panel.contentView = AgentCursorView(frame: NSRect(origin: .zero, size: size))
self.panel = panel
return panel
}
private func frame(for topLeftPoint: CGPoint) -> NSRect {
let screen = screenContaining(topLeftPoint) ?? NSScreen.main
let screenFrame = screen?.frame ?? NSRect(x: 0, y: 0, width: 1440, height: 900)
let x = screenFrame.minX + topLeftPoint.x - 2
let y = screenFrame.maxY - topLeftPoint.y - size.height + 2
return NSRect(x: x, y: y, width: size.width, height: size.height)
}
private func screenContaining(_ topLeftPoint: CGPoint) -> NSScreen? {
NSScreen.screens.first { screen in
let frame = screen.frame
return topLeftPoint.x >= frame.minX
&& topLeftPoint.x <= frame.maxX
&& topLeftPoint.y >= 0
&& topLeftPoint.y <= frame.height
}
}
}
final class AgentCursorPanel: NSPanel {
override var canBecomeKey: Bool { false }
override var canBecomeMain: Bool { false }
}
final class AgentCursorView: NSView {
override var isOpaque: Bool { false }
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let shadow = NSShadow()
shadow.shadowBlurRadius = 3
shadow.shadowOffset = NSSize(width: 0, height: -1)
shadow.shadowColor = NSColor.black.withAlphaComponent(0.35)
shadow.set()
let path = NSBezierPath()
path.move(to: NSPoint(x: 4, y: 30))
path.line(to: NSPoint(x: 4, y: 4))
path.line(to: NSPoint(x: 12, y: 11))
path.line(to: NSPoint(x: 17, y: 1))
path.line(to: NSPoint(x: 22, y: 4))
path.line(to: NSPoint(x: 17, y: 14))
path.line(to: NSPoint(x: 27, y: 14))
path.close()
NSColor.white.setFill()
path.fill()
shadow.shadowColor = .clear
shadow.set()
NSColor.systemBlue.setStroke()
path.lineWidth = 2
path.stroke()
}
}
@@ -0,0 +1,164 @@
import AppKit
import CoreGraphics
final class BackgroundActivationSession: @unchecked Sendable {
private final class TapContext {
let suppressFocusMessages: Bool
init(suppressFocusMessages: Bool) {
self.suppressFocusMessages = suppressFocusMessages
}
}
private struct TapRef {
let tap: CFMachPort
let source: CFRunLoopSource
let context: TapContext
}
private let previousPID: pid_t
private let targetPID: pid_t
private var taps: [TapRef] = []
private var runLoop: CFRunLoop?
private var thread: Thread?
private var startupError: Error?
private var started = false
private var lastTarget: WindowTarget?
init(previousPID: pid_t, targetPID: pid_t) {
self.previousPID = previousPID
self.targetPID = targetPID
}
deinit {
stop()
}
func start() throws {
guard !started else { return }
let ready = DispatchSemaphore(value: 0)
let thread = Thread { [weak self] in
guard let self else {
ready.signal()
return
}
self.runLoop = CFRunLoopGetCurrent()
do {
try self.installTapsOnCurrentRunLoop()
} catch {
self.startupError = error
}
ready.signal()
if self.startupError == nil {
CFRunLoopRun()
}
}
thread.name = "OpenWorkBackgroundActivationSession"
self.thread = thread
thread.start()
ready.wait()
if let startupError {
throw startupError
}
started = true
}
func activate(target: WindowTarget) async throws {
guard let windowNumber = target.windowNumber else {
throw ComputerUseError.strictModeViolation("target window has no CG window number")
}
postAppKitDefined(subtype: 1, target: target, windowNumber: windowNumber)
try await Task.sleep(nanoseconds: 25_000_000)
try await BackgroundInputDispatcher.click(pid: target.pid, windowNumber: windowNumber, point: target.center)
try await Task.sleep(nanoseconds: 80_000_000)
lastTarget = target
}
func stop() {
if let target = lastTarget, let windowNumber = target.windowNumber {
postAppKitDefined(subtype: 2, target: target, windowNumber: windowNumber)
}
for tapRef in taps {
CFMachPortInvalidate(tapRef.tap)
}
taps.removeAll()
if let runLoop {
CFRunLoopStop(runLoop)
}
runLoop = nil
thread = nil
started = false
}
private func installTapsOnCurrentRunLoop() throws {
if previousPID != targetPID {
taps.append(try installTap(pid: previousPID, suppressFocusMessages: true))
}
taps.append(try installTap(pid: targetPID, suppressFocusMessages: false))
}
private func installTap(pid: pid_t, suppressFocusMessages: Bool) throws -> TapRef {
let context = TapContext(suppressFocusMessages: suppressFocusMessages)
let userInfo = Unmanaged.passUnretained(context).toOpaque()
guard let tap = CGEvent.tapCreateForPid(
pid: pid,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask.max,
callback: BackgroundActivationSession.eventTapCallback,
userInfo: userInfo
) else {
throw ComputerUseError.strictModeViolation("could not install per-process event tap for pid \(pid)")
}
guard let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else {
CFMachPortInvalidate(tap)
throw ComputerUseError.strictModeViolation("could not create run loop source for pid \(pid) event tap")
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
return TapRef(tap: tap, source: source, context: context)
}
private func postAppKitDefined(subtype: Int16, target: WindowTarget, windowNumber: Int) {
guard let event = NSEvent.otherEvent(
with: .appKitDefined,
location: target.center,
modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: windowNumber,
context: nil,
subtype: subtype,
data1: 0,
data2: 0
)?.cgEvent else {
return
}
BackgroundInputDispatcher.address(event, pid: target.pid, windowNumber: windowNumber)
event.postToPid(target.pid)
}
private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
return Unmanaged.passUnretained(event)
}
guard let userInfo else {
return Unmanaged.passUnretained(event)
}
let context = Unmanaged<TapContext>.fromOpaque(userInfo).takeUnretainedValue()
if context.suppressFocusMessages && BackgroundActivationSession.isFocusMessage(type) {
return nil
}
return Unmanaged.passUnretained(event)
}
private static func isFocusMessage(_ type: CGEventType) -> Bool {
let raw = Int(type.rawValue)
return raw == 13 || raw == 19 || raw == 20
}
}
@@ -0,0 +1,163 @@
import Foundation
import CoreGraphics
enum BackgroundInputDispatcher {
private static let privateWindowField = CGEventField(rawValue: 51)
private static let privateRouteField = CGEventField(rawValue: 58)
static func click(pid: pid_t, windowNumber: Int, point: CGPoint, doubleClick: Bool = false) async throws {
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
let clickCount = doubleClick ? 2 : 1
for clickState in 1...clickCount {
guard let down = CGEvent(mouseEventSource: source, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left),
let up = CGEvent(mouseEventSource: source, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left) else {
throw ComputerUseError.eventCreationFailed
}
address(down, pid: pid, windowNumber: windowNumber)
down.setIntegerValueField(.mouseEventClickState, value: Int64(clickState))
down.setDoubleValueField(.mouseEventPressure, value: 1)
down.postToPid(pid)
try await Task.sleep(nanoseconds: 30_000_000)
address(up, pid: pid, windowNumber: windowNumber)
up.setIntegerValueField(.mouseEventClickState, value: Int64(clickState))
up.setDoubleValueField(.mouseEventPressure, value: 0)
up.postToPid(pid)
if clickState < clickCount {
try await Task.sleep(nanoseconds: 50_000_000)
}
}
}
static func scroll(pid: pid_t, windowNumber: Int, point: CGPoint, deltaX: Int32, deltaY: Int32) throws {
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
guard let event = CGEvent(scrollWheelEvent2Source: source, units: .line, wheelCount: 2, wheel1: deltaY, wheel2: deltaX, wheel3: 0) else {
throw ComputerUseError.eventCreationFailed
}
event.location = point
address(event, pid: pid, windowNumber: windowNumber)
event.postToPid(pid)
}
static func typeText(pid: pid_t, text: String) throws {
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
let units = Array(text.utf16)
let chunkSize = 20
for start in stride(from: 0, to: units.count, by: chunkSize) {
let end = min(start + chunkSize, units.count)
let chunk = Array(units[start..<end])
guard let event = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: true) else {
throw ComputerUseError.eventCreationFailed
}
event.setIntegerValueField(.eventTargetUnixProcessID, value: Int64(pid))
event.keyboardSetUnicodeString(stringLength: chunk.count, unicodeString: chunk)
event.postToPid(pid)
Thread.sleep(forTimeInterval: 0.01)
}
}
static func pressKey(pid: pid_t, combo: String) throws {
let parsed = try parseCombo(combo)
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
// Post real modifier key events around the main key. Chromium and other
// apps ignore bare flag bits on background-posted events, so flags alone
// are not enough for combos like command+a.
let modifierKeys: [(flag: CGEventFlags, keyCode: CGKeyCode)] = [
(.maskCommand, 0x37), (.maskShift, 0x38), (.maskAlternate, 0x3A), (.maskControl, 0x3B),
]
let activeModifiers = modifierKeys.filter { parsed.flags.contains($0.flag) }
var accumulated: CGEventFlags = []
for modifier in activeModifiers {
accumulated.insert(modifier.flag)
try postKey(source: source, pid: pid, keyCode: modifier.keyCode, keyDown: true, flags: accumulated)
Thread.sleep(forTimeInterval: 0.005)
}
try postKey(source: source, pid: pid, keyCode: parsed.keyCode, keyDown: true, flags: parsed.flags)
Thread.sleep(forTimeInterval: 0.01)
try postKey(source: source, pid: pid, keyCode: parsed.keyCode, keyDown: false, flags: parsed.flags)
for modifier in activeModifiers.reversed() {
accumulated.remove(modifier.flag)
Thread.sleep(forTimeInterval: 0.005)
try postKey(source: source, pid: pid, keyCode: modifier.keyCode, keyDown: false, flags: accumulated)
}
}
private static func postKey(source: CGEventSource, pid: pid_t, keyCode: CGKeyCode, keyDown: Bool, flags: CGEventFlags) throws {
guard let event = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: keyDown) else {
throw ComputerUseError.eventCreationFailed
}
event.flags = flags
event.setIntegerValueField(.eventTargetUnixProcessID, value: Int64(pid))
event.postToPid(pid)
}
static func address(_ event: CGEvent, pid: pid_t, windowNumber: Int) {
event.setIntegerValueField(.eventTargetUnixProcessID, value: Int64(pid))
event.setIntegerValueField(.mouseEventWindowUnderMousePointer, value: Int64(windowNumber))
event.setIntegerValueField(.mouseEventWindowUnderMousePointerThatCanHandleThisEvent, value: Int64(windowNumber))
if let privateWindowField {
event.setIntegerValueField(privateWindowField, value: Int64(windowNumber))
}
if let privateRouteField {
event.setIntegerValueField(privateRouteField, value: 1)
}
}
static func parseCombo(_ combo: String) throws -> (flags: CGEventFlags, keyCode: CGKeyCode) {
let parts = combo.lowercased().split(separator: "+").map(String.init)
var flags: CGEventFlags = []
var keyName = ""
for part in parts {
switch part {
case "command", "cmd", "meta": flags.insert(.maskCommand)
case "shift": flags.insert(.maskShift)
case "control", "ctrl": flags.insert(.maskControl)
case "option", "alt": flags.insert(.maskAlternate)
default: keyName = part
}
}
guard let keyCode = keyCodes[keyName] else {
throw ComputerUseError.unknownKey(keyName)
}
return (flags, keyCode)
}
private static let keyCodes: [String: CGKeyCode] = [
"return": 0x24, "enter": 0x24, "tab": 0x30, "space": 0x31,
"delete": 0x33, "backspace": 0x33, "escape": 0x35, "esc": 0x35,
"up": 0x7E, "down": 0x7D, "left": 0x7B, "right": 0x7C,
"home": 0x73, "end": 0x77, "pageup": 0x74, "pagedown": 0x79,
"a": 0x00, "b": 0x0B, "c": 0x08, "d": 0x02, "e": 0x0E,
"f": 0x03, "g": 0x05, "h": 0x04, "i": 0x22, "j": 0x26,
"k": 0x28, "l": 0x25, "m": 0x2E, "n": 0x2D, "o": 0x1F,
"p": 0x23, "q": 0x0C, "r": 0x0F, "s": 0x01, "t": 0x11,
"u": 0x20, "v": 0x09, "w": 0x0D, "x": 0x07, "y": 0x10, "z": 0x06,
"0": 0x1D, "1": 0x12, "2": 0x13, "3": 0x14, "4": 0x15,
"5": 0x17, "6": 0x16, "7": 0x1A, "8": 0x1C, "9": 0x19,
"f1": 0x7A, "f2": 0x78, "f3": 0x63, "f4": 0x76,
"f5": 0x60, "f6": 0x61, "f7": 0x62, "f8": 0x64,
"f9": 0x65, "f10": 0x6D, "f11": 0x67, "f12": 0x6F,
"-": 0x1B, "=": 0x18, "[": 0x21, "]": 0x1E,
"\\": 0x2A, ";": 0x29, "'": 0x27, ",": 0x2B,
".": 0x2F, "/": 0x2C, "`": 0x32,
]
}
@@ -0,0 +1,383 @@
import AppKit
import Foundation
actor ComputerUseRuntime {
private let accessibility = AccessibilityService()
private let foregroundInput = InputService()
private var lastSnapshot: AppSnapshot?
private var strictMode = true
private var activationSession: BackgroundActivationSession?
private var activationKey: String?
private var activatedWindowKey: String?
private var activationPreviousPID: pid_t?
private var activationTargetPID: pid_t?
private var frontmostMonitor: FrontmostApplicationMonitor?
private var snapshotSequence = 0
private var previousLabelsByWindow: [String: Set<String>] = [:]
private var recentActions: [String] = []
private var staleSnapshotReason: String?
func setStrictMode(_ enabled: Bool) -> ActionMetadata {
strictMode = enabled
if !enabled {
resetBackgroundActivation()
}
return ActionMetadata(
ok: true,
path: .none,
strictMode: enabled,
backgroundSafe: enabled,
fallbackUsed: false,
message: enabled ? "Strict background mode enabled." : "Strict background mode disabled. Foreground fallback is allowed."
)
}
func snapshot(appName: String?, pid: Int? = nil, windowTitle: String? = nil, strict requestedStrict: Bool?) async throws -> AppSnapshot {
let effectiveStrict = requestedStrict ?? strictMode
if !effectiveStrict {
resetBackgroundActivation()
}
let requestedPID = pid.map(pid_t.init)
var target = try accessibility.resolveTarget(appName: appName, pid: requestedPID, windowTitle: windowTitle)
let backgroundActivated: Bool
if effectiveStrict, !target.isFrontmost {
backgroundActivated = try await ensureBackgroundActivation(target: target)
target = try accessibility.resolveTarget(appName: appName, pid: requestedPID, windowTitle: windowTitle)
} else {
backgroundActivated = false
}
let snapshot = try await accessibility.snapshot(
target: target,
strictMode: effectiveStrict,
backgroundActivated: backgroundActivated
)
let annotatedSnapshot = annotate(snapshot: snapshot)
staleSnapshotReason = nil
lastSnapshot = annotatedSnapshot
return annotatedSnapshot
}
func click(snapshotID: String?, ref: String?, index: Int?, imageX: Double?, imageY: Double?, screenX: Double? = nil, screenY: Double? = nil, clickCount: Int, strict requestedStrict: Bool?) async throws -> ActionMetadata {
let snapshot = try requireSnapshot(snapshotID: snapshotID)
let effectiveStrict = requestedStrict ?? snapshot.strictMode
try validateSnapshotForAction(snapshot: snapshot, strict: effectiveStrict)
if let record = findRecord(ref: ref, index: index, in: snapshot) {
let fresh = freshRecord(matching: record, in: snapshot)
if let fresh {
AgentCursorOverlay.shared.show(at: fresh.semantic.frame.center)
if fresh.semantic.capabilities.canPress, accessibility.press(record: fresh) {
return recordAction(ActionMetadata(ok: true, path: .accessibility, strictMode: effectiveStrict, backgroundSafe: true, fallbackUsed: false, message: "Pressed \(fresh.semantic.ref) via AXPress."))
}
if fresh.semantic.capabilities.canFocus, accessibility.focus(record: fresh) {
return recordAction(ActionMetadata(ok: true, path: .accessibility, strictMode: effectiveStrict, backgroundSafe: true, fallbackUsed: false, message: "Focused \(fresh.semantic.ref) via AX."))
}
return try await clickPoint(fresh.semantic.frame.center, clickCount: clickCount, strict: effectiveStrict, fallbackUsed: true)
}
return try await clickPoint(record.semantic.frame.center, clickCount: clickCount, strict: effectiveStrict, fallbackUsed: true)
}
if let screenX, let screenY {
return try await clickPoint(CGPoint(x: screenX, y: screenY), clickCount: clickCount, strict: effectiveStrict, fallbackUsed: false)
}
if let imageX, let imageY {
let point = snapshot.screenshotMeta.toScreen(imageX: imageX, imageY: imageY)
return try await clickPoint(point, clickCount: clickCount, strict: effectiveStrict, fallbackUsed: false)
}
throw ComputerUseError.invalidElement(ref ?? index.map(String.init) ?? "<missing>")
}
func typeText(snapshotID: String?, text: String, strict requestedStrict: Bool?) throws -> ActionMetadata {
let snapshot = try requireSnapshot(snapshotID: snapshotID)
let effectiveStrict = requestedStrict ?? snapshot.strictMode
try validateSnapshotForAction(snapshot: snapshot, strict: effectiveStrict)
if effectiveStrict {
try BackgroundInputDispatcher.typeText(pid: snapshot.pid, text: text)
return recordAction(ActionMetadata(ok: true, path: .backgroundCGEvent, strictMode: true, backgroundSafe: true, fallbackUsed: false, message: "Typed text with postToPid."))
}
try foregroundInput.typeText(text)
return recordAction(ActionMetadata(ok: true, path: .foregroundCGEvent, strictMode: false, backgroundSafe: false, fallbackUsed: true, message: "Typed text with foreground HID fallback."))
}
func pressKey(snapshotID: String?, combo: String, strict requestedStrict: Bool?) throws -> ActionMetadata {
let snapshot = try requireSnapshot(snapshotID: snapshotID)
let effectiveStrict = requestedStrict ?? snapshot.strictMode
try validateSnapshotForAction(snapshot: snapshot, strict: effectiveStrict)
if effectiveStrict {
try BackgroundInputDispatcher.pressKey(pid: snapshot.pid, combo: combo)
return recordAction(ActionMetadata(ok: true, path: .backgroundCGEvent, strictMode: true, backgroundSafe: true, fallbackUsed: false, message: "Pressed key with postToPid."))
}
try foregroundInput.pressKey(combo)
return recordAction(ActionMetadata(ok: true, path: .foregroundCGEvent, strictMode: false, backgroundSafe: false, fallbackUsed: true, message: "Pressed key with foreground HID fallback."))
}
func scroll(snapshotID: String?, direction: String?, pages: Double, imageX: Double?, imageY: Double?, strict requestedStrict: Bool?) throws -> ActionMetadata {
let snapshot = try requireSnapshot(snapshotID: snapshotID)
let effectiveStrict = requestedStrict ?? snapshot.strictMode
try validateSnapshotForAction(snapshot: snapshot, strict: effectiveStrict)
let amount = max(1, Int32(pages * 5))
let deltas = scrollDeltas(direction: direction, amount: amount)
let point: CGPoint = {
if let imageX, let imageY {
return snapshot.screenshotMeta.toScreen(imageX: imageX, imageY: imageY)
}
return CGPoint(x: snapshot.screenshotMeta.capturedBounds.midX, y: snapshot.screenshotMeta.capturedBounds.midY)
}()
AgentCursorOverlay.shared.show(at: point)
if effectiveStrict {
guard let windowNumber = snapshot.windowNumber else {
throw ComputerUseError.strictModeViolation("background scroll requires a CG window number")
}
try BackgroundInputDispatcher.scroll(pid: snapshot.pid, windowNumber: windowNumber, point: point, deltaX: deltas.x, deltaY: deltas.y)
return recordAction(ActionMetadata(ok: true, path: .backgroundCGEvent, strictMode: true, backgroundSafe: true, fallbackUsed: false, message: "Scrolled with postToPid."))
}
try foregroundInput.scroll(point: point, deltaX: deltas.x, deltaY: deltas.y)
return recordAction(ActionMetadata(ok: true, path: .foregroundCGEvent, strictMode: false, backgroundSafe: false, fallbackUsed: true, message: "Scrolled with foreground HID fallback."))
}
func setValue(snapshotID: String?, ref: String?, index: Int?, value: String) throws -> ActionMetadata {
let snapshot = try requireSnapshot(snapshotID: snapshotID)
try validateSnapshotForAction(snapshot: snapshot, strict: snapshot.strictMode)
guard let record = findRecord(ref: ref, index: index, in: snapshot) else {
throw ComputerUseError.invalidElement(ref ?? index.map(String.init) ?? "<missing>")
}
guard let fresh = freshRecord(matching: record, in: snapshot) else {
throw ComputerUseError.staleSnapshot("The target element changed. Take a new snapshot before setting its value.")
}
AgentCursorOverlay.shared.show(at: fresh.semantic.frame.center)
let axWriteAccepted = accessibility.setValue(record: fresh, value: value)
if axWriteAccepted, valueMatches(record: fresh, expected: value) {
return recordAction(ActionMetadata(ok: true, path: .accessibility, strictMode: snapshot.strictMode, backgroundSafe: true, fallbackUsed: false, message: "Set \(fresh.semantic.ref) via AXValue."))
}
// Web inputs (Chromium/Electron) accept AXValue writes without applying
// them. Fall back to focus + select-all + retype, then verify.
_ = accessibility.focus(record: fresh)
try BackgroundInputDispatcher.pressKey(pid: snapshot.pid, combo: "command+a")
try BackgroundInputDispatcher.typeText(pid: snapshot.pid, text: value)
Thread.sleep(forTimeInterval: 0.08)
let verified = valueMatches(record: fresh, expected: value)
return recordAction(ActionMetadata(
ok: verified,
path: .backgroundCGEvent,
strictMode: snapshot.strictMode,
backgroundSafe: true,
fallbackUsed: true,
message: verified
? "AXValue write did not stick; set \(fresh.semantic.ref) via select-all + retype fallback."
: "Could not verify the value of \(fresh.semantic.ref) after AXValue and retype fallback."
))
}
private func valueMatches(record: AXElementRecord, expected: String) -> Bool {
accessibility.value(record: record) == expected
}
func performAction(snapshotID: String?, ref: String?, index: Int?, action: String) throws -> ActionMetadata {
let snapshot = try requireSnapshot(snapshotID: snapshotID)
try validateSnapshotForAction(snapshot: snapshot, strict: snapshot.strictMode)
guard let record = findRecord(ref: ref, index: index, in: snapshot) else {
throw ComputerUseError.invalidElement(ref ?? index.map(String.init) ?? "<missing>")
}
guard let fresh = freshRecord(matching: record, in: snapshot) else {
throw ComputerUseError.staleSnapshot("The target element changed. Take a new snapshot before performing another action.")
}
AgentCursorOverlay.shared.show(at: fresh.semantic.frame.center)
let ok = accessibility.performAction(record: fresh, action: action)
return recordAction(ActionMetadata(ok: ok, path: .accessibility, strictMode: snapshot.strictMode, backgroundSafe: true, fallbackUsed: false, message: ok ? "Performed \(action) on \(fresh.semantic.ref)." : "AX action \(action) failed."))
}
func wait(milliseconds: Int) async -> ActionMetadata {
let clamped = max(0, min(milliseconds, 10_000))
try? await Task.sleep(nanoseconds: UInt64(clamped) * 1_000_000)
return ActionMetadata(ok: true, path: .none, strictMode: strictMode, backgroundSafe: true, fallbackUsed: false, message: "Waited \(clamped)ms.")
}
private func clickPoint(_ point: CGPoint, clickCount: Int, strict: Bool, fallbackUsed: Bool) async throws -> ActionMetadata {
let snapshot = try requireSnapshot()
try validateSnapshotForAction(snapshot: snapshot, strict: strict)
AgentCursorOverlay.shared.show(at: point)
if strict {
guard let windowNumber = snapshot.windowNumber else {
throw ComputerUseError.strictModeViolation("background click requires a CG window number")
}
try await BackgroundInputDispatcher.click(pid: snapshot.pid, windowNumber: windowNumber, point: point, doubleClick: clickCount >= 2)
return recordAction(ActionMetadata(ok: true, path: .backgroundCGEvent, strictMode: true, backgroundSafe: true, fallbackUsed: fallbackUsed, message: "Clicked with postToPid at \(Int(point.x)),\(Int(point.y))."))
}
try await foregroundInput.click(point: point, doubleClick: clickCount >= 2)
return recordAction(ActionMetadata(ok: true, path: .foregroundCGEvent, strictMode: false, backgroundSafe: false, fallbackUsed: true, message: "Clicked with foreground HID fallback at \(Int(point.x)),\(Int(point.y))."))
}
private func ensureBackgroundActivation(target: WindowTarget) async throws -> Bool {
ensureFrontmostMonitor()
guard let previousPID = NSWorkspace.shared.frontmostApplication?.processIdentifier else {
throw ComputerUseError.noFrontmostApplication
}
let nextActivationKey = "\(previousPID):\(target.pid)"
if activationKey != nextActivationKey {
resetBackgroundActivation()
let next = BackgroundActivationSession(previousPID: previousPID, targetPID: target.pid)
try next.start()
activationSession = next
activationKey = nextActivationKey
activationPreviousPID = previousPID
activationTargetPID = target.pid
activatedWindowKey = nil
}
let nextWindowKey = "\(target.pid):\(target.windowNumber ?? -1)"
if activatedWindowKey != nextWindowKey {
guard let activationSession else {
throw ComputerUseError.strictModeViolation("background activation session was not created")
}
try await activationSession.activate(target: target)
activatedWindowKey = nextWindowKey
}
return true
}
private func ensureFrontmostMonitor() {
if frontmostMonitor != nil { return }
frontmostMonitor = FrontmostApplicationMonitor { [weak self] pid in
guard let runtime = self else { return }
Task { await runtime.frontmostApplicationChanged(pid: pid) }
}
}
private func frontmostApplicationChanged(pid: pid_t?) {
guard let pid, activationSession != nil else { return }
if pid == activationPreviousPID {
staleSnapshotReason = "The user returned focus to the previous app. Take a new snapshot before sending more input."
} else if pid == activationTargetPID {
staleSnapshotReason = "The target app became frontmost. Take a new snapshot before continuing."
} else {
staleSnapshotReason = "The user changed app focus. Take a new snapshot before sending more input."
}
resetBackgroundActivation()
}
private func resetBackgroundActivation() {
activationSession?.stop()
activationSession = nil
activationKey = nil
activatedWindowKey = nil
activationPreviousPID = nil
activationTargetPID = nil
}
private func requireSnapshot(snapshotID: String? = nil) throws -> AppSnapshot {
guard let lastSnapshot else { throw ComputerUseError.noSnapshot }
if let snapshotID, snapshotID != lastSnapshot.id {
throw ComputerUseError.staleSnapshot("Snapshot \(snapshotID) is no longer current. Take a new snapshot before retrying.")
}
return lastSnapshot
}
private func validateSnapshotForAction(snapshot: AppSnapshot, strict: Bool) throws {
if let staleSnapshotReason {
throw ComputerUseError.staleSnapshot(staleSnapshotReason)
}
guard strict else { return }
if NSWorkspace.shared.frontmostApplication?.processIdentifier == snapshot.pid { return }
guard activationSession != nil, activationTargetPID == snapshot.pid else {
throw ComputerUseError.staleSnapshot("The target app is no longer safely activated. Take a new snapshot before retrying.")
}
}
private func annotate(snapshot: AppSnapshot) -> AppSnapshot {
snapshotSequence += 1
let key = windowKey(snapshot: snapshot)
let labels = Set(snapshot.records.map { stableLabel(for: $0.semantic) }.filter { !$0.isEmpty })
let previous = previousLabelsByWindow[key] ?? []
previousLabelsByWindow[key] = labels
let added = labels.subtracting(previous).sorted()
let removed = previous.subtracting(labels).sorted()
return snapshot.withSessionMetadata(
id: UUID().uuidString.lowercased(),
observation: snapshotSequence,
recentActions: Array(recentActions.suffix(8)),
addedLabels: Array(added.prefix(16)),
removedLabels: Array(removed.prefix(16))
)
}
private func recordAction(_ metadata: ActionMetadata) -> ActionMetadata {
recentActions.append(metadata.message)
if recentActions.count > 12 {
recentActions.removeFirst(recentActions.count - 12)
}
return metadata
}
private func freshRecord(matching record: AXElementRecord, in snapshot: AppSnapshot) -> AXElementRecord? {
guard let target = try? accessibility.resolveTarget(appName: snapshot.appName, pid: snapshot.pid, windowTitle: snapshot.windowTitle) else {
return nil
}
let records = accessibility.records(target: target)
if let index = records.firstIndex(where: { $0.semantic.id == record.semantic.id }), stableMatch(records[index].semantic, record.semantic) {
return records[index]
}
return records.first { stableMatch($0.semantic, record.semantic) }
}
private func stableMatch(_ candidate: SemanticAXElement, _ target: SemanticAXElement) -> Bool {
candidate.role == target.role && stableLabel(for: candidate) == stableLabel(for: target)
}
private func stableLabel(for element: SemanticAXElement) -> String {
let value = element.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
let display = value.isEmpty ? label : "\(label) \(value)"
return "\(element.role):\(display)"
.components(separatedBy: .whitespacesAndNewlines)
.filter { !$0.isEmpty }
.joined(separator: " ")
.lowercased()
}
private func windowKey(snapshot: AppSnapshot) -> String {
"\(snapshot.pid):\(snapshot.windowNumber ?? -1)"
}
private func findRecord(ref: String?, index: Int?, in snapshot: AppSnapshot) -> AXElementRecord? {
if let ref {
let normalized = normalizeRef(ref)
return snapshot.records.first { $0.semantic.ref == normalized }
}
if let index {
if let byID = snapshot.records.first(where: { $0.semantic.id == index }) {
return byID
}
if index >= 0 && index < snapshot.records.count {
return snapshot.records[index]
}
}
return nil
}
private func normalizeRef(_ raw: String) -> String {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("{e"), trimmed.hasSuffix("}") { return trimmed }
if trimmed.hasPrefix("e") { return "{\(trimmed)}" }
return "{e\(trimmed)}"
}
private func scrollDeltas(direction: String?, amount: Int32) -> (x: Int32, y: Int32) {
switch direction?.lowercased() ?? "down" {
case "up": return (0, amount)
case "left": return (amount, 0)
case "right": return (-amount, 0)
default: return (0, -amount)
}
}
}
@@ -0,0 +1,28 @@
import AppKit
import Foundation
final class FrontmostApplicationMonitor: @unchecked Sendable {
private let queue = DispatchQueue(label: "openwork.computer-use.frontmost-monitor")
private var timer: DispatchSourceTimer?
private var lastPID: pid_t?
init(onChange: @escaping @Sendable (pid_t?) -> Void) {
lastPID = NSWorkspace.shared.frontmostApplication?.processIdentifier
let source = DispatchSource.makeTimerSource(queue: queue)
source.schedule(deadline: .now() + .milliseconds(250), repeating: .milliseconds(250))
source.setEventHandler { [weak self] in
guard let self else { return }
let pid = NSWorkspace.shared.frontmostApplication?.processIdentifier
if pid != self.lastPID {
self.lastPID = pid
onChange(pid)
}
}
timer = source
source.resume()
}
deinit {
timer?.cancel()
}
}
@@ -0,0 +1,102 @@
import Foundation
import CoreGraphics
final class InputService: @unchecked Sendable {
func moveMouse(point: CGPoint) throws {
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
guard let event = CGEvent(mouseEventSource: source, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left) else {
throw ComputerUseError.eventCreationFailed
}
event.post(tap: .cghidEventTap)
}
func click(point: CGPoint, doubleClick: Bool = false) async throws {
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
if let move = CGEvent(mouseEventSource: source, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left) {
move.post(tap: .cghidEventTap)
}
try await Task.sleep(nanoseconds: 50_000_000)
let count = doubleClick ? 2 : 1
for clickState in 1...count {
guard let down = CGEvent(mouseEventSource: source, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left),
let up = CGEvent(mouseEventSource: source, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left) else {
throw ComputerUseError.eventCreationFailed
}
down.setIntegerValueField(.mouseEventClickState, value: Int64(clickState))
up.setIntegerValueField(.mouseEventClickState, value: Int64(clickState))
down.post(tap: .cghidEventTap)
up.post(tap: .cghidEventTap)
}
}
func typeText(_ text: String) throws {
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
let units = Array(text.utf16)
for start in stride(from: 0, to: units.count, by: 20) {
let end = min(start + 20, units.count)
let chunk = Array(units[start..<end])
guard let event = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: true) else {
throw ComputerUseError.eventCreationFailed
}
event.keyboardSetUnicodeString(stringLength: chunk.count, unicodeString: chunk)
event.post(tap: .cghidEventTap)
Thread.sleep(forTimeInterval: 0.01)
}
}
func pressKey(_ combo: String) throws {
let parsed = try BackgroundInputDispatcher.parseCombo(combo)
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
guard let down = CGEvent(keyboardEventSource: source, virtualKey: parsed.keyCode, keyDown: true),
let up = CGEvent(keyboardEventSource: source, virtualKey: parsed.keyCode, keyDown: false) else {
throw ComputerUseError.eventCreationFailed
}
down.flags = parsed.flags
up.flags = parsed.flags
down.post(tap: .cghidEventTap)
up.post(tap: .cghidEventTap)
}
func scroll(point: CGPoint?, deltaX: Int32, deltaY: Int32) throws {
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
guard let event = CGEvent(scrollWheelEvent2Source: source, units: .line, wheelCount: 2, wheel1: deltaY, wheel2: deltaX, wheel3: 0) else {
throw ComputerUseError.eventCreationFailed
}
if let point { event.location = point }
event.post(tap: .cgSessionEventTap)
}
func drag(path: [CGPoint]) async throws {
guard path.count >= 2 else { return }
guard let source = CGEventSource(stateID: .combinedSessionState) else {
throw ComputerUseError.eventSourceFailed
}
guard let down = CGEvent(mouseEventSource: source, mouseType: .leftMouseDown, mouseCursorPosition: path[0], mouseButton: .left) else {
throw ComputerUseError.eventCreationFailed
}
down.post(tap: .cghidEventTap)
for point in path.dropFirst().dropLast() {
if let drag = CGEvent(mouseEventSource: source, mouseType: .leftMouseDragged, mouseCursorPosition: point, mouseButton: .left) {
drag.post(tap: .cghidEventTap)
}
try await Task.sleep(nanoseconds: 12_000_000)
}
guard let last = path.last,
let up = CGEvent(mouseEventSource: source, mouseType: .leftMouseUp, mouseCursorPosition: last, mouseButton: .left) else {
throw ComputerUseError.eventCreationFailed
}
up.post(tap: .cghidEventTap)
}
}
@@ -0,0 +1,623 @@
import AppKit
import ApplicationServices
import Foundation
import ScreenCaptureKit
actor MCPServer {
private let runtime = ComputerUseRuntime()
private let input = InputService()
private var cuaSnapshotFrontmostPID: pid_t?
func run() async {
log("Computer Use server starting")
while let line = readLine(strippingNewline: true) {
guard !line.isEmpty else { continue }
guard let data = line.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
log("Invalid JSON-RPC line")
continue
}
let id = json["id"]
let method = json["method"] as? String ?? ""
let params = json["params"] as? [String: Any] ?? [:]
switch method {
case "initialize":
respond(id: id, result: [
"protocolVersion": "2025-03-26",
"capabilities": ["tools": [:]],
"serverInfo": ["name": "openwork-computer-use", "version": "0.1.0"],
])
case "notifications/initialized":
break
case "tools/list":
respond(id: id, result: ["tools": toolSchemas()])
case "tools/call":
let name = params["name"] as? String ?? ""
let args = params["arguments"] as? [String: Any] ?? [:]
let content = await executeTool(name: name, args: args)
respond(id: id, result: ["content": content])
default:
if id != nil {
respondError(id: id, code: -32601, message: "Method not found: \(method)")
}
}
}
}
private func toolSchemas() -> [[String: Any]] {
[
toolSchema(
name: "snapshot",
description: "Return target-window screenshot plus compact semantic AX state. Uses strict background activation by default.",
properties: [
"app": ["type": "string", "description": "Optional running app name. Omit for frontmost app."],
"pid": ["type": "number", "description": "Optional process id. Disambiguates multiple instances of the same app."],
"window_title": ["type": "string", "description": "Optional window title to target a specific window."],
"strict": ["type": "boolean", "description": "Keep actions on background-safe AX/postToPid paths. Default true."],
]
),
toolSchema(
name: "click",
description: "Click a semantic ref like {e1}, an index, or screenshot x/y. AX is tried first; strict mode only falls back to background postToPid.",
properties: [
"ref": ["type": "string", "description": "Semantic ref from snapshot, e.g. {e1}."],
"snapshot_id": ["type": "string", "description": "Optional snapshot id from the latest snapshot response."],
"index": ["type": "number", "description": "Element id or zero-based compatibility index."],
"x": ["type": "number", "description": "Screenshot-image x coordinate (pixels in the snapshot image, e.g. element center.imageX). Not screen pixels."],
"y": ["type": "number", "description": "Screenshot-image y coordinate (pixels in the snapshot image, e.g. element center.imageY). Not screen pixels."],
"screen_x": ["type": "number", "description": "Absolute screen x coordinate (e.g. element center.screenX). Takes precedence over x/y."],
"screen_y": ["type": "number", "description": "Absolute screen y coordinate (e.g. element center.screenY). Takes precedence over x/y."],
"click_count": ["type": "number", "description": "1 or 2. Default 1."],
"strict": ["type": "boolean", "description": "Override strict mode for this action."],
]
),
toolSchema(
name: "type_text",
description: "Type text into the target process. In strict mode this uses CGEvent.postToPid and does not move the real cursor.",
properties: [
"text": ["type": "string", "description": "Text to type."],
"snapshot_id": ["type": "string", "description": "Optional snapshot id from the latest snapshot response."],
"strict": ["type": "boolean", "description": "Override strict mode for this action."],
]
),
toolSchema(
name: "press_key",
description: "Press a key combo such as command+k, return, tab, or escape.",
properties: [
"combo": ["type": "string", "description": "Key combo."],
"snapshot_id": ["type": "string", "description": "Optional snapshot id from the latest snapshot response."],
"strict": ["type": "boolean", "description": "Override strict mode for this action."],
]
),
toolSchema(
name: "scroll",
description: "Scroll the target window without foregrounding it in strict mode.",
properties: [
"direction": ["type": "string", "description": "up, down, left, or right."],
"snapshot_id": ["type": "string", "description": "Optional snapshot id from the latest snapshot response."],
"pages": ["type": "number", "description": "Approximate page count. Default 1."],
"x": ["type": "number", "description": "Optional screenshot x coordinate."],
"y": ["type": "number", "description": "Optional screenshot y coordinate."],
"strict": ["type": "boolean", "description": "Override strict mode for this action."],
]
),
toolSchema(
name: "set_value",
description: "Set a semantic AX element value directly. This stays background-safe.",
properties: [
"ref": ["type": "string", "description": "Semantic ref from snapshot."],
"snapshot_id": ["type": "string", "description": "Optional snapshot id from the latest snapshot response."],
"index": ["type": "number", "description": "Element id or zero-based compatibility index."],
"value": ["type": "string", "description": "Value to set."],
]
),
toolSchema(
name: "perform_action",
description: "Perform a named AX action such as AXPress, AXShowMenu, AXIncrement, or AXDecrement.",
properties: [
"ref": ["type": "string", "description": "Semantic ref from snapshot."],
"snapshot_id": ["type": "string", "description": "Optional snapshot id from the latest snapshot response."],
"index": ["type": "number", "description": "Element id or zero-based compatibility index."],
"action": ["type": "string", "description": "AX action name. Default AXPress."],
]
),
toolSchema(
name: "wait",
description: "Wait for UI to settle.",
properties: ["milliseconds": ["type": "number", "description": "Wait time. Default 1000."]]
),
toolSchema(
name: "set_strict_mode",
description: "Enable or disable strict background mode. Strict mode rejects foreground fallbacks.",
properties: ["enabled": ["type": "boolean", "description": "Whether strict background mode is enabled."]]
),
toolSchema(name: "check_permissions", description: "Check Accessibility and Screen Recording permission status.", properties: [:]),
toolSchema(name: "get_app_state", description: "Compatibility alias for snapshot.", properties: ["app": ["type": "string"], "strict": ["type": "boolean"]]),
toolSchema(name: "launch_app", description: "Launch a macOS app by name.", properties: ["name": ["type": "string"]]),
toolSchema(name: "activate_app", description: "Bring a running macOS app to the foreground.", properties: ["name": ["type": "string"]]),
toolSchema(name: "list_apps", description: "List running regular macOS apps.", properties: [:]),
toolSchema(name: "open_url", description: "Open a URL in the default browser or a specific browser app.", properties: ["url": ["type": "string"], "app": ["type": "string"]]),
toolSchema(name: "clipboard_read", description: "Read text from the macOS clipboard.", properties: [:]),
toolSchema(name: "clipboard_write", description: "Write text to the macOS clipboard.", properties: ["text": ["type": "string"]]),
toolSchema(name: "display_info", description: "Return main display logical dimensions and scale factor.", properties: [:]),
toolSchema(name: "cua_screenshot", description: "Compatibility full-screen screenshot for CUA loops. Returns logical-size PNG.", properties: [:]),
toolSchema(name: "cua_click", description: "Compatibility click at absolute screen coordinates.", properties: ["x": ["type": "number"], "y": ["type": "number"]]),
toolSchema(name: "cua_double_click", description: "Compatibility double-click at absolute screen coordinates.", properties: ["x": ["type": "number"], "y": ["type": "number"]]),
toolSchema(name: "cua_move", description: "Compatibility mouse move to absolute screen coordinates.", properties: ["x": ["type": "number"], "y": ["type": "number"]]),
toolSchema(name: "cua_type", description: "Compatibility text typing into focused input.", properties: ["text": ["type": "string"]]),
toolSchema(name: "cua_keypress", description: "Compatibility keypress using CUA key names.", properties: ["keys": ["type": "array", "items": ["type": "string"]]]),
toolSchema(name: "cua_scroll", description: "Compatibility scroll at absolute screen coordinates.", properties: ["x": ["type": "number"], "y": ["type": "number"], "scroll_x": ["type": "number"], "scroll_y": ["type": "number"]]),
toolSchema(name: "cua_drag", description: "Compatibility drag over an array of [x,y] points.", properties: ["path": ["type": "array"]]),
toolSchema(name: "cua_wait", description: "Compatibility wait for UI to settle.", properties: [:]),
]
}
private func toolSchema(name: String, description: String, properties: [String: Any]) -> [String: Any] {
["name": name, "description": description, "inputSchema": ["type": "object", "properties": properties]]
}
private func executeTool(name: String, args: [String: Any]) async -> [[String: Any]] {
do {
switch name {
case "snapshot", "get_app_state":
return try await snapshotResult(args: args)
case "click":
let metadata = try await runtime.click(
snapshotID: snapshotIDArg(args),
ref: args["ref"] as? String,
index: intArg(args, "index"),
imageX: doubleArg(args, "x"),
imageY: doubleArg(args, "y"),
screenX: doubleArg(args, "screen_x"),
screenY: doubleArg(args, "screen_y"),
clickCount: intArg(args, "click_count") ?? 1,
strict: boolArg(args, "strict")
)
return jsonResult(metadata.dictionary)
case "type_text":
let metadata = try await runtime.typeText(snapshotID: snapshotIDArg(args), text: args["text"] as? String ?? "", strict: boolArg(args, "strict"))
return jsonResult(metadata.dictionary)
case "press_key":
let metadata = try await runtime.pressKey(snapshotID: snapshotIDArg(args), combo: args["combo"] as? String ?? "", strict: boolArg(args, "strict"))
return jsonResult(metadata.dictionary)
case "scroll":
let metadata = try await runtime.scroll(
snapshotID: snapshotIDArg(args),
direction: args["direction"] as? String,
pages: doubleArg(args, "pages") ?? 1,
imageX: doubleArg(args, "x"),
imageY: doubleArg(args, "y"),
strict: boolArg(args, "strict")
)
return jsonResult(metadata.dictionary)
case "set_value":
let metadata = try await runtime.setValue(snapshotID: snapshotIDArg(args), ref: args["ref"] as? String, index: intArg(args, "index"), value: args["value"] as? String ?? "")
return jsonResult(metadata.dictionary)
case "perform_action":
let metadata = try await runtime.performAction(snapshotID: snapshotIDArg(args), ref: args["ref"] as? String, index: intArg(args, "index"), action: args["action"] as? String ?? kAXPressAction)
return jsonResult(metadata.dictionary)
case "wait":
let metadata = await runtime.wait(milliseconds: intArg(args, "milliseconds") ?? 1000)
return jsonResult(metadata.dictionary)
case "set_strict_mode":
let metadata = await runtime.setStrictMode(boolArg(args, "enabled") ?? true)
return jsonResult(metadata.dictionary)
case "check_permissions":
return jsonResult(checkPermissions())
case "launch_app":
return try await jsonResult(handleLaunchApp(args: args))
case "activate_app":
return jsonResult(handleActivateApp(args: args))
case "list_apps":
let details = runningApps()
.compactMap { app -> [String: Any]? in
guard let name = app.localizedName else { return nil }
return ["name": name, "pid": Int(app.processIdentifier)]
}
return jsonResult([
"ok": true,
"apps": runningApps().compactMap(\.localizedName).sorted(),
"appDetails": details,
])
case "open_url":
return try await jsonResult(handleOpenURL(args: args))
case "clipboard_read":
return jsonResult(["ok": true, "text": NSPasteboard.general.string(forType: .string) ?? ""])
case "clipboard_write":
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(args["text"] as? String ?? "", forType: .string)
return jsonResult(["ok": true])
case "display_info":
return jsonResult(displayInfo())
case "cua_screenshot":
return try await cuaScreenshotResult()
case "cua_click":
try validateCuaSnapshotFresh()
AgentCursorOverlay.shared.show(at: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0))
try await input.click(point: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0))
return jsonResult(["ok": true])
case "cua_double_click":
try validateCuaSnapshotFresh()
AgentCursorOverlay.shared.show(at: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0))
try await input.click(point: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0), doubleClick: true)
return jsonResult(["ok": true])
case "cua_move":
try validateCuaSnapshotFresh()
AgentCursorOverlay.shared.show(at: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0))
try input.moveMouse(point: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0))
return jsonResult(["ok": true])
case "cua_type":
try validateCuaSnapshotFresh()
try input.typeText(args["text"] as? String ?? "")
return jsonResult(["ok": true])
case "cua_keypress":
try validateCuaSnapshotFresh()
try input.pressKey(cuaKeysToCombo(args["keys"] as? [String] ?? []))
return jsonResult(["ok": true])
case "cua_scroll":
try validateCuaSnapshotFresh()
AgentCursorOverlay.shared.show(at: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0))
try input.scroll(
point: CGPoint(x: intArg(args, "x") ?? 0, y: intArg(args, "y") ?? 0),
deltaX: Int32(intArg(args, "scroll_x") ?? 0),
deltaY: Int32(-(intArg(args, "scroll_y") ?? 0))
)
return jsonResult(["ok": true])
case "cua_drag":
try validateCuaSnapshotFresh()
let path = parsePointPath(args["path"])
if let first = path.first {
AgentCursorOverlay.shared.show(at: first)
}
try await input.drag(path: path)
return jsonResult(["ok": true])
case "cua_wait":
try await Task.sleep(nanoseconds: 1_000_000_000)
return jsonResult(["ok": true])
default:
return jsonResult(["ok": false, "error": "Unknown tool: \(name)"])
}
} catch {
return jsonResult(errorPayload(error))
}
}
private func snapshotResult(args: [String: Any]) async throws -> [[String: Any]] {
let snapshot = try await runtime.snapshot(
appName: args["app"] as? String,
pid: intArg(args, "pid"),
windowTitle: args["window_title"] as? String,
strict: boolArg(args, "strict")
)
let payload = snapshotPayload(snapshot)
guard let text = jsonString(payload) else {
return textResult("Failed to serialize semantic AX snapshot.")
}
return [
["type": "image", "data": snapshot.screenshotData.base64EncodedString(), "mimeType": snapshot.screenshotMimeType],
["type": "text", "text": text],
]
}
private func snapshotPayload(_ snapshot: AppSnapshot) -> [String: Any] {
let elements = snapshot.elements.map { element -> [String: Any] in
var dict = element.dictionary
let imagePoint = snapshot.screenshotMeta.toImage(point: element.frame.center)
dict["center"] = [
"screenX": Int(element.frame.center.x),
"screenY": Int(element.frame.center.y),
"imageX": Int(imagePoint.x),
"imageY": Int(imagePoint.y),
]
return dict
}
var result: [String: Any] = [
"ok": true,
"semanticAXVersion": 1,
"snapshotId": snapshot.id,
"snapshot_id": snapshot.id,
"observation": snapshot.observation,
"app": snapshot.appName,
"pid": Int(snapshot.pid),
"windowTitle": snapshot.windowTitle ?? "",
"screenshot": snapshot.screenshotMeta.dictionary,
"execution": [
"strictMode": snapshot.strictMode,
"backgroundActivated": snapshot.backgroundActivated,
"defaultPath": snapshot.strictMode ? "accessibility_then_background_cgevent" : "accessibility_then_foreground_fallback",
],
"elements": elements,
"hint": "Use refs like {e1}. Prefer AX-capable refs; strict mode rejects foreground fallback and reports path metadata after every action.",
]
if !snapshot.recentActions.isEmpty {
result["recentActions"] = snapshot.recentActions
}
if !snapshot.addedLabels.isEmpty || !snapshot.removedLabels.isEmpty {
result["stateDelta"] = ["added": snapshot.addedLabels, "removed": snapshot.removedLabels]
}
if let windowNumber = snapshot.windowNumber {
result["windowNumber"] = windowNumber
}
return result
}
private func checkPermissions() -> [String: Any] {
ComputerUsePermissions.status().dictionary
}
private func handleActivateApp(args: [String: Any]) -> [String: Any] {
let name = args["name"] as? String ?? ""
guard let app = runningApp(named: name) else {
return ["ok": false, "error": "App '\(name)' is not running."]
}
app.activate()
return ["ok": true, "app": app.localizedName ?? name]
}
private func handleLaunchApp(args: [String: Any]) async throws -> [String: Any] {
let name = (args["name"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else { return ["ok": false, "error": "App name is required."] }
if let app = runningApp(named: name) {
app.activate()
return ["ok": true, "app": app.localizedName ?? name, "alreadyRunning": true]
}
guard let appURL = applicationURL(named: name) else {
return ["ok": false, "error": "App '\(name)' was not found."]
}
let config = NSWorkspace.OpenConfiguration()
config.activates = true
let app = try await NSWorkspace.shared.openApplication(at: appURL, configuration: config)
return ["ok": true, "app": app.localizedName ?? name]
}
private func handleOpenURL(args: [String: Any]) async throws -> [String: Any] {
guard let rawURL = args["url"] as? String, let url = URL(string: rawURL) else {
return ["ok": false, "error": "Invalid URL."]
}
if let appName = args["app"] as? String, !appName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
guard let appURL = applicationURL(named: appName) else {
return ["ok": false, "error": "App '\(appName)' was not found."]
}
_ = try await NSWorkspace.shared.open([url], withApplicationAt: appURL, configuration: NSWorkspace.OpenConfiguration())
return ["ok": true]
}
return ["ok": NSWorkspace.shared.open(url)]
}
private func displayInfo() -> [String: Any] {
guard let screen = NSScreen.main else { return ["ok": false, "error": "No main screen."] }
return [
"ok": true,
"width": Int(screen.frame.width),
"height": Int(screen.frame.height),
"scale_factor": screen.backingScaleFactor,
]
}
private func cuaScreenshotResult() async throws -> [[String: Any]] {
guard let screen = NSScreen.main else { throw ComputerUseError.screenshotFailed }
cuaSnapshotFrontmostPID = NSWorkspace.shared.frontmostApplication?.processIdentifier
let cgImage = await screenCaptureKitDisplayImage() ?? CGWindowListCreateImage(CGRect.null, .optionOnScreenOnly, kCGNullWindowID, [.bestResolution])
guard let cgImage else {
throw ComputerUseError.screenshotFailed
}
let logicalWidth = Int(screen.frame.width)
let logicalHeight = Int(screen.frame.height)
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: logicalWidth,
pixelsHigh: logicalHeight,
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bytesPerRow: 0,
bitsPerPixel: 0
) else {
throw ComputerUseError.screenshotFailed
}
rep.size = NSSize(width: logicalWidth, height: logicalHeight)
guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
throw ComputerUseError.screenshotFailed
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
.draw(in: NSRect(x: 0, y: 0, width: logicalWidth, height: logicalHeight))
NSGraphicsContext.restoreGraphicsState()
guard let png = rep.representation(using: .png, properties: [:]) else {
throw ComputerUseError.screenshotFailed
}
return [
["type": "text", "text": jsonString(["ok": true, "width": logicalWidth, "height": logicalHeight]) ?? "{\"ok\":true}"],
["type": "image", "data": png.base64EncodedString(), "mimeType": "image/png"],
]
}
private func screenCaptureKitDisplayImage() async -> CGImage? {
do {
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
guard let display = content.displays.first else { return nil }
let configuration = SCStreamConfiguration()
configuration.width = display.width
configuration.height = display.height
configuration.showsCursor = true
let filter = SCContentFilter(display: display, excludingWindows: [])
return try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
} catch {
return nil
}
}
private func runningApps() -> [NSRunningApplication] {
NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
}
private func runningApp(named name: String) -> NSRunningApplication? {
let needle = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return runningApps().first { $0.localizedName?.lowercased() == needle }
?? runningApps().first { $0.localizedName?.lowercased().contains(needle) == true }
}
private func applicationURL(named name: String) -> URL? {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId(for: trimmed)) {
return url
}
if let path = NSWorkspace.shared.fullPath(forApplication: trimmed) {
return URL(fileURLWithPath: path)
}
let candidates = [
"/Applications/\(trimmed).app",
"/System/Applications/\(trimmed).app",
"/Applications/Utilities/\(trimmed).app",
NSString(string: "~/Applications/\(trimmed).app").expandingTildeInPath,
]
return candidates.map(URL.init(fileURLWithPath:)).first { FileManager.default.fileExists(atPath: $0.path) }
}
private func bundleId(for appName: String) -> String {
switch appName.lowercased() {
case "safari": return "com.apple.Safari"
case "google chrome", "chrome": return "com.google.Chrome"
case "arc": return "company.thebrowser.Browser"
case "microsoft edge", "edge": return "com.microsoft.edgemac"
case "brave", "brave browser": return "com.brave.Browser"
case "slack": return "com.tinyspeck.slackmacgap"
default: return ""
}
}
private func cuaKeysToCombo(_ keys: [String]) -> String {
keys.map { key in
switch key.lowercased() {
case "ctrl", "control": return "command"
case "meta", "super", "win", "cmd": return "command"
case "alt": return "option"
case "arrowup": return "up"
case "arrowdown": return "down"
case "arrowleft": return "left"
case "arrowright": return "right"
case "backspace": return "delete"
case " ": return "space"
default: return key.lowercased()
}
}.joined(separator: "+")
}
private func parsePointPath(_ raw: Any?) -> [CGPoint] {
guard let pairs = raw as? [[Any]] else { return [] }
return pairs.compactMap { pair in
guard pair.count >= 2 else { return nil }
let x = valueAsDouble(pair[0])
let y = valueAsDouble(pair[1])
guard let x, let y else { return nil }
return CGPoint(x: x, y: y)
}
}
private func validateCuaSnapshotFresh() throws {
guard let snapshotPID = cuaSnapshotFrontmostPID else {
throw ComputerUseError.staleSnapshot("CUA action requires a screenshot first.")
}
let currentPID = NSWorkspace.shared.frontmostApplication?.processIdentifier
guard currentPID == snapshotPID else {
throw ComputerUseError.staleSnapshot("The user changed focus since the last CUA screenshot. Capture a new screenshot before acting.")
}
}
private func valueAsDouble(_ value: Any) -> Double? {
if let value = value as? Double { return value }
if let value = value as? Int { return Double(value) }
if let value = value as? String { return Double(value) }
return nil
}
private func respond(id: Any?, result: Any) {
var response: [String: Any] = ["jsonrpc": "2.0", "result": result]
if let id { response["id"] = id }
writeLine(response)
}
private func respondError(id: Any?, code: Int, message: String) {
var response: [String: Any] = ["jsonrpc": "2.0", "error": ["code": code, "message": message]]
if let id { response["id"] = id }
writeLine(response)
}
private func writeLine(_ object: [String: Any]) {
guard let text = jsonString(object) else { return }
print(text)
}
private func textResult(_ text: String) -> [[String: Any]] {
[["type": "text", "text": text]]
}
private func jsonResult(_ payload: [String: Any]) -> [[String: Any]] {
textResult(jsonString(payload) ?? "{\"ok\":false,\"error\":\"Failed to serialize result.\"}")
}
private func jsonString(_ value: Any) -> String? {
guard JSONSerialization.isValidJSONObject(value),
let data = try? JSONSerialization.data(withJSONObject: value),
let text = String(data: data, encoding: .utf8) else {
return nil
}
return text
}
private func errorPayload(_ error: Error) -> [String: Any] {
let message = error.localizedDescription
var payload: [String: Any] = ["ok": false, "error": message]
if case ComputerUseError.staleSnapshot = error {
payload["staleSnapshot"] = true
payload["retryable"] = false
payload["requiredNextAction"] = "snapshot"
payload["hint"] = "Take a fresh snapshot before retrying. Do not repeat the same action against stale UI state."
}
if message.localizedCaseInsensitiveContains("accessibility") {
payload["permissionNeeded"] = "accessibility"
}
if message.localizedCaseInsensitiveContains("screenshot") || message.localizedCaseInsensitiveContains("screen recording") {
payload["permissionNeeded"] = "screen-recording"
}
return payload
}
private func log(_ message: String) {
fputs("[ComputerUse] \(message)\n", stderr)
}
private func intArg(_ args: [String: Any], _ key: String) -> Int? {
if let value = args[key] as? Int { return value }
if let value = args[key] as? Double { return Int(value) }
if let value = args[key] as? String { return Int(value) }
return nil
}
private func doubleArg(_ args: [String: Any], _ key: String) -> Double? {
if let value = args[key] as? Double { return value }
if let value = args[key] as? Int { return Double(value) }
if let value = args[key] as? String { return Double(value) }
return nil
}
private func boolArg(_ args: [String: Any], _ key: String) -> Bool? {
if let value = args[key] as? Bool { return value }
if let value = args[key] as? String {
if value == "true" { return true }
if value == "false" { return false }
}
return nil
}
private func snapshotIDArg(_ args: [String: Any]) -> String? {
args["snapshot_id"] as? String ?? args["snapshotId"] as? String
}
}
@@ -0,0 +1,593 @@
import AppKit
import ApplicationServices
import Foundation
enum ComputerUsePermissionTarget: String {
case accessibility
case screenRecording = "screen-recording"
}
struct ComputerUsePermissionStatus {
let accessibility: Bool
let screenRecording: Bool
var ok: Bool { accessibility && screenRecording }
var dictionary: [String: Any] {
["ok": ok, "accessibility": accessibility, "screenRecording": screenRecording]
}
}
enum ComputerUsePermissions {
/// Non-prompting check — safe to call on a timer / from HTTP polling.
static func status() -> ComputerUsePermissionStatus {
ComputerUsePermissionStatus(
accessibility: AXIsProcessTrusted(),
screenRecording: CGPreflightScreenCaptureAccess()
)
}
/// Prompting request — only call when the user explicitly clicks "Grant".
static func request(_ target: ComputerUsePermissionTarget) {
switch target {
case .accessibility:
let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
_ = AXIsProcessTrustedWithOptions(opts)
case .screenRecording:
_ = CGRequestScreenCaptureAccess()
}
}
}
// MARK: - App delegate
@MainActor
final class PermissionSetupAppDelegate: NSObject, NSApplicationDelegate {
private var windowController: NSWindowController?
func applicationDidFinishLaunching(_ notification: Notification) {
let window = PermissionSetupWindow()
let vc = PermissionSetupViewController()
window.contentViewController = vc
let wc = NSWindowController(window: window)
windowController = wc
wc.showWindow(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
}
// MARK: - Window
private final class PermissionSetupWindow: NSWindow {
init() {
super.init(
contentRect: NSRect(x: 0, y: 0, width: 460, height: 600),
styleMask: [.titled, .closable, .fullSizeContentView],
backing: .buffered,
defer: false
)
title = "OpenWork Computer Use"
titleVisibility = .hidden
titlebarAppearsTransparent = true
isMovableByWindowBackground = true
backgroundColor = .clear
isOpaque = false
hasShadow = true
center()
}
}
// MARK: - View controller
@MainActor
final class PermissionSetupViewController: NSViewController {
private var axBadge = StatusBadge()
private var srBadge = StatusBadge()
private var axGrantButton: NSButton?
private var srGrantButton: NSButton?
private var timer: Timer?
init() { super.init(nibName: nil, bundle: nil) }
required init?(coder: NSCoder) { nil }
deinit { timer?.invalidate() }
override func loadView() {
// Liquid glass: NSVisualEffectView blending with content behind the window
let blur = NSVisualEffectView()
blur.material = .underWindowBackground
blur.blendingMode = .behindWindow
blur.state = .active
blur.wantsLayer = true
blur.layer?.cornerRadius = 12
blur.layer?.masksToBounds = true
view = blur
}
override func viewDidLoad() {
super.viewDidLoad()
buildLayout()
refresh()
timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [weak self] _ in
guard let controller = self else { return }
Task { @MainActor in controller.refresh() }
}
}
private func buildLayout() {
let iconView = NSImageView()
iconView.image = NSApplication.shared.applicationIconImage
iconView.imageScaling = .scaleProportionallyUpOrDown
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.widthAnchor.constraint(equalToConstant: 52).isActive = true
iconView.heightAnchor.constraint(equalToConstant: 52).isActive = true
let titleField = textField("Computer Use Setup", size: 20, weight: .semibold)
titleField.alignment = .center
let subtitleField = wrappingField(
"Grant two permissions so agents can see and control apps in the background.",
size: 13
)
subtitleField.textColor = .secondaryLabelColor
subtitleField.alignment = .center
let header = vstack([iconView, titleField, subtitleField], spacing: 8, alignment: NSLayoutConstraint.Attribute.centerX)
let axCard = makeAccessibilityCard()
let srCard = makeScreenRecordingCard()
let doneBtn = NSButton(title: "Done — Return to OpenWork", target: self, action: #selector(done))
doneBtn.bezelStyle = .rounded
doneBtn.controlSize = .large
doneBtn.keyEquivalent = "\r"
let root = vstack([header, axCard, srCard, doneBtn], spacing: 14, alignment: NSLayoutConstraint.Attribute.leading)
root.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(root)
NSLayoutConstraint.activate([
root.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
root.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
root.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 52),
root.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -20),
root.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 10),
axCard.widthAnchor.constraint(equalTo: root.widthAnchor),
srCard.widthAnchor.constraint(equalTo: root.widthAnchor),
doneBtn.widthAnchor.constraint(equalTo: root.widthAnchor),
])
}
// MARK: Accessibility card
private func makeAccessibilityCard() -> NSView {
let step = StepCircle(number: "1")
let title = textField("Accessibility", size: 15, weight: .semibold)
let body = wrappingField(
"Allows agents to interact with UI controls, click buttons, and type text entirely in the background.",
size: 13
)
body.textColor = .secondaryLabelColor
let btn = NSButton(title: "Grant Accessibility", target: self, action: #selector(grantAccessibility))
btn.bezelStyle = .rounded
btn.controlSize = .regular
axGrantButton = btn
let top = hstack([step, title, springy(), axBadge], spacing: 8)
let inner = vstack([top, body, btn], spacing: 10, alignment: NSLayoutConstraint.Attribute.leading)
btn.widthAnchor.constraint(equalTo: inner.widthAnchor).isActive = true
return glassCard(inner)
}
// MARK: Screen Recording card
private func makeScreenRecordingCard() -> NSView {
let step = StepCircle(number: "2")
let title = textField("Screen Recording", size: 15, weight: .semibold)
let body = wrappingField(
"Lets agents see what is on screen. If macOS does not prompt automatically, drag the app icon below into the Screen Recording list.",
size: 13
)
body.textColor = .secondaryLabelColor
let dragFlow = makeDragFlowView()
let reqBtn = NSButton(title: "Request Screen Recording", target: self, action: #selector(requestScreenRecording))
reqBtn.bezelStyle = .rounded
reqBtn.controlSize = .regular
srGrantButton = reqBtn
let openBtn = NSButton(title: "Open Privacy & Security", target: self, action: #selector(openPrivacySecurity))
openBtn.bezelStyle = .rounded
openBtn.controlSize = .small
let top = hstack([step, title, springy(), srBadge], spacing: 8)
let inner = vstack([top, body, dragFlow, reqBtn, openBtn], spacing: 10, alignment: NSLayoutConstraint.Attribute.leading)
reqBtn.widthAnchor.constraint(equalTo: inner.widthAnchor).isActive = true
openBtn.widthAnchor.constraint(equalTo: inner.widthAnchor).isActive = true
dragFlow.widthAnchor.constraint(equalTo: inner.widthAnchor).isActive = true
return glassCard(inner)
}
// MARK: Drag flow visualization
private func makeDragFlowView() -> NSView {
// Large draggable app icon
let iconView = DraggableAppIconView(frame: .zero)
iconView.image = NSApplication.shared.applicationIconImage
iconView.imageScaling = .scaleProportionallyUpOrDown
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.widthAnchor.constraint(equalToConstant: 56).isActive = true
iconView.heightAnchor.constraint(equalToConstant: 56).isActive = true
let dragHint = textField("Drag me", size: 10)
dragHint.textColor = .tertiaryLabelColor
dragHint.alignment = .center
let iconCol = vstack([iconView, dragHint], spacing: 5, alignment: NSLayoutConstraint.Attribute.centerX)
let arrow = textField("→", size: 20)
arrow.textColor = .tertiaryLabelColor
let dropZone = PrivacyDropZoneView()
dropZone.translatesAutoresizingMaskIntoConstraints = false
dropZone.widthAnchor.constraint(equalToConstant: 116).isActive = true
dropZone.heightAnchor.constraint(equalToConstant: 60).isActive = true
let row = hstack([iconCol, arrow, dropZone], spacing: 14)
row.alignment = NSLayoutConstraint.Attribute.centerY
row.distribution = NSStackView.Distribution.fill
let hint = wrappingField(
"Drag this icon into the Screen Recording list in Privacy & Security, then enable it.",
size: 11
)
hint.textColor = .secondaryLabelColor
hint.alignment = .center
let content = vstack([row, hint], spacing: 8, alignment: NSLayoutConstraint.Attribute.centerX)
content.translatesAutoresizingMaskIntoConstraints = false
let wrapper = InnerTintView()
wrapper.translatesAutoresizingMaskIntoConstraints = false
wrapper.addSubview(content)
NSLayoutConstraint.activate([
content.topAnchor.constraint(equalTo: wrapper.topAnchor, constant: 12),
content.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor, constant: -12),
content.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor, constant: 12),
content.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor, constant: -12),
])
return wrapper
}
// MARK: Refresh
private func refresh() {
let status = ComputerUsePermissions.status()
axBadge.update(granted: status.accessibility)
srBadge.update(granted: status.screenRecording)
axGrantButton?.isEnabled = !status.accessibility
srGrantButton?.isEnabled = !status.screenRecording
}
// MARK: Actions
@objc private func grantAccessibility() {
ComputerUsePermissions.request(.accessibility)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in self?.refresh() }
}
@objc private func requestScreenRecording() {
ComputerUsePermissions.request(.screenRecording)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in self?.refresh() }
}
@objc private func openPrivacySecurity() {
guard let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") else { return }
NSWorkspace.shared.open(url)
}
@objc private func done() {
NSApplication.shared.terminate(nil)
}
// MARK: Builder helpers
private func glassCard(_ content: NSView) -> GlassCardView {
let card = GlassCardView()
card.addContent(content)
return card
}
private func textField(_ text: String, size: CGFloat, weight: NSFont.Weight = .regular) -> NSTextField {
let f = NSTextField(labelWithString: text)
f.font = .systemFont(ofSize: size, weight: weight)
return f
}
private func wrappingField(_ text: String, size: CGFloat) -> NSTextField {
let f = NSTextField(wrappingLabelWithString: text)
f.font = .systemFont(ofSize: size)
return f
}
private func vstack(_ views: [NSView], spacing: CGFloat, alignment: NSLayoutConstraint.Attribute = NSLayoutConstraint.Attribute.leading) -> NSStackView {
let sv = NSStackView(views: views)
sv.orientation = .vertical
sv.spacing = spacing
sv.alignment = alignment
return sv
}
private func hstack(_ views: [NSView], spacing: CGFloat) -> NSStackView {
let sv = NSStackView(views: views)
sv.orientation = .horizontal
sv.spacing = spacing
sv.alignment = NSLayoutConstraint.Attribute.centerY
return sv
}
private func springy() -> NSView {
let v = NSView()
v.setContentHuggingPriority(.defaultLow, for: .horizontal)
v.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return v
}
}
// MARK: - Glass card
/// NSView container with an NSVisualEffectView inside for the glass effect.
/// Uses updateLayer() so the border color responds to appearance changes.
final class GlassCardView: NSView {
private let blur: NSVisualEffectView
override init(frame: NSRect) {
blur = NSVisualEffectView()
blur.material = .contentBackground
blur.blendingMode = .withinWindow
blur.state = .active
blur.wantsLayer = true
blur.layer?.cornerRadius = 13
blur.layer?.masksToBounds = true
blur.translatesAutoresizingMaskIntoConstraints = false
super.init(frame: frame)
wantsLayer = true
layer?.cornerRadius = 14
addSubview(blur)
NSLayoutConstraint.activate([
blur.topAnchor.constraint(equalTo: topAnchor),
blur.bottomAnchor.constraint(equalTo: bottomAnchor),
blur.leadingAnchor.constraint(equalTo: leadingAnchor),
blur.trailingAnchor.constraint(equalTo: trailingAnchor),
])
}
required init?(coder: NSCoder) { nil }
override var wantsUpdateLayer: Bool { true }
override func updateLayer() {
layer?.borderWidth = 0.5
layer?.borderColor = NSColor.separatorColor.cgColor
}
func addContent(_ content: NSView) {
content.translatesAutoresizingMaskIntoConstraints = false
blur.addSubview(content)
NSLayoutConstraint.activate([
content.topAnchor.constraint(equalTo: blur.topAnchor, constant: 16),
content.bottomAnchor.constraint(equalTo: blur.bottomAnchor, constant: -16),
content.leadingAnchor.constraint(equalTo: blur.leadingAnchor, constant: 16),
content.trailingAnchor.constraint(equalTo: blur.trailingAnchor, constant: -16),
])
}
}
// MARK: - Status badge
/// Pill badge showing Granted/Needed with adaptive color.
@MainActor
final class StatusBadge: NSView {
private let dot = NSView()
private let label = NSTextField(labelWithString: "")
private var isGranted = false
override init(frame: NSRect) {
super.init(frame: frame)
wantsLayer = true
layer?.cornerRadius = 10
dot.wantsLayer = true
dot.layer?.cornerRadius = 4
dot.translatesAutoresizingMaskIntoConstraints = false
dot.widthAnchor.constraint(equalToConstant: 8).isActive = true
dot.heightAnchor.constraint(equalToConstant: 8).isActive = true
label.font = .systemFont(ofSize: 12, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
let row = NSStackView(views: [dot, label])
row.orientation = .horizontal
row.spacing = 5
row.alignment = .centerY
row.translatesAutoresizingMaskIntoConstraints = false
addSubview(row)
NSLayoutConstraint.activate([
row.topAnchor.constraint(equalTo: topAnchor, constant: 4),
row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
])
}
required init?(coder: NSCoder) { nil }
func update(granted: Bool) {
isGranted = granted
applyColors()
label.stringValue = granted ? "Granted" : "Needed"
label.textColor = granted ? .systemGreen : .systemOrange
}
private func applyColors() {
layer?.backgroundColor = isGranted
? NSColor.systemGreen.withAlphaComponent(0.15).cgColor
: NSColor.systemOrange.withAlphaComponent(0.12).cgColor
dot.layer?.backgroundColor = isGranted
? NSColor.systemGreen.cgColor
: NSColor.systemOrange.cgColor
}
override var wantsUpdateLayer: Bool { true }
override func updateLayer() { applyColors() }
}
// MARK: - Step circle
private final class StepCircle: NSView {
init(number: String) {
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = NSColor.controlAccentColor.cgColor
layer?.cornerRadius = 11
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: 22).isActive = true
heightAnchor.constraint(equalToConstant: 22).isActive = true
let label = NSTextField(labelWithString: number)
label.font = .systemFont(ofSize: 12, weight: .semibold)
label.textColor = .white
label.alignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
required init?(coder: NSCoder) { nil }
override var wantsUpdateLayer: Bool { true }
override func updateLayer() {
layer?.backgroundColor = NSColor.controlAccentColor.cgColor
}
}
// MARK: - Inner tint (drag flow background)
/// Subtle tinted background for the drag-flow visualization.
/// Adapts to light/dark appearance via updateLayer().
private final class InnerTintView: NSView {
override init(frame: NSRect) {
super.init(frame: frame)
wantsLayer = true
layer?.cornerRadius = 10
}
required init?(coder: NSCoder) { nil }
override var wantsUpdateLayer: Bool { true }
override func updateLayer() {
let isDark = effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
layer?.backgroundColor = isDark
? NSColor.white.withAlphaComponent(0.05).cgColor
: NSColor.black.withAlphaComponent(0.04).cgColor
}
}
// MARK: - Privacy drop zone
/// Dashed rounded rect showing where the user should drop the app icon.
final class PrivacyDropZoneView: NSView {
private let label: NSTextField
override init(frame: NSRect) {
label = NSTextField(labelWithString: "Screen Recording\nlist")
super.init(frame: frame)
label.font = .systemFont(ofSize: 11)
label.textColor = .tertiaryLabelColor
label.alignment = .center
label.maximumNumberOfLines = 2
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor),
label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 4),
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -4),
])
}
required init?(coder: NSCoder) { nil }
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSGraphicsContext.saveGraphicsState()
let path = NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 8, yRadius: 8)
path.lineWidth = 1.5
path.setLineDash([6.0, 4.0], count: 2, phase: 0)
NSColor.tertiaryLabelColor.setStroke()
path.stroke()
NSGraphicsContext.restoreGraphicsState()
}
}
// MARK: - Draggable app icon
/// NSImageView that initiates a drag session carrying the app bundle URL,
/// so it can be dropped into System Settings > Privacy & Security > Screen Recording.
final class DraggableAppIconView: NSImageView, NSDraggingSource {
override init(frame: NSRect) {
super.init(frame: frame)
wantsLayer = true
layer?.cornerRadius = 12
layer?.masksToBounds = true
addTrackingArea(NSTrackingArea(
rect: .zero,
options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect],
owner: self,
userInfo: nil
))
}
required init?(coder: NSCoder) { nil }
override func mouseEntered(with event: NSEvent) {
NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0.12
animator().alphaValue = 0.75
}
}
override func mouseExited(with event: NSEvent) {
NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0.12
animator().alphaValue = 1.0
}
}
override func mouseDown(with event: NSEvent) {
guard let image else { return }
let appURL = Bundle.main.bundleURL
let item = NSPasteboardItem()
item.setString(appURL.absoluteString, forType: .fileURL)
let dragItem = NSDraggingItem(pasteboardWriter: item)
dragItem.setDraggingFrame(bounds, contents: image)
beginDraggingSession(with: [dragItem], event: event, source: self)
}
func draggingSession(
_ session: NSDraggingSession,
sourceOperationMaskFor context: NSDraggingContext
) -> NSDragOperation {
.copy
}
}
@@ -0,0 +1,248 @@
import AppKit
import ApplicationServices
struct ElementFrame: Sendable {
let x: Int
let y: Int
let width: Int
let height: Int
var center: CGPoint {
CGPoint(x: x + width / 2, y: y + height / 2)
}
var dictionary: [String: Any] {
["x": x, "y": y, "width": width, "height": height]
}
}
struct AXElementState: Sendable {
let enabled: Bool?
let focused: Bool?
let selected: Bool?
let expanded: Bool?
let checked: Bool?
var dictionary: [String: Any] {
var result: [String: Any] = [:]
if let enabled { result["enabled"] = enabled }
if let focused { result["focused"] = focused }
if let selected { result["selected"] = selected }
if let expanded { result["expanded"] = expanded }
if let checked { result["checked"] = checked }
return result
}
}
struct AXElementCapabilities: Sendable {
let canPress: Bool
let canFocus: Bool
let canScroll: Bool
let canAdjust: Bool
let canSetValue: Bool
let actions: [String]
var dictionary: [String: Any] {
[
"press": canPress,
"focus": canFocus,
"scroll": canScroll,
"adjust": canAdjust,
"setValue": canSetValue,
"actions": actions,
]
}
}
struct SemanticAXElement: Identifiable, Sendable {
let id: Int
let ref: String
let role: String
let label: String
let value: String?
let frame: ElementFrame
let state: AXElementState
let capabilities: AXElementCapabilities
var dictionary: [String: Any] {
var result: [String: Any] = [
"id": id,
"ref": ref,
"role": role,
"label": label,
"frame": frame.dictionary,
"state": state.dictionary,
"capabilities": capabilities.dictionary,
]
if let value { result["value"] = value }
return result
}
}
struct AXElementRecord: @unchecked Sendable {
let element: AXUIElement
let semantic: SemanticAXElement
}
struct ScreenshotMetadata: Sendable {
let imageWidth: Int
let imageHeight: Int
let capturedBounds: CGRect
var scaleX: CGFloat { capturedBounds.width / CGFloat(imageWidth) }
var scaleY: CGFloat { capturedBounds.height / CGFloat(imageHeight) }
func toScreen(imageX: Double, imageY: Double) -> CGPoint {
CGPoint(
x: capturedBounds.origin.x + imageX * scaleX,
y: capturedBounds.origin.y + imageY * scaleY
)
}
func toImage(point: CGPoint) -> CGPoint {
CGPoint(
x: (point.x - capturedBounds.origin.x) / scaleX,
y: (point.y - capturedBounds.origin.y) / scaleY
)
}
var dictionary: [String: Any] {
[
"imageWidth": imageWidth,
"imageHeight": imageHeight,
"capturedBounds": [
"x": Int(capturedBounds.origin.x),
"y": Int(capturedBounds.origin.y),
"width": Int(capturedBounds.width),
"height": Int(capturedBounds.height),
],
]
}
}
struct WindowTarget: @unchecked Sendable {
let appName: String
let pid: pid_t
let windowNumber: Int?
let windowTitle: String?
let bounds: CGRect
let isFrontmost: Bool
let axWindow: AXUIElement?
var center: CGPoint {
CGPoint(x: bounds.midX, y: bounds.midY)
}
}
struct AppSnapshot: @unchecked Sendable {
let id: String
let observation: Int
let appName: String
let pid: pid_t
let windowNumber: Int?
let windowTitle: String?
let screenshotData: Data
let screenshotMimeType: String
let screenshotMeta: ScreenshotMetadata
let records: [AXElementRecord]
let strictMode: Bool
let backgroundActivated: Bool
let recentActions: [String]
let addedLabels: [String]
let removedLabels: [String]
var elements: [SemanticAXElement] {
records.map(\.semantic)
}
func withSessionMetadata(id: String, observation: Int, recentActions: [String], addedLabels: [String], removedLabels: [String]) -> AppSnapshot {
AppSnapshot(
id: id,
observation: observation,
appName: appName,
pid: pid,
windowNumber: windowNumber,
windowTitle: windowTitle,
screenshotData: screenshotData,
screenshotMimeType: screenshotMimeType,
screenshotMeta: screenshotMeta,
records: records,
strictMode: strictMode,
backgroundActivated: backgroundActivated,
recentActions: recentActions,
addedLabels: addedLabels,
removedLabels: removedLabels
)
}
}
enum ExecutionPath: String {
case accessibility = "accessibility"
case backgroundCGEvent = "background_cgevent"
case foregroundCGEvent = "foreground_cgevent"
case none = "none"
}
struct ActionMetadata: Sendable {
let ok: Bool
let path: ExecutionPath
let strictMode: Bool
let backgroundSafe: Bool
let fallbackUsed: Bool
let message: String
var dictionary: [String: Any] {
[
"ok": ok,
"path": path.rawValue,
"strictMode": strictMode,
"backgroundSafe": backgroundSafe,
"fallbackUsed": fallbackUsed,
"message": message,
]
}
}
enum ComputerUseError: LocalizedError {
case accessibilityDenied
case screenshotFailed
case appNotFound(String)
case noFrontmostApplication
case noWindow(String)
case noSnapshot
case staleSnapshot(String)
case invalidElement(String)
case strictModeViolation(String)
case eventSourceFailed
case eventCreationFailed
case unknownKey(String)
var errorDescription: String? {
switch self {
case .accessibilityDenied:
return "Accessibility permission is not granted."
case .screenshotFailed:
return "Screenshot capture failed."
case .appNotFound(let name):
return "App '\(name)' is not running."
case .noFrontmostApplication:
return "No frontmost application found."
case .noWindow(let app):
return "No usable window found for \(app)."
case .noSnapshot:
return "No snapshot is available. Call snapshot first."
case .staleSnapshot(let reason):
return "The last snapshot is stale. \(reason)"
case .invalidElement(let ref):
return "Element \(ref) was not found in the last semantic snapshot."
case .strictModeViolation(let reason):
return "Strict background mode rejected the action: \(reason)"
case .eventSourceFailed:
return "Failed to create CGEventSource."
case .eventCreationFailed:
return "Failed to create CGEvent."
case .unknownKey(let key):
return "Unknown key: \(key)"
}
}
}
@@ -0,0 +1,65 @@
/// Computer Use: semantic AX and background-safe macOS computer use.
///
/// Four modes:
/// mcp — run the MCP server over stdio
/// --check — print permission status as JSON to stdout and exit
/// --list-apps — print running regular apps as JSON to stdout and exit
/// (default) — open the permission setup GUI
import AppKit
import Foundation
setbuf(stdout, nil)
let args = CommandLine.arguments
let subcommand = args.count >= 2 ? args[1] : ""
switch subcommand {
case "mcp":
if ProcessInfo.processInfo.environment["OPENWORK_COMPUTER_USE_CURSOR_OVERLAY"] == "0" {
let server = MCPServer()
await server.run()
} else {
await runMCPServerWithOverlay()
}
case "--check":
// Fresh process → fresh TCC read → always accurate.
let status = ComputerUsePermissions.status()
let json = "{\"ok\":\(status.ok),\"accessibility\":\(status.accessibility),\"screenRecording\":\(status.screenRecording)}"
print(json)
exit(0)
case "--list-apps":
// Running regular apps (Dock-visible). Needs no TCC permissions, so this
// is safe to call from the composer at any time.
let apps = NSWorkspace.shared.runningApplications
.filter { $0.activationPolicy == .regular }
.compactMap(\.localizedName)
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
let payload: [String: Any] = ["ok": true, "apps": apps]
let data = (try? JSONSerialization.data(withJSONObject: payload)) ?? Data("{\"ok\":false,\"apps\":[]}".utf8)
print(String(decoding: data, as: UTF8.self))
exit(0)
default:
await runPermissionSetupApp()
}
@MainActor
func runMCPServerWithOverlay() async {
NSApplication.shared.setActivationPolicy(.accessory)
let server = MCPServer()
Task.detached {
await server.run()
await MainActor.run {
NSApplication.shared.terminate(nil)
}
}
NSApplication.shared.run()
}
@MainActor
func runPermissionSetupApp() async {
NSApplication.shared.setActivationPolicy(.regular)
let appDelegate = PermissionSetupAppDelegate()
NSApplication.shared.delegate = appDelegate
NSApplication.shared.run()
}