@aimarket/warden 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README-es.md +26 -11
- package/README-fr.md +26 -11
- package/README-ru.md +26 -11
- package/README-zh.md +26 -11
- package/README.md +26 -11
- package/dist/atomic-screen.d.ts +61 -0
- package/dist/atomic-screen.d.ts.map +1 -0
- package/dist/atomic-screen.js +165 -0
- package/dist/atomic-screen.js.map +1 -0
- package/dist/flow-guard.d.ts +181 -2
- package/dist/flow-guard.d.ts.map +1 -1
- package/dist/flow-guard.js +1194 -72
- package/dist/flow-guard.js.map +1 -1
- package/dist/mcp-server.js +5 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +46 -9
- package/dist/mcp-tools.js.map +1 -1
- package/dist/review-diff.d.ts +36 -0
- package/dist/review-diff.d.ts.map +1 -1
- package/dist/review-diff.js +179 -7
- package/dist/review-diff.js.map +1 -1
- package/dist/task-adapter.d.ts +73 -0
- package/dist/task-adapter.d.ts.map +1 -0
- package/dist/task-adapter.js +379 -0
- package/dist/task-adapter.js.map +1 -0
- package/dist/task-cli.d.ts +3 -0
- package/dist/task-cli.d.ts.map +1 -0
- package/dist/task-cli.js +162 -0
- package/dist/task-cli.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wrap-cli.d.ts +5 -0
- package/dist/wrap-cli.d.ts.map +1 -1
- package/dist/wrap-cli.js +27 -1
- package/dist/wrap-cli.js.map +1 -1
- package/dist/wrap-screen.d.ts +3 -0
- package/dist/wrap-screen.d.ts.map +1 -0
- package/dist/wrap-screen.js +20 -0
- package/dist/wrap-screen.js.map +1 -0
- package/dist/wrap-state.d.ts +5 -0
- package/dist/wrap-state.d.ts.map +1 -1
- package/dist/wrap-state.js +3 -1
- package/dist/wrap-state.js.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/dist/wrap.js +161 -5
- package/dist/wrap.js.map +1 -1
- package/docs/flow-guard.es.md +274 -9
- package/docs/flow-guard.fr.md +274 -9
- package/docs/flow-guard.md +275 -10
- package/docs/flow-guard.ru.md +275 -10
- package/docs/flow-guard.zh.md +273 -5
- package/docs/security-hardening.es.md +1 -1
- package/docs/security-hardening.fr.md +1 -1
- package/docs/security-hardening.md +1 -1
- package/docs/security-hardening.ru.md +1 -1
- package/docs/security-hardening.zh.md +1 -1
- package/examples/atomic-screen-server.mjs +42 -0
- package/native/macos/Package.swift +9 -0
- package/native/macos/Sources/WardenDesktop/NativeSurface.swift +246 -0
- package/native/macos/Sources/WardenDesktop/StandardIO.swift +139 -0
- package/native/macos/Sources/WardenDesktopHost/Main.swift +211 -0
- package/package.json +17 -5
- package/scripts/build-macos.mjs +22 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import CryptoKit
|
|
3
|
+
import Security
|
|
4
|
+
import ObjectiveC
|
|
5
|
+
|
|
6
|
+
public let wardenAtomicProtocol = "warden.screen.atomic.v1"
|
|
7
|
+
|
|
8
|
+
public enum DesktopError: Error { case invalid, unavailable, unsupported, uncertain }
|
|
9
|
+
private func digest(_ data: Data) -> String { SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() }
|
|
10
|
+
private func digest(_ text: String) -> String { digest(Data(text.utf8)) }
|
|
11
|
+
private func opaque() throws -> String {
|
|
12
|
+
var bytes = [UInt8](repeating: 0, count: 32)
|
|
13
|
+
guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else { throw DesktopError.unavailable }
|
|
14
|
+
return bytes.map { String(format: "%02x", $0) }.joined()
|
|
15
|
+
}
|
|
16
|
+
private func json(_ value: Any) throws -> String {
|
|
17
|
+
let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed, .withoutEscapingSlashes, .sortedKeys])
|
|
18
|
+
guard let result = String(data: data, encoding: .utf8) else { throw DesktopError.invalid }
|
|
19
|
+
return result
|
|
20
|
+
}
|
|
21
|
+
private func exact(_ value: [String: Any], _ keys: [String]) -> Bool { Set(value.keys) == Set(keys) }
|
|
22
|
+
private func hex(_ value: Any?) -> String? {
|
|
23
|
+
guard let s = value as? String, s.count == 64, s.allSatisfy({ "0123456789abcdef".contains($0) }) else { return nil }; return s
|
|
24
|
+
}
|
|
25
|
+
private func number(_ value: Any?) -> Double? {
|
|
26
|
+
guard let n = value as? NSNumber, CFGetTypeID(n) != CFBooleanGetTypeID(), n.doubleValue.isFinite, abs(n.doubleValue) <= 1_000_000 else { return nil }
|
|
27
|
+
return n.doubleValue
|
|
28
|
+
}
|
|
29
|
+
private typealias NativeAction = @convention(c) (AnyObject, Selector, AnyObject) -> Void
|
|
30
|
+
private func actionImplementation(_ target: NSObject, _ selector: Selector) -> IMP? {
|
|
31
|
+
guard NSStringFromSelector(selector).filter({ $0 == ":" }).count == 1,
|
|
32
|
+
let method = class_getInstanceMethod(type(of: target), selector), method_getNumberOfArguments(method) == 3 else { return nil }
|
|
33
|
+
var returns = [CChar](repeating: 0, count: 32), argument = [CChar](repeating: 0, count: 32)
|
|
34
|
+
method_getReturnType(method, &returns, returns.count); method_getArgumentType(method, 2, &argument, argument.count)
|
|
35
|
+
// A block (@?) is not an object sender, despite sharing the '@' prefix.
|
|
36
|
+
guard returns[0] == 118 && returns[1] == 0 && argument[0] == 64 && argument[1] == 0 else { return nil } // void action(id sender)
|
|
37
|
+
return method_getImplementation(method)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Target-side authority. Embed INSIDE the application that owns these AppKit controls.
|
|
41
|
+
/// All model/handler changes must run on MainActor and call invalidate BEFORE mutation.
|
|
42
|
+
/// This is deliberately not an AXUIElement/CGEvent proxy for other applications.
|
|
43
|
+
@MainActor public final class WardenDesktopSurface {
|
|
44
|
+
private struct Binding {
|
|
45
|
+
let button: NSButton
|
|
46
|
+
let target: NSObject
|
|
47
|
+
let selector: Selector
|
|
48
|
+
let implementation: IMP
|
|
49
|
+
let identity: String
|
|
50
|
+
}
|
|
51
|
+
private struct Capture { let observation: [String: Any]; let fingerprint: String; let boxes: [String: CGRect]; let stamps: [String: String] }
|
|
52
|
+
private struct Ticket {
|
|
53
|
+
let revision: String; let stamp: String; let fingerprint: String; let action: String; let target: String
|
|
54
|
+
let expires: Double; let deadline: UInt64
|
|
55
|
+
}
|
|
56
|
+
private let window: NSWindow
|
|
57
|
+
private let applicationID: String
|
|
58
|
+
private var bindings: [String: Binding] = [:]
|
|
59
|
+
private var tickets: [String: Ticket] = [:]
|
|
60
|
+
private var revision: String
|
|
61
|
+
private var poisoned = false
|
|
62
|
+
private var uncertain = false
|
|
63
|
+
private var committing = false
|
|
64
|
+
private var lastFingerprint: String?
|
|
65
|
+
private let now: () -> Double
|
|
66
|
+
private let uptime: () -> UInt64
|
|
67
|
+
private var observers: [NSObjectProtocol] = []
|
|
68
|
+
deinit { for observer in observers { NotificationCenter.default.removeObserver(observer) } }
|
|
69
|
+
|
|
70
|
+
public init(window: NSWindow, applicationID: String, now: @escaping () -> Double = { Date().timeIntervalSince1970 * 1000 }, uptime: @escaping () -> UInt64 = { DispatchTime.now().uptimeNanoseconds }) throws {
|
|
71
|
+
guard !applicationID.isEmpty, applicationID.utf16.count <= 200, !applicationID.unicodeScalars.contains(where: { $0.value < 32 || $0.value == 127 }) else { throw DesktopError.invalid }
|
|
72
|
+
self.window = window; self.applicationID = applicationID; self.revision = try opaque(); self.now = now; self.uptime = uptime
|
|
73
|
+
for event in [NSWindow.didMoveNotification, NSWindow.didResizeNotification, NSWindow.willCloseNotification, NSWindow.willMiniaturizeNotification, NSWindow.didMiniaturizeNotification,
|
|
74
|
+
NSWindow.willBeginSheetNotification, NSWindow.didEndSheetNotification,
|
|
75
|
+
NSWindow.didDeminiaturizeNotification, NSWindow.didChangeScreenNotification, NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification] {
|
|
76
|
+
observers.append(NotificationCenter.default.addObserver(forName: event, object: window, queue: .main) { [weak self] _ in
|
|
77
|
+
MainActor.assumeIsolated { if let s = self, !s.committing, !s.uncertain { try? s.invalidate() } }
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Only explicit, retained action targets; nil/first-responder routing is not supported.
|
|
83
|
+
/// Register momentary push buttons whose synchronous handlers do not pump a nested run loop.
|
|
84
|
+
public func register(id: String, button: NSButton, target: NSObject, selector: Selector) throws {
|
|
85
|
+
guard !committing, !uncertain else { throw DesktopError.unavailable }
|
|
86
|
+
let identity = button.accessibilityIdentifier()
|
|
87
|
+
guard !committing, bindings.count < 64, bindings[id] == nil, id.range(of: "^[A-Za-z0-9_.:-]{1,40}$", options: .regularExpression) != nil,
|
|
88
|
+
button.window === window, button.target === target, button.action == selector,
|
|
89
|
+
!identity.isEmpty, identity.utf16.count <= 200, !identity.unicodeScalars.contains(where: { $0.value < 32 || $0.value == 127 }),
|
|
90
|
+
!bindings.values.contains(where: { $0.identity == identity || $0.button === button }),
|
|
91
|
+
button.accessibilityRole() == .button,
|
|
92
|
+
let cell = button.cell as? NSButtonCell, cell.showsStateBy.isEmpty,
|
|
93
|
+
let implementation = actionImplementation(target, selector) else { throw DesktopError.unsupported }
|
|
94
|
+
try invalidate()
|
|
95
|
+
bindings[id] = Binding(button: button, target: target, selector: selector, implementation: implementation, identity: identity)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Mandatory for invisible model/recipient/handler changes, even A -> B -> A.
|
|
99
|
+
/// Failure invalidates existing approvals. Does not recover an uncertain effect.
|
|
100
|
+
public func invalidate() throws {
|
|
101
|
+
guard !committing, !uncertain else { throw DesktopError.unavailable }
|
|
102
|
+
poisoned = true; revision = try opaque(); lastFingerprint = nil; poisoned = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func capture() throws -> Capture {
|
|
106
|
+
guard !poisoned, !bindings.isEmpty, window.isVisible, !window.isMiniaturized,
|
|
107
|
+
window.alphaValue.isFinite, window.alphaValue > 0, window.attachedSheet == nil, window.sheets.isEmpty,
|
|
108
|
+
!NSApp.windows.contains(where: { $0.sheetParent === window }),
|
|
109
|
+
NSApp.modalWindow == nil || NSApp.modalWindow === window,
|
|
110
|
+
window.title.utf16.count <= 160, let content = window.contentView else { throw DesktopError.unavailable }
|
|
111
|
+
let top = NSScreen.screens.first?.frame.maxY ?? 0
|
|
112
|
+
var elements: [[String: Any]] = [], boxes: [String: CGRect] = [:], stamps: [String: String] = [:], internals: [[String: Any]] = []
|
|
113
|
+
for id in bindings.keys.sorted() {
|
|
114
|
+
let b = bindings[id]!, button = b.button
|
|
115
|
+
guard button.window === window, button.isDescendant(of: content), !button.isHiddenOrHasHiddenAncestor, button.isEnabled,
|
|
116
|
+
button.target === b.target, button.action == b.selector, actionImplementation(b.target, b.selector) == b.implementation,
|
|
117
|
+
button.accessibilityIdentifier() == b.identity, button.accessibilityRole() == .button,
|
|
118
|
+
(button.cell as? NSButtonCell)?.showsStateBy.isEmpty == true else { throw DesktopError.unavailable }
|
|
119
|
+
guard button.title.utf16.count <= 160 else { throw DesktopError.unavailable }
|
|
120
|
+
var appearance: [[String: Any]] = [], ancestor: NSView? = button
|
|
121
|
+
while let view = ancestor {
|
|
122
|
+
guard appearance.count < 128, view.alphaValue.isFinite, view.alphaValue > 0 else { throw DesktopError.unavailable }
|
|
123
|
+
appearance.append(["view": String(describing: ObjectIdentifier(view)), "alpha": view.alphaValue])
|
|
124
|
+
if view === content { break }
|
|
125
|
+
ancestor = view.superview
|
|
126
|
+
}
|
|
127
|
+
let screen = window.convertToScreen(button.convert(button.bounds, to: nil))
|
|
128
|
+
let box = CGRect(x: screen.minX, y: top - screen.maxY, width: screen.width, height: screen.height)
|
|
129
|
+
guard [box.minX, box.minY, box.width, box.height].allSatisfy({ $0.isFinite && abs($0) <= 1_000_000 }), box.width > 0, box.height > 0 else { throw DesktopError.unavailable }
|
|
130
|
+
let bounds: [String: Any] = ["x": box.minX, "y": box.minY, "width": box.width, "height": box.height]
|
|
131
|
+
elements.append(["index": id, "role": "button", "name": button.title, "accessibilityIdentifier": b.identity, "bounds": bounds])
|
|
132
|
+
boxes[id] = box
|
|
133
|
+
// Match flow-guard.ts screenTargetEvidence exactly; conformance is integration-tested.
|
|
134
|
+
let label = try json([button.title] + Array(repeating: NSNull() as Any, count: 8))
|
|
135
|
+
let geometry = try "{\"x\":\(json(box.minX)),\"y\":\(json(box.minY)),\"width\":\(json(box.width)),\"height\":\(json(box.height))}"
|
|
136
|
+
let stampText = try "{\"target\":\(json(id)),\"role\":\"button\",\"reference\":\(json(digest(id))),\"identity\":\(json(digest(b.identity))),\"bounds\":\(geometry),\"label\":\(json(digest(label))),\"application\":\(json(applicationID)),\"titles\":[\(json(digest(String(window.title.trimmingCharacters(in: .whitespacesAndNewlines).prefix(160)))))]}"
|
|
137
|
+
stamps[id] = digest(stampText)
|
|
138
|
+
internals.append(["id": id, "object": String(describing: ObjectIdentifier(button)), "target": String(describing: ObjectIdentifier(b.target)),
|
|
139
|
+
"selector": NSStringFromSelector(b.selector), "frame": bounds, "title": button.title, "identity": b.identity,
|
|
140
|
+
"state": button.state.rawValue, "tag": button.tag, "alternateTitle": button.alternateTitle, "appearance": appearance])
|
|
141
|
+
}
|
|
142
|
+
let observation: [String: Any] = ["content": [["type": "text", "text": "Native AppKit controls owned by this application; coordinates are global top-left points, not pixels."]],
|
|
143
|
+
"structuredContent": ["applicationId": applicationID, "windowTitle": window.title, "elements": elements]]
|
|
144
|
+
let state: [String: Any] = ["elements": internals, "window": String(describing: ObjectIdentifier(window)), "windowNumber": window.windowNumber,
|
|
145
|
+
"title": window.title, "alpha": window.alphaValue, "modal": NSApp.modalWindow.map { String(describing: ObjectIdentifier($0)) } ?? "",
|
|
146
|
+
"key": window.isKeyWindow, "focus": window.firstResponder.map { String(describing: ObjectIdentifier($0)) } ?? ""]
|
|
147
|
+
return Capture(observation: observation, fingerprint: digest(try json(state)), boxes: boxes, stamps: stamps)
|
|
148
|
+
}
|
|
149
|
+
private func fresh() throws -> Capture {
|
|
150
|
+
guard !committing else { throw DesktopError.unavailable }
|
|
151
|
+
do {
|
|
152
|
+
let view = try capture()
|
|
153
|
+
if let before = lastFingerprint, before != view.fingerprint { revision = try opaque() }
|
|
154
|
+
lastFingerprint = view.fingerprint
|
|
155
|
+
return view
|
|
156
|
+
} catch {
|
|
157
|
+
poisoned = true; tickets.removeAll(); throw DesktopError.unavailable
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
public func read() throws -> [String: Any] { try fresh().observation }
|
|
161
|
+
private func resolve(_ action: [String: Any], _ view: Capture) throws -> String {
|
|
162
|
+
guard exact(action, ["name", "arguments"]), action["name"] as? String == "click", let args = action["arguments"] as? [String: Any] else { throw DesktopError.unsupported }
|
|
163
|
+
let id: String
|
|
164
|
+
var requestedPoint: CGPoint?
|
|
165
|
+
if exact(args, ["index"]), let ref = args["index"] as? String, bindings[ref] != nil { id = ref }
|
|
166
|
+
else if exact(args, ["x", "y"]), let x = number(args["x"]), let y = number(args["y"]) {
|
|
167
|
+
let hits = view.boxes.filter { $0.value.contains(CGPoint(x: x, y: y)) }
|
|
168
|
+
guard hits.count == 1, let ref = hits.first?.key else { throw DesktopError.unsupported }; id = ref
|
|
169
|
+
requestedPoint = CGPoint(x: x, y: y)
|
|
170
|
+
} else { throw DesktopError.unsupported }
|
|
171
|
+
// Bind visible native hit target too; another view in this window must not cover it.
|
|
172
|
+
let b = bindings[id]!, center = requestedPoint ?? CGPoint(x: view.boxes[id]!.midX, y: view.boxes[id]!.midY)
|
|
173
|
+
let top = NSScreen.screens.first?.frame.maxY ?? 0
|
|
174
|
+
let inWindow = window.convertPoint(fromScreen: CGPoint(x: center.x, y: top - center.y))
|
|
175
|
+
guard let content = window.contentView, let hit = content.hitTest(content.convert(inWindow, from: nil)), hit === b.button || hit.isDescendant(of: b.button) else { throw DesktopError.unavailable }
|
|
176
|
+
return id
|
|
177
|
+
}
|
|
178
|
+
public func prepare(_ params: [String: Any]) throws -> [String: Any] {
|
|
179
|
+
guard exact(params, ["protocol", "observation", "action"]), params["protocol"] as? String == wardenAtomicProtocol,
|
|
180
|
+
let observation = params["observation"] as? [String: Any], exact(observation, ["name", "arguments"]), observation["name"] as? String == "screenshot",
|
|
181
|
+
let args = observation["arguments"] as? [String: Any], args.isEmpty, let action = params["action"] as? [String: Any] else { throw DesktopError.invalid }
|
|
182
|
+
let view = try fresh(), observedRevision = revision, target = try resolve(action, view)
|
|
183
|
+
let final = try fresh()
|
|
184
|
+
guard revision == observedRevision, view.fingerprint == final.fingerprint,
|
|
185
|
+
view.stamps[target] == final.stamps[target] else { throw DesktopError.unavailable }
|
|
186
|
+
let actionText = try json(action), time = now(), monotonic = uptime()
|
|
187
|
+
let (deadline, overflow) = monotonic.addingReportingOverflow(300_000_000_000)
|
|
188
|
+
guard time.isFinite, time >= 0, time <= 9_007_199_254_440_991, !overflow,
|
|
189
|
+
revision == observedRevision, !poisoned, !committing else { throw DesktopError.unavailable }
|
|
190
|
+
tickets = tickets.filter { $0.value.expires > time && $0.value.deadline > monotonic }
|
|
191
|
+
guard tickets.count < 256 else { throw DesktopError.unavailable }
|
|
192
|
+
let ticket = try opaque(), expiry = floor(time + 300_000), stamp = view.stamps[target]!
|
|
193
|
+
tickets[ticket] = Ticket(revision: observedRevision, stamp: stamp, fingerprint: view.fingerprint, action: actionText, target: target, expires: expiry, deadline: deadline)
|
|
194
|
+
return ["protocol": wardenAtomicProtocol, "ticket": ticket, "revision": revision, "stamp": stamp, "expiresAt": expiry, "observation": view.observation]
|
|
195
|
+
}
|
|
196
|
+
public func cancel(_ params: [String: Any]) throws {
|
|
197
|
+
guard exact(params, ["protocol", "ticket"]), params["protocol"] as? String == wardenAtomicProtocol, let ticket = hex(params["ticket"]) else { throw DesktopError.invalid }
|
|
198
|
+
tickets.removeValue(forKey: ticket)
|
|
199
|
+
}
|
|
200
|
+
public func commit(_ params: [String: Any]) throws -> [String: Any] {
|
|
201
|
+
guard exact(params, ["protocol", "ticket", "expected", "action"]), params["protocol"] as? String == wardenAtomicProtocol,
|
|
202
|
+
let ticket = hex(params["ticket"]), let expected = params["expected"] as? [String: Any], exact(expected, ["revision", "stamp"]),
|
|
203
|
+
let expectedRevision = hex(expected["revision"]), let stamp = hex(expected["stamp"]), let action = params["action"] as? [String: Any] else { throw DesktopError.invalid }
|
|
204
|
+
func refused(_ reason: String) -> [String: Any] { ["protocol": wardenAtomicProtocol, "status": "refused", "reason": reason] }
|
|
205
|
+
guard let approved = tickets.removeValue(forKey: ticket) else { return refused("unknown") }
|
|
206
|
+
guard !poisoned, !committing else { return refused("unavailable") }
|
|
207
|
+
let admissionTime = now()
|
|
208
|
+
guard admissionTime < approved.expires, admissionTime >= approved.expires - 300_000, uptime() < approved.deadline else { return refused("expired") }
|
|
209
|
+
guard expectedRevision == approved.revision, stamp == approved.stamp, try json(action) == approved.action else { return refused("mismatch") }
|
|
210
|
+
let view: Capture
|
|
211
|
+
do { view = try fresh() } catch { return refused("unavailable") }
|
|
212
|
+
guard revision == approved.revision, view.fingerprint == approved.fingerprint, view.stamps[approved.target] == approved.stamp else { return refused("stale") }
|
|
213
|
+
guard (try? resolve(action, view)) == approved.target, revision == approved.revision, !poisoned,
|
|
214
|
+
let b = bindings[approved.target] else { return refused("stale") }
|
|
215
|
+
// hitTest is overridable AppKit code. Do not rely solely on the host's
|
|
216
|
+
// invalidation callback: visible properties/handler may have changed there.
|
|
217
|
+
let final: Capture
|
|
218
|
+
do { final = try fresh() } catch { return refused("unavailable") }
|
|
219
|
+
guard revision == approved.revision, final.fingerprint == approved.fingerprint,
|
|
220
|
+
final.stamps[approved.target] == approved.stamp else { return refused("stale") }
|
|
221
|
+
// Validation itself may take time. Check both clocks at the dispatch boundary.
|
|
222
|
+
// Allocate the receipt revision before any effect; entropy failure must not
|
|
223
|
+
// escape as an ordinary unavailable error after a successful handler.
|
|
224
|
+
let committedRevision = try opaque()
|
|
225
|
+
let dispatchTime = now()
|
|
226
|
+
guard dispatchTime < approved.expires, dispatchTime >= approved.expires - 300_000,
|
|
227
|
+
uptime() < approved.deadline else { return refused("expired") }
|
|
228
|
+
// Clock providers are injected host callbacks too. No callbacks may run
|
|
229
|
+
// after this final CAS and before the pinned implementation invocation.
|
|
230
|
+
guard revision == approved.revision, !poisoned, !committing else { return refused("stale") }
|
|
231
|
+
// Linearization point in the TARGET APPLICATION's main run loop. No AX RPC,
|
|
232
|
+
// CGEvent, performClick animation, await, timer or queued coordinate dispatch.
|
|
233
|
+
committing = true
|
|
234
|
+
defer { committing = false }
|
|
235
|
+
uncertain = true; poisoned = true
|
|
236
|
+
// NSApplication.sendAction is overridable and may reroute/queue or report
|
|
237
|
+
// success without invoking this method. Call the checked, retained IMP.
|
|
238
|
+
let invoke = unsafeBitCast(b.implementation, to: NativeAction.self)
|
|
239
|
+
invoke(b.target, b.selector, b.button)
|
|
240
|
+
revision = committedRevision; lastFingerprint = nil
|
|
241
|
+
// A receipt attests local handler dispatch, never remote payment/mail completion.
|
|
242
|
+
poisoned = false; uncertain = false
|
|
243
|
+
return ["protocol": wardenAtomicProtocol, "status": "committed", "revisionBefore": approved.revision, "revisionAfter": revision, "stamp": stamp,
|
|
244
|
+
"result": ["content": [["type": "text", "text": "Native AppKit action dispatched synchronously to the approved button target. External outcomes require reconciliation."]]]]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
/// Bounded NDJSON transport over an exclusive inherited pipe, not a public listener.
|
|
4
|
+
/// Parsing and every surface operation run on the application's main actor.
|
|
5
|
+
@MainActor public final class WardenDesktopStandardIO {
|
|
6
|
+
private let surface: WardenDesktopSurface
|
|
7
|
+
private var initialized = false
|
|
8
|
+
private var stopped = false
|
|
9
|
+
public init(surface: WardenDesktopSurface) { self.surface = surface }
|
|
10
|
+
#if WARDEN_DESKTOP_TESTS
|
|
11
|
+
public var extraRequest: ((String, [String: Any]) throws -> [String: Any]?)?
|
|
12
|
+
#endif
|
|
13
|
+
private func send(_ value: [String: Any]) {
|
|
14
|
+
do {
|
|
15
|
+
let data = try JSONSerialization.data(withJSONObject: value, options: [.withoutEscapingSlashes, .sortedKeys])
|
|
16
|
+
guard data.count <= 1_048_576 else { throw DesktopError.invalid }
|
|
17
|
+
try FileHandle.standardOutput.write(contentsOf: data + Data([10]))
|
|
18
|
+
} catch { stopped = true; NSApp.terminate(nil) }
|
|
19
|
+
}
|
|
20
|
+
public func receive(_ data: Data) {
|
|
21
|
+
guard !stopped else { return }
|
|
22
|
+
var id: Any = NSNull()
|
|
23
|
+
do {
|
|
24
|
+
let raw = try DesktopJSON.parse(data)
|
|
25
|
+
guard let msg = raw as? [String: Any], Set(msg.keys).isSubset(of: ["jsonrpc", "id", "method", "params"]), msg["jsonrpc"] as? String == "2.0",
|
|
26
|
+
let method = msg["method"] as? String else { throw DesktopError.invalid }
|
|
27
|
+
if let v = msg["id"] {
|
|
28
|
+
guard v is String || (v is NSNumber && CFGetTypeID(v as! NSNumber) != CFBooleanGetTypeID() && (v as! NSNumber).doubleValue.isFinite && abs((v as! NSNumber).doubleValue) <= 9_007_199_254_740_991 && (v as! NSNumber).doubleValue.rounded() == (v as! NSNumber).doubleValue) else { throw DesktopError.invalid }
|
|
29
|
+
id = v
|
|
30
|
+
}
|
|
31
|
+
let params: [String: Any]
|
|
32
|
+
if let p = msg["params"] { guard let object = p as? [String: Any] else { throw DesktopError.invalid }; params = object } else { params = [:] }
|
|
33
|
+
if method == "notifications/initialized" || method == "notifications/cancelled" { return }
|
|
34
|
+
if method == "warden/screen/cancel" { try surface.cancel(params); if msg["id"] != nil { send(["jsonrpc": "2.0", "id": id, "result": [:]]) }; return }
|
|
35
|
+
guard msg["id"] != nil else { return }
|
|
36
|
+
let result: [String: Any]
|
|
37
|
+
if method == "initialize" {
|
|
38
|
+
guard !initialized else { throw DesktopError.invalid }; initialized = true
|
|
39
|
+
result = ["protocolVersion": "2025-06-18", "capabilities": ["tools": [:]], "serverInfo": ["name": "warden-macos-owned-surface", "version": "1.0.0"]]
|
|
40
|
+
} else {
|
|
41
|
+
guard initialized else { throw DesktopError.invalid }
|
|
42
|
+
let extra: [String: Any]?
|
|
43
|
+
#if WARDEN_DESKTOP_TESTS
|
|
44
|
+
extra = try extraRequest?(method, params)
|
|
45
|
+
#else
|
|
46
|
+
extra = nil
|
|
47
|
+
#endif
|
|
48
|
+
if let extra = extra { result = extra }
|
|
49
|
+
else if method == "tools/list" {
|
|
50
|
+
result = ["tools": [
|
|
51
|
+
["name": "screenshot", "description": "Observe registered native controls in the connected application.", "inputSchema": ["type": "object", "properties": [:], "additionalProperties": false]],
|
|
52
|
+
["name": "click", "description": "Activate a registered native button through an atomic target-side commit.", "inputSchema": ["type": "object", "properties": ["index": ["type": "string"]], "required": ["index"], "additionalProperties": false]],
|
|
53
|
+
]]
|
|
54
|
+
} else if method == "tools/call" {
|
|
55
|
+
guard Set(params.keys).isSubset(of: ["name", "arguments"]), params["name"] as? String == "screenshot" else { throw DesktopError.unsupported }
|
|
56
|
+
if let a = params["arguments"] { guard let args = a as? [String: Any], args.isEmpty else { throw DesktopError.invalid } }
|
|
57
|
+
result = try surface.read()
|
|
58
|
+
} else if method == "warden/screen/prepare" { result = try surface.prepare(params) }
|
|
59
|
+
else if method == "warden/screen/commit" { result = try surface.commit(params) }
|
|
60
|
+
else if method == "ping" { result = [:] }
|
|
61
|
+
else { throw DesktopError.unsupported }
|
|
62
|
+
}
|
|
63
|
+
send(["jsonrpc": "2.0", "id": id, "result": result])
|
|
64
|
+
} catch {
|
|
65
|
+
// No raw UI text, arguments, tickets, paths or arbitrary backend errors in diagnostics.
|
|
66
|
+
send(["jsonrpc": "2.0", "id": id, "error": ["code": -32041, "message": "Native desktop refused or outcome is uncertain; do not retry automatically."]])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
public func start() {
|
|
70
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
71
|
+
var buffer = Data()
|
|
72
|
+
let pending = DispatchSemaphore(value: 32)
|
|
73
|
+
while true {
|
|
74
|
+
let chunk = FileHandle.standardInput.availableData
|
|
75
|
+
if chunk.isEmpty { DispatchQueue.main.async { NSApp.terminate(nil) }; return }
|
|
76
|
+
for byte in chunk {
|
|
77
|
+
if byte == 10 {
|
|
78
|
+
let line = buffer; buffer.removeAll(keepingCapacity: true)
|
|
79
|
+
if !line.isEmpty { pending.wait(); DispatchQueue.main.async { defer { pending.signal() }; self.receive(line) } }
|
|
80
|
+
} else {
|
|
81
|
+
buffer.append(byte)
|
|
82
|
+
if buffer.count > 65_536 { DispatchQueue.main.async { NSApp.terminate(nil) }; return }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Audit duplicate JSON keys before Foundation parsing (including escaped aliases).
|
|
91
|
+
/// Depth and wire limits apply before recursively constructing a Foundation object.
|
|
92
|
+
public enum DesktopJSON {
|
|
93
|
+
public static func parse(_ data: Data) throws -> Any {
|
|
94
|
+
guard data.count <= 65_536, String(data: data, encoding: .utf8) != nil else { throw DesktopError.invalid }
|
|
95
|
+
var scan = Scanner(bytes: Array(data)); try scan.value(0); scan.whitespace()
|
|
96
|
+
guard scan.offset == data.count else { throw DesktopError.invalid }
|
|
97
|
+
return try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
|
|
98
|
+
}
|
|
99
|
+
private struct Scanner {
|
|
100
|
+
let bytes: [UInt8]; var offset = 0
|
|
101
|
+
mutating func whitespace() { while offset < bytes.count && [9, 10, 13, 32].contains(bytes[offset]) { offset += 1 } }
|
|
102
|
+
mutating func take(_ b: UInt8) throws { whitespace(); guard offset < bytes.count, bytes[offset] == b else { throw DesktopError.invalid }; offset += 1 }
|
|
103
|
+
mutating func string() throws -> String {
|
|
104
|
+
try take(34); let start = offset - 1
|
|
105
|
+
while offset < bytes.count {
|
|
106
|
+
let b = bytes[offset]; offset += 1
|
|
107
|
+
if b == 92 { guard offset < bytes.count else { throw DesktopError.invalid }; offset += 1 }
|
|
108
|
+
else if b == 34 {
|
|
109
|
+
guard let text = try JSONSerialization.jsonObject(with: Data(bytes[start..<offset]), options: [.fragmentsAllowed]) as? String else { throw DesktopError.invalid }; return text
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw DesktopError.invalid
|
|
113
|
+
}
|
|
114
|
+
mutating func value(_ depth: Int) throws {
|
|
115
|
+
guard depth <= 32 else { throw DesktopError.invalid }; whitespace(); guard offset < bytes.count else { throw DesktopError.invalid }
|
|
116
|
+
switch bytes[offset] {
|
|
117
|
+
case 123:
|
|
118
|
+
offset += 1; whitespace(); if offset < bytes.count && bytes[offset] == 125 { offset += 1; return }
|
|
119
|
+
var keys = Set<String>()
|
|
120
|
+
while true {
|
|
121
|
+
let key = try string(); guard keys.insert(key).inserted else { throw DesktopError.invalid }
|
|
122
|
+
try take(58); try value(depth + 1); whitespace(); guard offset < bytes.count else { throw DesktopError.invalid }
|
|
123
|
+
if bytes[offset] == 125 { offset += 1; return }; try take(44)
|
|
124
|
+
}
|
|
125
|
+
case 91:
|
|
126
|
+
offset += 1; whitespace(); if offset < bytes.count && bytes[offset] == 93 { offset += 1; return }
|
|
127
|
+
while true {
|
|
128
|
+
try value(depth + 1); whitespace(); guard offset < bytes.count else { throw DesktopError.invalid }
|
|
129
|
+
if bytes[offset] == 93 { offset += 1; return }; try take(44)
|
|
130
|
+
}
|
|
131
|
+
case 34: _ = try string()
|
|
132
|
+
default:
|
|
133
|
+
let start = offset
|
|
134
|
+
while offset < bytes.count && ![9, 10, 13, 32, 44, 93, 125].contains(bytes[offset]) { offset += 1 }
|
|
135
|
+
guard offset > start else { throw DesktopError.invalid }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
#if canImport(WardenDesktop)
|
|
3
|
+
import WardenDesktop
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
#if WARDEN_DESKTOP_TESTS
|
|
7
|
+
@MainActor final class TestApplication: NSApplication {
|
|
8
|
+
var interceptActions = false
|
|
9
|
+
var interceptedActions = 0
|
|
10
|
+
override func sendAction(_ action: Selector, to target: Any?, from sender: Any?) -> Bool {
|
|
11
|
+
if interceptActions { interceptedActions += 1; return true }
|
|
12
|
+
return super.sendAction(action, to: target, from: sender)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
@MainActor final class RaceButton: NSButton {
|
|
16
|
+
var onHit: (() -> Void)?
|
|
17
|
+
override func hitTest(_ point: NSPoint) -> NSView? { onHit?(); return super.hitTest(point) }
|
|
18
|
+
}
|
|
19
|
+
#endif
|
|
20
|
+
|
|
21
|
+
@MainActor final class DemoOwner: NSObject {
|
|
22
|
+
var count = 0
|
|
23
|
+
var buttons: [NSButton] = []
|
|
24
|
+
var window: NSWindow!
|
|
25
|
+
var surface: WardenDesktopSurface!
|
|
26
|
+
var clockOffset = 0.0
|
|
27
|
+
var uptimeOffset: UInt64 = 0
|
|
28
|
+
#if WARDEN_DESKTOP_TESTS
|
|
29
|
+
var clockReads = 0
|
|
30
|
+
var invalidateOnClockRead = 0
|
|
31
|
+
var clockRaceTriggered = false
|
|
32
|
+
var auxiliaryWindows: [NSWindow] = []
|
|
33
|
+
var nestedCommit: [String: Any]?
|
|
34
|
+
var reentrantRefused = false
|
|
35
|
+
#endif
|
|
36
|
+
var raceBeforeCommit = false
|
|
37
|
+
var hitRaceTriggered = false
|
|
38
|
+
@objc func pressed(_ sender: NSButton) {
|
|
39
|
+
precondition(Thread.isMainThread)
|
|
40
|
+
count += 1
|
|
41
|
+
#if WARDEN_DESKTOP_TESTS
|
|
42
|
+
if let params = nestedCommit {
|
|
43
|
+
nestedCommit = nil
|
|
44
|
+
let receipt = try? surface.commit(params)
|
|
45
|
+
var readRefused = false, invalidateRefused = false
|
|
46
|
+
do { _ = try surface.read() } catch { readRefused = true }
|
|
47
|
+
do { try surface.invalidate() } catch { invalidateRefused = true }
|
|
48
|
+
reentrantRefused = receipt?["status"] as? String == "refused" && readRefused && invalidateRefused
|
|
49
|
+
}
|
|
50
|
+
#endif
|
|
51
|
+
sender.title = "Pressed \(count)"
|
|
52
|
+
}
|
|
53
|
+
@objc func alternate(_ sender: NSButton) { count += 100 }
|
|
54
|
+
#if WARDEN_DESKTOP_TESTS
|
|
55
|
+
@objc func invalidAction(_ sender: NSButton) -> String { "not a void action" }
|
|
56
|
+
@objc func blockAction(_ sender: @convention(block) () -> Void) { count += 1000 }
|
|
57
|
+
#endif
|
|
58
|
+
func monotonicClock() -> UInt64 {
|
|
59
|
+
#if WARDEN_DESKTOP_TESTS
|
|
60
|
+
if invalidateOnClockRead > 0 {
|
|
61
|
+
clockReads += 1
|
|
62
|
+
if clockReads == invalidateOnClockRead {
|
|
63
|
+
clockRaceTriggered = true
|
|
64
|
+
try? surface.invalidate(); buttons[0].tag += 1
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
#endif
|
|
68
|
+
return DispatchTime.now().uptimeNanoseconds + uptimeOffset
|
|
69
|
+
}
|
|
70
|
+
func start() throws {
|
|
71
|
+
window = NSWindow(contentRect: NSRect(x: 40, y: 80, width: 340, height: 160), styleMask: [.titled, .closable], backing: .buffered, defer: false)
|
|
72
|
+
window.title = "WARDEN native test — local only"
|
|
73
|
+
window.isReleasedWhenClosed = false
|
|
74
|
+
window.isRestorable = false
|
|
75
|
+
#if WARDEN_DESKTOP_TESTS
|
|
76
|
+
let button = RaceButton(title: "Local action", target: self, action: #selector(pressed(_:)))
|
|
77
|
+
#else
|
|
78
|
+
let button = NSButton(title: "Local action", target: self, action: #selector(pressed(_:)))
|
|
79
|
+
#endif
|
|
80
|
+
button.setAccessibilityElement(true); button.setAccessibilityRole(.button)
|
|
81
|
+
button.setButtonType(.momentaryPushIn); button.setAccessibilityIdentifier("warden.demo.local-action")
|
|
82
|
+
button.frame = NSRect(x: 30, y: 55, width: 180, height: 36)
|
|
83
|
+
window.contentView!.addSubview(button); buttons = [button]
|
|
84
|
+
window.orderFront(nil) // Never activate another app or generate global input.
|
|
85
|
+
surface = try WardenDesktopSurface(window: window, applicationID: "dev.warden.macos.demo", now: { [weak self] in Date().timeIntervalSince1970 * 1000 + (self?.clockOffset ?? 0) }, uptime: { [weak self] in self?.monotonicClock() ?? DispatchTime.now().uptimeNanoseconds })
|
|
86
|
+
try surface.register(id: "3", button: button, target: self, selector: #selector(pressed(_:)))
|
|
87
|
+
}
|
|
88
|
+
#if WARDEN_DESKTOP_TESTS
|
|
89
|
+
func presentSheet() -> [String: Any] {
|
|
90
|
+
let sheet = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 200, height: 80), styleMask: [.titled], backing: .buffered, defer: false)
|
|
91
|
+
sheet.isReleasedWhenClosed = false; sheet.isRestorable = false
|
|
92
|
+
auxiliaryWindows.append(sheet); window.beginSheet(sheet)
|
|
93
|
+
return ["sheets": window.sheets.count, "attached": window.attachedSheet != nil, "logicalParent": sheet.sheetParent != nil, "knownSheet": NSApp.windows.contains(where: { $0 === sheet })]
|
|
94
|
+
}
|
|
95
|
+
func testRequest(_ method: String, _ params: [String: Any]) throws -> [String: Any]? {
|
|
96
|
+
if method == "test/clock-race" {
|
|
97
|
+
clockReads = 0; invalidateOnClockRead = params["at"] as? Int ?? 2
|
|
98
|
+
return [:]
|
|
99
|
+
}
|
|
100
|
+
if method == "test/clock-stats" { return ["triggered": clockRaceTriggered] }
|
|
101
|
+
if method == "test/intercept-dispatch" { (NSApp as! TestApplication).interceptActions = true; return [:] }
|
|
102
|
+
if method == "test/dispatch-stats" { return ["intercepted": (NSApp as! TestApplication).interceptedActions] }
|
|
103
|
+
if method == "test/arm-reentrant" { nestedCommit = params; return [:] }
|
|
104
|
+
if method == "test/invalid-registration" {
|
|
105
|
+
let button = NSButton(title: "Not registered", target: self, action: #selector(invalidAction(_:)))
|
|
106
|
+
if params["kind"] as? String == "block" { button.action = #selector(blockAction(_:)) }
|
|
107
|
+
button.setAccessibilityElement(true); button.setAccessibilityRole(.button); button.setButtonType(.momentaryPushIn)
|
|
108
|
+
button.setAccessibilityIdentifier("warden.demo.invalid-signature"); button.frame = NSRect(x: 230, y: 10, width: 80, height: 30); window.contentView!.addSubview(button)
|
|
109
|
+
defer { button.removeFromSuperview() }
|
|
110
|
+
do { try surface.register(id: "4", button: button, target: self, selector: button.action!); return ["refused": false] }
|
|
111
|
+
catch { return ["refused": true] }
|
|
112
|
+
}
|
|
113
|
+
if method == "warden/screen/commit" && raceBeforeCommit { try surface.invalidate(); raceBeforeCommit = false; return try surface.commit(params) }
|
|
114
|
+
if method == "test/stats" { return ["count": count, "mainThread": Thread.isMainThread, "windowVisible": window.isVisible, "hitRaceTriggered": hitRaceTriggered, "reentrantRefused": reentrantRefused, "pid": ProcessInfo.processInfo.processIdentifier] }
|
|
115
|
+
guard method == "test/change", let kind = params["kind"] as? String else { return nil }
|
|
116
|
+
if kind == "race-before-commit" { raceBeforeCommit = true; return [:] }
|
|
117
|
+
if kind == "monotonic-expiry" { uptimeOffset += 300_001_000_000; return [:] }
|
|
118
|
+
if kind == "expiry" { clockOffset += 300_001; return [:] }
|
|
119
|
+
if kind == "rollback" { clockOffset -= 1000; return [:] }
|
|
120
|
+
if kind == "untracked-sheet" { return presentSheet() }
|
|
121
|
+
if kind == "untracked-ancestor-alpha" { window.contentView!.alphaValue = 0; return [:] }
|
|
122
|
+
if kind == "untracked-partial-alpha" { buttons[0].alphaValue = 0.5; return [:] }
|
|
123
|
+
if kind == "untracked-implementation" {
|
|
124
|
+
guard let a = class_getInstanceMethod(DemoOwner.self, #selector(pressed(_:))), let b = class_getInstanceMethod(DemoOwner.self, #selector(alternate(_:))) else { throw DesktopError.invalid }
|
|
125
|
+
method_exchangeImplementations(a, b); return [:]
|
|
126
|
+
}
|
|
127
|
+
if kind == "untracked-button-alpha" { buttons[0].alphaValue = 0; return [:] }
|
|
128
|
+
if kind == "untracked-window-alpha" { window.alphaValue = 0; return [:] }
|
|
129
|
+
if kind == "untracked-title" { buttons[0].title = "Untracked change"; return [:] }
|
|
130
|
+
if kind.hasPrefix("hit-untracked-") {
|
|
131
|
+
(buttons[0] as! RaceButton).onHit = { [weak self] in
|
|
132
|
+
guard let s = self, !s.hitRaceTriggered else { return }
|
|
133
|
+
s.hitRaceTriggered = true
|
|
134
|
+
switch kind {
|
|
135
|
+
case "hit-untracked-sheet": _ = s.presentSheet()
|
|
136
|
+
case "hit-untracked-button-alpha": s.buttons[0].alphaValue = 0
|
|
137
|
+
case "hit-untracked-window-alpha": s.window.alphaValue = 0
|
|
138
|
+
case "hit-untracked-handler": s.buttons[0].action = #selector(DemoOwner.alternate(_:))
|
|
139
|
+
case "hit-untracked-title": s.buttons[0].title = "Different outcome"
|
|
140
|
+
case "hit-untracked-disabled": s.buttons[0].isEnabled = false
|
|
141
|
+
case "hit-untracked-bounds": s.buttons[0].frame.origin.x += 5
|
|
142
|
+
case "hit-untracked-expiry": s.clockOffset += 300_001
|
|
143
|
+
case "hit-untracked-monotonic-expiry": s.uptimeOffset += 300_001_000_000
|
|
144
|
+
default: break
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [:]
|
|
148
|
+
}
|
|
149
|
+
if kind == "hit-test-mutation" {
|
|
150
|
+
(buttons[0] as! RaceButton).onHit = { [weak self] in
|
|
151
|
+
guard let s = self, !s.hitRaceTriggered else { return }
|
|
152
|
+
s.hitRaceTriggered = true; try? s.surface.invalidate(); s.buttons[0].tag += 1
|
|
153
|
+
}
|
|
154
|
+
return [:]
|
|
155
|
+
}
|
|
156
|
+
if kind == "replace-object" {
|
|
157
|
+
try surface.invalidate()
|
|
158
|
+
let old = buttons[0], replacement = NSButton(title: old.title, target: self, action: #selector(pressed(_:)))
|
|
159
|
+
replacement.setAccessibilityElement(true); replacement.setAccessibilityRole(.button)
|
|
160
|
+
replacement.setButtonType(.momentaryPushIn); replacement.frame = old.frame; replacement.setAccessibilityIdentifier(old.accessibilityIdentifier())
|
|
161
|
+
old.removeFromSuperview(); window.contentView!.addSubview(replacement); return [:]
|
|
162
|
+
}
|
|
163
|
+
try surface.invalidate() // BEFORE every controlled mutation, including ABA/semantics.
|
|
164
|
+
switch kind {
|
|
165
|
+
case "aba", "semantic": break
|
|
166
|
+
case "title": buttons[0].title = "Other action"
|
|
167
|
+
case "unicode": buttons[0].title = "Отправить / 发送 / Envoyer"; window.title = "WARDEN — проверка 发送"
|
|
168
|
+
case "identity": buttons[0].setAccessibilityIdentifier("warden.demo.changed")
|
|
169
|
+
case "handler": buttons[0].action = #selector(alternate(_:))
|
|
170
|
+
case "bounds": buttons[0].frame.origin.x += 5
|
|
171
|
+
case "window": window.title = "Changed native window"
|
|
172
|
+
case "disabled": buttons[0].isEnabled = false
|
|
173
|
+
case "hidden": buttons[0].isHidden = true
|
|
174
|
+
case "close": window.orderOut(nil)
|
|
175
|
+
case "overlay":
|
|
176
|
+
let overlay = NSView(frame: buttons[0].frame); window.contentView!.addSubview(overlay, positioned: .above, relativeTo: buttons[0])
|
|
177
|
+
default: throw DesktopError.invalid
|
|
178
|
+
}
|
|
179
|
+
return [:]
|
|
180
|
+
}
|
|
181
|
+
#endif
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@main struct WardenDesktopHost {
|
|
185
|
+
@MainActor static func main() {
|
|
186
|
+
// A native reference application, not an agent injected into unrelated applications.
|
|
187
|
+
// The reusable library must be embedded by those applications themselves.
|
|
188
|
+
guard Array(CommandLine.arguments.dropFirst()) == ["--demo"] else {
|
|
189
|
+
FileHandle.standardError.write(Data("Usage: warden-desktop --demo (owned native window only; no external AX/CGEvent dispatch)\n".utf8)); exit(2)
|
|
190
|
+
}
|
|
191
|
+
#if WARDEN_DESKTOP_TESTS
|
|
192
|
+
_ = TestApplication.shared
|
|
193
|
+
precondition(NSApp is TestApplication)
|
|
194
|
+
#else
|
|
195
|
+
_ = NSApplication.shared
|
|
196
|
+
#endif
|
|
197
|
+
NSApp.setActivationPolicy(.accessory)
|
|
198
|
+
let owner = DemoOwner()
|
|
199
|
+
do {
|
|
200
|
+
try owner.start()
|
|
201
|
+
let io = WardenDesktopStandardIO(surface: owner.surface)
|
|
202
|
+
#if WARDEN_DESKTOP_TESTS
|
|
203
|
+
io.extraRequest = { method, params in try owner.testRequest(method, params) }
|
|
204
|
+
#endif
|
|
205
|
+
io.start()
|
|
206
|
+
withExtendedLifetime([owner, io] as [AnyObject]) { NSApp.run() }
|
|
207
|
+
} catch {
|
|
208
|
+
FileHandle.standardError.write(Data("Native desktop unavailable; no action was run.\n".utf8)); exit(3)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|