fix(computer-use): prevent repeated screen recording prompts (#13427)

This commit is contained in:
Jinwoo Hong 2026-08-09 20:20:36 -07:00 committed by GitHub
parent bdd763188f
commit 6e63bbbb52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 32 additions and 213 deletions

View File

@ -1044,11 +1044,7 @@ private func screenCaptureTrusted() -> Bool {
}
private func screenCaptureTrustedSettled() -> Bool {
PermissionTrustSettling.settleWithFallback(
finalTimeoutMs: 500,
probe: screenCaptureTrusted,
fallbackProbe: screenCaptureTrustedByCaptureProbe
)
PermissionTrustSettling.settle(timeoutMs: 2_000, probe: screenCaptureTrusted).settled
}
private func permissionStatusSnapshotSettled() -> PermissionStatusSnapshot {
@ -1058,40 +1054,6 @@ private func permissionStatusSnapshotSettled() -> PermissionStatusSnapshot {
)
}
private func screenCaptureTrustedByCaptureProbe() -> Bool {
guard let infos = CGWindowListCopyWindowInfo(
[.optionOnScreenOnly],
kCGNullWindowID
) as? [[String: Any]] else {
return false
}
let windows = infos.compactMap { info -> ScreenCaptureProbeWindow? in
guard let layer = info[kCGWindowLayer as String] as? Int,
let ownerPid = info[kCGWindowOwnerPID as String] as? NSNumber,
let number = info[kCGWindowNumber as String] as? NSNumber
else {
return nil
}
return ScreenCaptureProbeWindow(
layer: layer,
ownerPid: ownerPid.int32Value,
windowId: number.uint32Value
)
}
guard let windowId = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
ownPid: ProcessInfo.processInfo.processIdentifier,
windows: windows
) else {
return false
}
return CGWindowListCreateImage(
.null,
[.optionIncludingWindow],
CGWindowID(windowId),
[.boundsIgnoreFraming]
) != nil
}
private func requestScreenCaptureAccess() -> Bool {
CGRequestScreenCaptureAccess()
}

View File

@ -41,33 +41,4 @@ public enum PermissionTrustSettling {
waitedMs += interval
}
}
public static func settleWithFallback(
initialTimeoutMs: Int = defaultTimeoutMs,
finalTimeoutMs: Int,
intervalMs: Int = defaultIntervalMs,
sleepMs: (Int) -> Void = { interval in
Thread.sleep(forTimeInterval: TimeInterval(interval) / 1_000)
},
probe: () -> Bool,
fallbackProbe: () -> Bool
) -> Bool {
if settle(
timeoutMs: initialTimeoutMs,
intervalMs: intervalMs,
sleepMs: sleepMs,
probe: probe
).settled {
return true
}
if fallbackProbe() {
return true
}
return settle(
timeoutMs: finalTimeoutMs,
intervalMs: intervalMs,
sleepMs: sleepMs,
probe: probe
).settled
}
}

View File

@ -1,22 +0,0 @@
public struct ScreenCaptureProbeWindow: Equatable {
public let layer: Int
public let ownerPid: Int32
public let windowId: UInt32
public init(layer: Int, ownerPid: Int32, windowId: UInt32) {
self.layer = layer
self.ownerPid = ownerPid
self.windowId = windowId
}
}
public enum ScreenCaptureProbeWindowSelection {
public static func firstCrossProcessNormalWindow(
ownPid: Int32,
windows: [ScreenCaptureProbeWindow]
) -> UInt32? {
windows.first { window in
window.layer == 0 && window.ownerPid != ownPid
}?.windowId
}
}

View File

