@aimarket/warden 0.20.0 → 0.22.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 (160) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README-es.md +27 -11
  3. package/README-fr.md +27 -11
  4. package/README-ru.md +27 -11
  5. package/README-zh.md +27 -11
  6. package/README.md +27 -11
  7. package/dist/annotation-review.d.ts +10 -0
  8. package/dist/annotation-review.d.ts.map +1 -0
  9. package/dist/annotation-review.js +36 -0
  10. package/dist/annotation-review.js.map +1 -0
  11. package/dist/atomic-screen.d.ts +61 -0
  12. package/dist/atomic-screen.d.ts.map +1 -0
  13. package/dist/atomic-screen.js +165 -0
  14. package/dist/atomic-screen.js.map +1 -0
  15. package/dist/flow-guard-answer.d.ts +29 -0
  16. package/dist/flow-guard-answer.d.ts.map +1 -0
  17. package/dist/flow-guard-answer.js +201 -0
  18. package/dist/flow-guard-answer.js.map +1 -0
  19. package/dist/flow-guard-classify.d.ts +14 -0
  20. package/dist/flow-guard-classify.d.ts.map +1 -0
  21. package/dist/flow-guard-classify.js +224 -0
  22. package/dist/flow-guard-classify.js.map +1 -0
  23. package/dist/flow-guard-decode.d.ts +17 -0
  24. package/dist/flow-guard-decode.d.ts.map +1 -0
  25. package/dist/flow-guard-decode.js +149 -0
  26. package/dist/flow-guard-decode.js.map +1 -0
  27. package/dist/flow-guard-destinations.d.ts +23 -0
  28. package/dist/flow-guard-destinations.d.ts.map +1 -0
  29. package/dist/flow-guard-destinations.js +158 -0
  30. package/dist/flow-guard-destinations.js.map +1 -0
  31. package/dist/flow-guard-fingerprints.d.ts +27 -0
  32. package/dist/flow-guard-fingerprints.d.ts.map +1 -0
  33. package/dist/flow-guard-fingerprints.js +164 -0
  34. package/dist/flow-guard-fingerprints.js.map +1 -0
  35. package/dist/flow-guard-fragments.d.ts +17 -0
  36. package/dist/flow-guard-fragments.d.ts.map +1 -0
  37. package/dist/flow-guard-fragments.js +122 -0
  38. package/dist/flow-guard-fragments.js.map +1 -0
  39. package/dist/flow-guard-instructions.d.ts +12 -0
  40. package/dist/flow-guard-instructions.d.ts.map +1 -0
  41. package/dist/flow-guard-instructions.js +36 -0
  42. package/dist/flow-guard-instructions.js.map +1 -0
  43. package/dist/flow-guard-labels.d.ts +10 -0
  44. package/dist/flow-guard-labels.d.ts.map +1 -0
  45. package/dist/flow-guard-labels.js +55 -0
  46. package/dist/flow-guard-labels.js.map +1 -0
  47. package/dist/flow-guard-leak.d.ts +29 -0
  48. package/dist/flow-guard-leak.d.ts.map +1 -0
  49. package/dist/flow-guard-leak.js +135 -0
  50. package/dist/flow-guard-leak.js.map +1 -0
  51. package/dist/flow-guard-paths.d.ts +21 -0
  52. package/dist/flow-guard-paths.d.ts.map +1 -0
  53. package/dist/flow-guard-paths.js +72 -0
  54. package/dist/flow-guard-paths.js.map +1 -0
  55. package/dist/flow-guard-policy.d.ts +15 -0
  56. package/dist/flow-guard-policy.d.ts.map +1 -0
  57. package/dist/flow-guard-policy.js +179 -0
  58. package/dist/flow-guard-policy.js.map +1 -0
  59. package/dist/flow-guard-post.d.ts +6 -0
  60. package/dist/flow-guard-post.d.ts.map +1 -0
  61. package/dist/flow-guard-post.js +131 -0
  62. package/dist/flow-guard-post.js.map +1 -0
  63. package/dist/flow-guard-pre.d.ts +13 -0
  64. package/dist/flow-guard-pre.d.ts.map +1 -0
  65. package/dist/flow-guard-pre.js +335 -0
  66. package/dist/flow-guard-pre.js.map +1 -0
  67. package/dist/flow-guard-screen.d.ts +85 -0
  68. package/dist/flow-guard-screen.d.ts.map +1 -0
  69. package/dist/flow-guard-screen.js +558 -0
  70. package/dist/flow-guard-screen.js.map +1 -0
  71. package/dist/flow-guard-state.d.ts +12 -0
  72. package/dist/flow-guard-state.d.ts.map +1 -0
  73. package/dist/flow-guard-state.js +121 -0
  74. package/dist/flow-guard-state.js.map +1 -0
  75. package/dist/flow-guard-text.d.ts +9 -0
  76. package/dist/flow-guard-text.d.ts.map +1 -0
  77. package/dist/flow-guard-text.js +46 -0
  78. package/dist/flow-guard-text.js.map +1 -0
  79. package/dist/flow-guard-types.d.ts +154 -0
  80. package/dist/flow-guard-types.d.ts.map +1 -0
  81. package/dist/flow-guard-types.js +2 -0
  82. package/dist/flow-guard-types.js.map +1 -0
  83. package/dist/flow-guard.d.ts +20 -215
  84. package/dist/flow-guard.d.ts.map +1 -1
  85. package/dist/flow-guard.js +26 -1400
  86. package/dist/flow-guard.js.map +1 -1
  87. package/dist/mcp-client.d.ts +1 -2
  88. package/dist/mcp-client.d.ts.map +1 -1
  89. package/dist/mcp-client.js +2 -19
  90. package/dist/mcp-client.js.map +1 -1
  91. package/dist/mcp-server.js +5 -0
  92. package/dist/mcp-server.js.map +1 -1
  93. package/dist/mcp-tools.d.ts.map +1 -1
  94. package/dist/mcp-tools.js +46 -9
  95. package/dist/mcp-tools.js.map +1 -1
  96. package/dist/public-address.d.ts +3 -0
  97. package/dist/public-address.d.ts.map +1 -0
  98. package/dist/public-address.js +58 -0
  99. package/dist/public-address.js.map +1 -0
  100. package/dist/review-diff.d.ts +36 -0
  101. package/dist/review-diff.d.ts.map +1 -1
  102. package/dist/review-diff.js +177 -7
  103. package/dist/review-diff.js.map +1 -1
  104. package/dist/task-adapter.d.ts +73 -0
  105. package/dist/task-adapter.d.ts.map +1 -0
  106. package/dist/task-adapter.js +379 -0
  107. package/dist/task-adapter.js.map +1 -0
  108. package/dist/task-cli.d.ts +3 -0
  109. package/dist/task-cli.d.ts.map +1 -0
  110. package/dist/task-cli.js +162 -0
  111. package/dist/task-cli.js.map +1 -0
  112. package/dist/types.d.ts +9 -0
  113. package/dist/types.d.ts.map +1 -1
  114. package/dist/wrap-cli.d.ts +5 -0
  115. package/dist/wrap-cli.d.ts.map +1 -1
  116. package/dist/wrap-cli.js +29 -2
  117. package/dist/wrap-cli.js.map +1 -1
  118. package/dist/wrap-screen.d.ts +3 -0
  119. package/dist/wrap-screen.d.ts.map +1 -0
  120. package/dist/wrap-screen.js +20 -0
  121. package/dist/wrap-screen.js.map +1 -0
  122. package/dist/wrap-state.d.ts +8 -1
  123. package/dist/wrap-state.d.ts.map +1 -1
  124. package/dist/wrap-state.js +21 -4
  125. package/dist/wrap-state.js.map +1 -1
  126. package/dist/wrap.d.ts.map +1 -1
  127. package/dist/wrap.js +204 -10
  128. package/dist/wrap.js.map +1 -1
  129. package/docs/flow-guard-model-comparison-2026-10-25.es.md +82 -0
  130. package/docs/flow-guard-model-comparison-2026-10-25.fr.md +82 -0
  131. package/docs/flow-guard-model-comparison-2026-10-25.md +82 -0
  132. package/docs/flow-guard-model-comparison-2026-10-25.ru.md +82 -0
  133. package/docs/flow-guard-model-comparison-2026-10-25.zh.md +82 -0
  134. package/docs/flow-guard.es.md +307 -12
  135. package/docs/flow-guard.fr.md +307 -12
  136. package/docs/flow-guard.md +315 -13
  137. package/docs/flow-guard.ru.md +314 -13
  138. package/docs/flow-guard.zh.md +306 -8
  139. package/docs/scan-limits.es.md +39 -0
  140. package/docs/scan-limits.fr.md +39 -0
  141. package/docs/scan-limits.md +39 -0
  142. package/docs/scan-limits.ru.md +39 -0
  143. package/docs/scan-limits.zh.md +39 -0
  144. package/docs/scan.es.md +2 -0
  145. package/docs/scan.fr.md +2 -0
  146. package/docs/scan.md +2 -0
  147. package/docs/scan.ru.md +2 -0
  148. package/docs/scan.zh.md +2 -0
  149. package/docs/security-hardening.es.md +1 -1
  150. package/docs/security-hardening.fr.md +1 -1
  151. package/docs/security-hardening.md +1 -1
  152. package/docs/security-hardening.ru.md +1 -1
  153. package/docs/security-hardening.zh.md +1 -1
  154. package/examples/atomic-screen-server.mjs +42 -0
  155. package/native/macos/Package.swift +9 -0
  156. package/native/macos/Sources/WardenDesktop/NativeSurface.swift +246 -0
  157. package/native/macos/Sources/WardenDesktop/StandardIO.swift +139 -0
  158. package/native/macos/Sources/WardenDesktopHost/Main.swift +211 -0
  159. package/package.json +18 -6
  160. package/scripts/build-macos.mjs +22 -0
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimarket/warden",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "WARDEN — MCP security firewall. Stdio wrap proxy, inspection tools and library. Vets definitions before model exposure. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,11 @@
29
29
  "types": "./dist/jcs.d.ts",
