@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README-es.md +26 -11
  3. package/README-fr.md +26 -11
  4. package/README-ru.md +26 -11
  5. package/README-zh.md +26 -11
  6. package/README.md +26 -11
  7. package/dist/atomic-screen.d.ts +61 -0
  8. package/dist/atomic-screen.d.ts.map +1 -0
  9. package/dist/atomic-screen.js +165 -0
  10. package/dist/atomic-screen.js.map +1 -0
  11. package/dist/flow-guard.d.ts +181 -2
  12. package/dist/flow-guard.d.ts.map +1 -1
  13. package/dist/flow-guard.js +1194 -72
  14. package/dist/flow-guard.js.map +1 -1
  15. package/dist/mcp-server.js +5 -0
  16. package/dist/mcp-server.js.map +1 -1
  17. package/dist/mcp-tools.d.ts.map +1 -1
  18. package/dist/mcp-tools.js +46 -9
  19. package/dist/mcp-tools.js.map +1 -1
  20. package/dist/review-diff.d.ts +36 -0
  21. package/dist/review-diff.d.ts.map +1 -1
  22. package/dist/review-diff.js +179 -7
  23. package/dist/review-diff.js.map +1 -1
  24. package/dist/task-adapter.d.ts +73 -0
  25. package/dist/task-adapter.d.ts.map +1 -0
  26. package/dist/task-adapter.js +379 -0
  27. package/dist/task-adapter.js.map +1 -0
  28. package/dist/task-cli.d.ts +3 -0
  29. package/dist/task-cli.d.ts.map +1 -0
  30. package/dist/task-cli.js +162 -0
  31. package/dist/task-cli.js.map +1 -0
  32. package/dist/types.d.ts +9 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/wrap-cli.d.ts +5 -0
  35. package/dist/wrap-cli.d.ts.map +1 -1
  36. package/dist/wrap-cli.js +27 -1
  37. package/dist/wrap-cli.js.map +1 -1
  38. package/dist/wrap-screen.d.ts +3 -0
  39. package/dist/wrap-screen.d.ts.map +1 -0
  40. package/dist/wrap-screen.js +20 -0
  41. package/dist/wrap-screen.js.map +1 -0
  42. package/dist/wrap-state.d.ts +5 -0
  43. package/dist/wrap-state.d.ts.map +1 -1
  44. package/dist/wrap-state.js +3 -1
  45. package/dist/wrap-state.js.map +1 -1
  46. package/dist/wrap.d.ts.map +1 -1
  47. package/dist/wrap.js +161 -5
  48. package/dist/wrap.js.map +1 -1
  49. package/docs/flow-guard.es.md +274 -9
  50. package/docs/flow-guard.fr.md +274 -9
  51. package/docs/flow-guard.md +275 -10
  52. package/docs/flow-guard.ru.md +275 -10
  53. package/docs/flow-guard.zh.md +273 -5
  54. package/docs/security-hardening.es.md +1 -1
  55. package/docs/security-hardening.fr.md +1 -1
  56. package/docs/security-hardening.md +1 -1
  57. package/docs/security-hardening.ru.md +1 -1
  58. package/docs/security-hardening.zh.md +1 -1
  59. package/examples/atomic-screen-server.mjs +42 -0
  60. package/native/macos/Package.swift +9 -0
  61. package/native/macos/Sources/WardenDesktop/NativeSurface.swift +246 -0
  62. package/native/macos/Sources/WardenDesktop/StandardIO.swift +139 -0
  63. package/native/macos/Sources/WardenDesktopHost/Main.swift +211 -0
  64. package/package.json +17 -5
  65. 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
+ }