@ -0,0 +1,31 @@
import XCTest
final class ScreenCapturePermissionPreflightSafetyTests: XCTestCase {
func testPermissionPreflightCannotRequestOrCaptureTheScreen() throws {
let source = try computerUseSource()
let start = try XCTUnwrap(source.range(of: "private func screenCaptureTrusted()"))
let end = try XCTUnwrap(
source.range(of: "private func requestScreenCaptureAccess()", range: start.upperBound..<source.endIndex)
)
let preflightSource = source[start.lowerBound..<end.lowerBound]
XCTAssertTrue(preflightSource.contains("CGPreflightScreenCaptureAccess()"))
XCTAssertTrue(preflightSource.contains("timeoutMs: 2_000"))
XCTAssertFalse(preflightSource.contains("CGRequestScreenCaptureAccess()"))
XCTAssertFalse(preflightSource.contains("CGWindowListCreateImage"))
XCTAssertFalse(preflightSource.contains("SCScreenshotManager"))
}
private func computerUseSource() throws -> String {
let testFile = URL(fileURLWithPath: #filePath)
let packageRoot = testFile
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let mainPath = packageRoot
.appendingPathComponent("Sources")
.appendingPathComponent("OrcaComputerUseMacOS")
.appendingPathComponent("main.swift")
return try String(contentsOf: mainPath, encoding: .utf8)
}
}

View File

@ -1,79 +0,0 @@
import Testing
@testable import OrcaComputerUseMacOSCore
@Suite("Screen Capture permission settling")
struct ScreenCapturePermissionSettlingTests {
@Test("initial preflight success skips fallback")
func initialSuccess() {
var fallbackCalls = 0
let trusted = PermissionTrustSettling.settleWithFallback(
finalTimeoutMs: 500,
sleepMs: { _ in },
probe: { true },
fallbackProbe: {
fallbackCalls += 1
return true
}
)
#expect(trusted)
#expect(fallbackCalls == 0)
}
@Test("capture fallback can establish trust")
func fallbackSuccess() {
var fallbackCalls = 0
let trusted = PermissionTrustSettling.settleWithFallback(
initialTimeoutMs: 200,
finalTimeoutMs: 200,
intervalMs: 100,
sleepMs: { _ in },
probe: { false },
fallbackProbe: {
fallbackCalls += 1
return true
}
)
#expect(trusted)
#expect(fallbackCalls == 1)
}
@Test("preflight is retried after fallback failure")
func finalPreflightSuccess() {
var probeCalls = 0
let trusted = PermissionTrustSettling.settleWithFallback(
initialTimeoutMs: 100,
finalTimeoutMs: 200,
intervalMs: 100,
sleepMs: { _ in },
probe: {
probeCalls += 1
return probeCalls == 4
},
fallbackProbe: { false }
)
#expect(trusted)
#expect(probeCalls == 4)
}
@Test("persistent denial remains denied")
func persistentDenial() {
var fallbackCalls = 0
let trusted = PermissionTrustSettling.settleWithFallback(
initialTimeoutMs: 100,
finalTimeoutMs: 100,
intervalMs: 100,
sleepMs: { _ in },
probe: { false },
fallbackProbe: {
fallbackCalls += 1
return false
}
)
#expect(!trusted)
#expect(fallbackCalls == 1)
}
}

View File

@ -1,44 +0,0 @@
import Testing
@testable import OrcaComputerUseMacOSCore
@Suite("Screen capture probe window selection")
struct ScreenCaptureProbeWindowSelectionTests {
@Test("own process windows cannot prove cross-app capture permission")
func excludesOwnWindows() {
let selected = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
ownPid: 42,
windows: [
.init(layer: 0, ownerPid: 42, windowId: 1),
.init(layer: 0, ownerPid: 84, windowId: 2)
]
)
#expect(selected == 2)
}
@Test("non-normal windows are skipped")
func excludesNonNormalWindows() {
let selected = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
ownPid: 42,
windows: [
.init(layer: 1, ownerPid: 84, windowId: 1),
.init(layer: 0, ownerPid: 84, windowId: 2)
]
)
#expect(selected == 2)
}
@Test("no cross-process normal window returns nil")
func noCandidate() {
let selected = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
ownPid: 42,
windows: [
.init(layer: 0, ownerPid: 42, windowId: 1),
.init(layer: 1, ownerPid: 84, windowId: 2)
]
)
#expect(selected == nil)
}
}