30
30
  "import": "./dist/jcs.js",
31
31
  "default": "./dist/jcs.js"
32
+ },
33
+ "./atomic-screen": {
34
+ "types": "./dist/atomic-screen.d.ts",
35
+ "import": "./dist/atomic-screen.js",
36
+ "default": "./dist/atomic-screen.js"
32
37
  }
33
38
  },
34
39
  "files": [
@@ -45,7 +50,11 @@
45
50
  "LICENSE",
46
51
  "docs/security-hardening*.md",
47
52
  "docs/gates*.md",
48
- "docs/flow-guard*.md"
53
+ "docs/flow-guard*.md",
54
+ "examples/atomic-screen-server.mjs",
55
+ "native/macos/Package.swift",
56
+ "native/macos/Sources",
57
+ "scripts/build-macos.mjs"
49
58
  ],
50
59
  "engines": {
51
60
  "node": ">=20"
@@ -57,12 +66,15 @@
57
66
  "prepare": "npm run build",
58
67
  "typecheck": "tsc -p tsconfig.json --noEmit",
59
68
  "test": "vitest run",
69
+ "test:redteam": "node scripts/redteam/flow-redteam.mjs --check",
60
70
  "test:coverage": "vitest run --coverage",
61
71
  "test:watch": "vitest",
62
- "prepublishOnly": "npm run build && npm test && npm run check:ruleset && npm run check:quality",
72
+ "prepublishOnly": "npm run build && npm test && npm run test:redteam && npm run check:ruleset && npm run check:quality",
63
73
  "check:quality": "node scripts/check-quality-gate.mjs",
64
74
  "check:ruleset": "node scripts/check-published-ruleset.mjs",
65
- "bench:wrap": "npm run build && node scripts/benchmark-wrap.mjs"
75
+ "bench:wrap": "npm run build && node scripts/benchmark-wrap.mjs",
76
+ "build:macos": "node scripts/build-macos.mjs",
77
+ "test:macos": "node scripts/build-macos.mjs && node scripts/build-macos.mjs --tests && npm run build && vitest run --config vitest.macos.config.ts"
66
78
  },
