chore: import upstream snapshot with attribution
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
public struct ActionArgumentValidationError: Error, Equatable {
|
||||
public let message: String
|
||||
|
||||
public init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
public enum ActionArgumentValidation {
|
||||
public static func positiveInteger(
|
||||
_ value: Double?,
|
||||
defaultValue: Int,
|
||||
name: String
|
||||
) -> Result<Int, ActionArgumentValidationError> {
|
||||
guard let value else { return .success(defaultValue) }
|
||||
guard value.isFinite, value > 0, let parsed = boundedInteger(value, as: Int.self) else {
|
||||
return .failure(ActionArgumentValidationError("\(name) must be a positive integer"))
|
||||
}
|
||||
return .success(parsed)
|
||||
}
|
||||
|
||||
public static func positiveNumber(
|
||||
_ value: Double?,
|
||||
defaultValue: Double,
|
||||
name: String
|
||||
) -> Result<Double, ActionArgumentValidationError> {
|
||||
guard let value else { return .success(defaultValue) }
|
||||
guard value.isFinite, value > 0 else {
|
||||
return .failure(ActionArgumentValidationError("\(name) must be a positive number"))
|
||||
}
|
||||
return .success(value)
|
||||
}
|
||||
|
||||
public static func scrollDirection(_ value: String) -> Result<String, ActionArgumentValidationError> {
|
||||
switch value {
|
||||
case "up", "down", "left", "right":
|
||||
return .success(value)
|
||||
default:
|
||||
return .failure(ActionArgumentValidationError("unsupported scroll direction: \(value)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
|
||||
public enum ComputerSnapshotCachePolicy {
|
||||
public static let maxEntries = 32
|
||||
public static let maxAge: TimeInterval = 2 * 60
|
||||
|
||||
public static func isExpired(createdAt: Date, now: Date = Date()) -> Bool {
|
||||
now.timeIntervalSince(createdAt) > maxAge
|
||||
}
|
||||
|
||||
public static func shouldPrune(entryCount: Int, createdAt: Date, now: Date = Date()) -> Bool {
|
||||
entryCount > maxEntries || isExpired(createdAt: createdAt, now: now)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
public enum KeyboardInputSafety {
|
||||
public enum FocusFailure: Equatable {
|
||||
case targetNotFocused
|
||||
case targetNotFocusedAfterRestore
|
||||
}
|
||||
|
||||
public static func syntheticInputFocusFailure(targetWindowFocused: Bool, restoreWindowRequested: Bool) -> FocusFailure? {
|
||||
guard !targetWindowFocused else {
|
||||
return nil
|
||||
}
|
||||
return restoreWindowRequested ? .targetNotFocusedAfterRestore : .targetNotFocused
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
/// Converts a JSON-derived number to a fixed-width integer, truncating toward
|
||||
/// zero like `Int(Double)` does, but returning nil instead of trapping when the
|
||||
/// value is non-finite or outside the destination type's range. A malformed
|
||||
/// request (e.g. `elementIndex: 1e300`) would otherwise crash the whole agent
|
||||
/// process on the unchecked `Int(Double)` cast.
|
||||
public func boundedInteger<T: BinaryInteger>(_ value: Double, as type: T.Type = T.self) -> T? {
|
||||
T(exactly: value.rounded(.towardZero))
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import Foundation
|
||||
|
||||
public struct SnapshotRenderNode: Equatable {
|
||||
public var role: String
|
||||
public var roleDescription: String?
|
||||
public var title: String?
|
||||
public var label: String?
|
||||
public var linkText: String?
|
||||
public var value: String?
|
||||
public var placeholder: String?
|
||||
public var url: String?
|
||||
public var traits: [String]
|
||||
public var rawActions: [String]
|
||||
public var childCount: Int
|
||||
public var summary: String?
|
||||
public var rowSummary: String?
|
||||
public var webAreaDepth: Int?
|
||||
|
||||
public init(
|
||||
role: String,
|
||||
roleDescription: String? = nil,
|
||||
title: String? = nil,
|
||||
label: String? = nil,
|
||||
linkText: String? = nil,
|
||||
value: String? = nil,
|
||||
placeholder: String? = nil,
|
||||
url: String? = nil,
|
||||
traits: [String] = [],
|
||||
rawActions: [String] = [],
|
||||
childCount: Int = 0,
|
||||
summary: String? = nil,
|
||||
rowSummary: String? = nil,
|
||||
webAreaDepth: Int? = nil
|
||||
) {
|
||||
self.role = role
|
||||
self.roleDescription = roleDescription
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.linkText = linkText
|
||||
self.value = value
|
||||
self.placeholder = placeholder
|
||||
self.url = url
|
||||
self.traits = traits
|
||||
self.rawActions = rawActions
|
||||
self.childCount = childCount
|
||||
self.summary = summary
|
||||
self.rowSummary = rowSummary
|
||||
self.webAreaDepth = webAreaDepth
|
||||
}
|
||||
}
|
||||
|
||||
public struct SnapshotTabStripCompaction: Equatable {
|
||||
public var retainedIndexes: Set<Int>
|
||||
public var omittedCount: Int
|
||||
|
||||
public init(retainedIndexes: Set<Int>, omittedCount: Int) {
|
||||
self.retainedIndexes = retainedIndexes
|
||||
self.omittedCount = omittedCount
|
||||
}
|
||||
}
|
||||
|
||||
public enum SnapshotRenderHeuristics {
|
||||
public static func shouldProbeSecureTextMetadata(role: String) -> Bool {
|
||||
let normalized = role.lowercased()
|
||||
return normalized.contains("text") ||
|
||||
normalized.contains("field") ||
|
||||
normalized.contains("password") ||
|
||||
normalized.contains("search") ||
|
||||
normalized.contains("combo")
|
||||
}
|
||||
|
||||
public static func supportsAttribute(_ attribute: String, advertisedAttributes: Set<String>?) -> Bool {
|
||||
guard let advertisedAttributes else { return true }
|
||||
return advertisedAttributes.contains(attribute)
|
||||
}
|
||||
|
||||
public static func displayName(_ node: SnapshotRenderNode) -> String? {
|
||||
if let title = clean(node.title) {
|
||||
return title
|
||||
}
|
||||
if node.role == "AXLink", let url = clean(node.url), let text = clean(node.linkText ?? node.label ?? node.value) {
|
||||
return "[\(markdownEscaped(text))](\(url))"
|
||||
}
|
||||
if node.role == "AXWebArea" {
|
||||
return clean(node.label) ?? clean(node.value)
|
||||
}
|
||||
if ["AXButton", "AXPopUpButton", "AXImage"].contains(node.role) {
|
||||
return clean(node.label)
|
||||
}
|
||||
if ["AXRow", "AXCell", "AXOutlineRow"].contains(node.role) {
|
||||
return clean(node.rowSummary)
|
||||
}
|
||||
return clean(node.label)
|
||||
}
|
||||
|
||||
public static func meaningfulActions(_ rawActions: [String], role: String) -> [String] {
|
||||
let noisy: Set<String> = [
|
||||
"AXPress",
|
||||
"AXShowDefaultUI",
|
||||
"AXShowAlternateUI",
|
||||
"AXShowMenu",
|
||||
"AXScrollToVisible",
|
||||
"AXConfirm",
|
||||
"AXRaise",
|
||||
]
|
||||
return rawActions.filter { action in
|
||||
if noisy.contains(action) { return false }
|
||||
if role == "AXMenu" || role == "AXMenuItem" {
|
||||
return action != "AXCancel" && action != "AXPick"
|
||||
}
|
||||
if role == "AXScrollArea",
|
||||
(rawActions.contains("AXScrollUpByPage") || rawActions.contains("AXScrollDownByPage")),
|
||||
action == "AXScrollLeftByPage" || action == "AXScrollRightByPage" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public static func shouldElide(_ node: SnapshotRenderNode) -> Bool {
|
||||
guard node.role == "AXGroup" || node.role == "AXUnknown" else { return false }
|
||||
guard displayName(node) == nil,
|
||||
node.traits.isEmpty,
|
||||
meaningfulActions(node.rawActions, role: node.role).isEmpty,
|
||||
clean(node.summary) == nil
|
||||
else {
|
||||
return false
|
||||
}
|
||||
if node.webAreaDepth != nil, node.childCount > 1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static func shouldSuppressChildren(_ node: SnapshotRenderNode) -> Bool {
|
||||
if node.role == "AXMenuBarItem" {
|
||||
return true
|
||||
}
|
||||
let name = displayName(node)
|
||||
if node.role == "AXLink" && name?.hasPrefix("[") == true {
|
||||
return true
|
||||
}
|
||||
let hasCompactLabel = name != nil || clean(node.value) != nil || clean(node.summary) != nil
|
||||
return hasCompactLabel && compactControlRoles.contains(node.role)
|
||||
}
|
||||
|
||||
public static func shouldSuppressChildren(role: String, name: String? = nil) -> Bool {
|
||||
shouldSuppressChildren(SnapshotRenderNode(role: role, title: name))
|
||||
}
|
||||
|
||||
public static func tabStripCompaction(
|
||||
parent: SnapshotRenderNode,
|
||||
children: [SnapshotRenderNode]
|
||||
) -> SnapshotTabStripCompaction? {
|
||||
guard isBrowserTabStripContainer(parent) else { return nil }
|
||||
let tabIndexes = Set(children.indices.filter { isBrowserTabLike(children[$0]) })
|
||||
guard tabIndexes.count >= 10 else { return nil }
|
||||
|
||||
let selectedTabIndexes = Set(tabIndexes.filter { isSelectedBrowserTab(children[$0]) })
|
||||
guard !selectedTabIndexes.isEmpty else { return nil }
|
||||
|
||||
let nonTabIndexes = Set(children.indices.filter { !tabIndexes.contains($0) })
|
||||
let retainedIndexes = nonTabIndexes.union(selectedTabIndexes)
|
||||
let omittedCount = tabIndexes.count - selectedTabIndexes.count
|
||||
guard omittedCount > 0 else { return nil }
|
||||
return SnapshotTabStripCompaction(retainedIndexes: retainedIndexes, omittedCount: omittedCount)
|
||||
}
|
||||
|
||||
public static func line(index: Int, node: SnapshotRenderNode) -> String {
|
||||
let name = displayName(node)
|
||||
let roleText = roleText(node)
|
||||
let meaningful = meaningfulActions(node.rawActions, role: node.role)
|
||||
var line = roleText.isEmpty ? "\(index)" : "\(index) \(roleText)"
|
||||
if !node.traits.isEmpty { line += " (\(node.traits.joined(separator: ", ")))" }
|
||||
if let name { line += " \(sanitize(name))" }
|
||||
if node.role != "AXLink", let description = clean(node.label), description != name {
|
||||
line += ", Description: \(sanitize(description))"
|
||||
}
|
||||
if let valueSegment = formattedValueSegment(roleText: roleText, name: name, value: node.value) {
|
||||
line += valueSegment
|
||||
}
|
||||
if let placeholder = clean(node.placeholder), placeholder != name, placeholder != node.value {
|
||||
line += name == nil && clean(node.value) == nil ? " Placeholder: \(sanitize(placeholder))" : ", Placeholder: \(sanitize(placeholder))"
|
||||
}
|
||||
if let summary = clean(node.summary), summary != name {
|
||||
line += ", Text: \(sanitize(summary))"
|
||||
} else if let rowSummary = clean(node.rowSummary), rowSummary != name {
|
||||
line += ", Text: \(sanitize(rowSummary))"
|
||||
}
|
||||
if !meaningful.isEmpty {
|
||||
line += ", Secondary Actions: \(meaningful.map(prettyAction).joined(separator: ", "))"
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
public static func roleText(_ node: SnapshotRenderNode) -> String {
|
||||
if node.role == "AXGroup" || node.role == "AXUnknown" {
|
||||
return "container"
|
||||
}
|
||||
if node.role == "AXLink" {
|
||||
return "link"
|
||||
}
|
||||
if node.role == "AXWebArea" {
|
||||
return clean(node.roleDescription) ?? "html content"
|
||||
}
|
||||
if node.role == "AXMenuBarItem" {
|
||||
return ""
|
||||
}
|
||||
if let value = clean(node.roleDescription) {
|
||||
return value.lowercased()
|
||||
}
|
||||
if node.role.hasPrefix("AX") {
|
||||
return splitCamelCase(String(node.role.dropFirst(2))).lowercased()
|
||||
}
|
||||
return node.role
|
||||
}
|
||||
|
||||
public static func prettyAction(_ action: String) -> String {
|
||||
if action == "AXZoomWindow" {
|
||||
return "zoom the window"
|
||||
}
|
||||
let stripped = action.hasPrefix("AX") ? String(action.dropFirst(2)) : action
|
||||
return splitCamelCase(stripped.replacingOccurrences(of: "ByPage", with: "")).lowercased()
|
||||
}
|
||||
|
||||
public static func sanitize(_ value: String) -> String {
|
||||
value.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: " ")
|
||||
}
|
||||
|
||||
private static func clean(_ value: String?) -> String? {
|
||||
guard let value else { return nil }
|
||||
let sanitized = sanitize(value)
|
||||
return sanitized.isEmpty ? nil : sanitized
|
||||
}
|
||||
|
||||
private static func formattedValueSegment(roleText: String, name: String?, value: String?) -> String? {
|
||||
guard let value = clean(value), value != name else { return nil }
|
||||
if roleText == "heading", Int(value) != nil {
|
||||
return nil
|
||||
}
|
||||
let clean = sanitize(value)
|
||||
if roleText == "text" || roleText == "text entry area" || roleText == "scroll bar" || roleText == "value indicator" {
|
||||
return " \(clean)"
|
||||
}
|
||||
return ", Value: \(clean)"
|
||||
}
|
||||
|
||||
private static func markdownEscaped(_ value: String) -> String {
|
||||
sanitize(value)
|
||||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||||
.replacingOccurrences(of: "[", with: "\\[")
|
||||
.replacingOccurrences(of: "]", with: "\\]")
|
||||
}
|
||||
|
||||
private static func isBrowserTabStripContainer(_ node: SnapshotRenderNode) -> Bool {
|
||||
let roleText = roleText(node)
|
||||
let title = clean(node.title)?.lowercased() ?? ""
|
||||
let label = clean(node.label)?.lowercased() ?? ""
|
||||
let description = clean(node.roleDescription)?.lowercased() ?? ""
|
||||
return roleText == "scroll area" ||
|
||||
description == "tab bar" ||
|
||||
title == "tab bar" ||
|
||||
label == "tab bar"
|
||||
}
|
||||
|
||||
private static func isBrowserTabLike(_ node: SnapshotRenderNode) -> Bool {
|
||||
let roleDescription = clean(node.roleDescription)?.lowercased() ?? ""
|
||||
return node.role == "AXTab" || roleDescription == "tab"
|
||||
}
|
||||
|
||||
private static func isSelectedBrowserTab(_ node: SnapshotRenderNode) -> Bool {
|
||||
node.traits.contains("selected") || clean(node.value) == "1"
|
||||
}
|
||||
|
||||
private static func splitCamelCase(_ value: String) -> String {
|
||||
var result = ""
|
||||
for character in value {
|
||||
if character.isUppercase, !result.isEmpty {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(character)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static let compactControlRoles: Set<String> = [
|
||||
"AXButton",
|
||||
"AXCheckBox",
|
||||
"AXComboBox",
|
||||
"AXDisclosureTriangle",
|
||||
"AXHeading",
|
||||
"AXMenuItem",
|
||||
"AXPopUpButton",
|
||||
"AXRadioButton",
|
||||
"AXStaticText",
|
||||
"AXTab",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Darwin
|
||||
|
||||
public enum UnixSocketPathSafety {
|
||||
public static func isSocketMode(_ mode: mode_t) -> Bool {
|
||||
(mode & mode_t(S_IFMT)) == mode_t(S_IFSOCK)
|
||||
}
|
||||
|
||||
public static func shouldRejectExistingPathAfterBindFailure(
|
||||
bindErrno: Int32,
|
||||
existingMode: mode_t?
|
||||
) -> Bool {
|
||||
bindErrno == EADDRINUSE && existingMode.map { !isSocketMode($0) } == true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user