67
79
  "keywords": [
68
80
  "mcp",
@@ -84,8 +96,8 @@
84
96
  },
85
97
  "devDependencies": {
86
98
  "@types/node": "^26.6.4",
87
- "@vitest/coverage-v8": "^4.1.11",
99
+ "@vitest/coverage-v8": "^4.1.12",
88
100
  "typescript": "^5.7.2",
89
- "vitest": "^4.1.11"
101
+ "vitest": "^4.1.12"
90
102
  }
91
103
  }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { join } from 'node:path';
6
+
7
+ if (process.platform !== 'darwin') throw new Error('Native desktop build requires macOS and Xcode command-line tools');
8
+ if (process.argv.slice(2).some(x => x !== '--tests') || process.argv.slice(2).length > 1) throw new Error('Expected [--tests]');
9
+ const root = fileURLToPath(new URL('..', import.meta.url));
10
+ const build = join(root, 'native/macos/.build'); mkdirSync(build, { recursive: true });
11
+ const cache = join(build, 'module-cache'); mkdirSync(cache, { recursive: true });
12
+ {
13
+ if (!['arm64', 'x64'].includes(process.arch)) throw new Error('Unsupported macOS architecture');
14
+ const args = ['swiftc', '-swift-version', '5', '-parse-as-library', '-O', '-target', `${process.arch === 'arm64' ? 'arm64' : 'x86_64'}-apple-macos13.0`, '-module-cache-path', cache,
15
+ '-framework', 'AppKit', '-framework', 'CryptoKit', '-framework', 'Security',
16
+ ...['WardenDesktop/NativeSurface.swift', 'WardenDesktop/StandardIO.swift', 'WardenDesktopHost/Main.swift'].map(f => join(root, 'native/macos/Sources', f))];
17
+ const test = process.argv.includes('--tests');
18
+ if (test) args.push('-D', 'WARDEN_DESKTOP_TESTS');
19
+ const path = join(build, test ? 'warden-desktop-test' : 'warden-desktop');
20
+ execFileSync('/usr/bin/xcrun', [...args, '-o', path], { cwd: root, stdio: 'inherit', env: { ...process.env, CLANG_MODULE_CACHE_PATH: cache } });
21
+ process.stdout.write(path + '\n');
22
+ }