@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.
- package/CHANGELOG.md +113 -0
- package/README-es.md +27 -11
- package/README-fr.md +27 -11
- package/README-ru.md +27 -11
- package/README-zh.md +27 -11
- package/README.md +27 -11
- package/dist/annotation-review.d.ts +10 -0
- package/dist/annotation-review.d.ts.map +1 -0
- package/dist/annotation-review.js +36 -0
- package/dist/annotation-review.js.map +1 -0
- 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-answer.d.ts +29 -0
- package/dist/flow-guard-answer.d.ts.map +1 -0
- package/dist/flow-guard-answer.js +201 -0
- package/dist/flow-guard-answer.js.map +1 -0
- package/dist/flow-guard-classify.d.ts +14 -0
- package/dist/flow-guard-classify.d.ts.map +1 -0
- package/dist/flow-guard-classify.js +224 -0
- package/dist/flow-guard-classify.js.map +1 -0
- package/dist/flow-guard-decode.d.ts +17 -0
- package/dist/flow-guard-decode.d.ts.map +1 -0
- package/dist/flow-guard-decode.js +149 -0
- package/dist/flow-guard-decode.js.map +1 -0
- package/dist/flow-guard-destinations.d.ts +23 -0
- package/dist/flow-guard-destinations.d.ts.map +1 -0
- package/dist/flow-guard-destinations.js +158 -0
- package/dist/flow-guard-destinations.js.map +1 -0
- package/dist/flow-guard-fingerprints.d.ts +27 -0
- package/dist/flow-guard-fingerprints.d.ts.map +1 -0
- package/dist/flow-guard-fingerprints.js +164 -0
- package/dist/flow-guard-fingerprints.js.map +1 -0
- package/dist/flow-guard-fragments.d.ts +17 -0
- package/dist/flow-guard-fragments.d.ts.map +1 -0
- package/dist/flow-guard-fragments.js +122 -0
- package/dist/flow-guard-fragments.js.map +1 -0
- package/dist/flow-guard-instructions.d.ts +12 -0
- package/dist/flow-guard-instructions.d.ts.map +1 -0
- package/dist/flow-guard-instructions.js +36 -0
- package/dist/flow-guard-instructions.js.map +1 -0
- package/dist/flow-guard-labels.d.ts +10 -0
- package/dist/flow-guard-labels.d.ts.map +1 -0
- package/dist/flow-guard-labels.js +55 -0
- package/dist/flow-guard-labels.js.map +1 -0
- package/dist/flow-guard-leak.d.ts +29 -0
- package/dist/flow-guard-leak.d.ts.map +1 -0
- package/dist/flow-guard-leak.js +135 -0
- package/dist/flow-guard-leak.js.map +1 -0
- package/dist/flow-guard-paths.d.ts +21 -0
- package/dist/flow-guard-paths.d.ts.map +1 -0
- package/dist/flow-guard-paths.js +72 -0
- package/dist/flow-guard-paths.js.map +1 -0
- package/dist/flow-guard-policy.d.ts +15 -0
- package/dist/flow-guard-policy.d.ts.map +1 -0
- package/dist/flow-guard-policy.js +179 -0
- package/dist/flow-guard-policy.js.map +1 -0
- package/dist/flow-guard-post.d.ts +6 -0
- package/dist/flow-guard-post.d.ts.map +1 -0
- package/dist/flow-guard-post.js +131 -0
- package/dist/flow-guard-post.js.map +1 -0
- package/dist/flow-guard-pre.d.ts +13 -0
- package/dist/flow-guard-pre.d.ts.map +1 -0
- package/dist/flow-guard-pre.js +335 -0
- package/dist/flow-guard-pre.js.map +1 -0
- package/dist/flow-guard-screen.d.ts +85 -0
- package/dist/flow-guard-screen.d.ts.map +1 -0
- package/dist/flow-guard-screen.js +558 -0
- package/dist/flow-guard-screen.js.map +1 -0
- package/dist/flow-guard-state.d.ts +12 -0
- package/dist/flow-guard-state.d.ts.map +1 -0
- package/dist/flow-guard-state.js +121 -0
- package/dist/flow-guard-state.js.map +1 -0
- package/dist/flow-guard-text.d.ts +9 -0
- package/dist/flow-guard-text.d.ts.map +1 -0
- package/dist/flow-guard-text.js +46 -0
- package/dist/flow-guard-text.js.map +1 -0
- package/dist/flow-guard-types.d.ts +154 -0
- package/dist/flow-guard-types.d.ts.map +1 -0
- package/dist/flow-guard-types.js +2 -0
- package/dist/flow-guard-types.js.map +1 -0
- package/dist/flow-guard.d.ts +20 -215
- package/dist/flow-guard.d.ts.map +1 -1
- package/dist/flow-guard.js +26 -1400
- package/dist/flow-guard.js.map +1 -1
- package/dist/mcp-client.d.ts +1 -2
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +2 -19
- package/dist/mcp-client.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/public-address.d.ts +3 -0
- package/dist/public-address.d.ts.map +1 -0
- package/dist/public-address.js +58 -0
- package/dist/public-address.js.map +1 -0
- package/dist/review-diff.d.ts +36 -0
- package/dist/review-diff.d.ts.map +1 -1
- package/dist/review-diff.js +177 -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 +29 -2
- 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 +8 -1
- package/dist/wrap-state.d.ts.map +1 -1
- package/dist/wrap-state.js +21 -4
- package/dist/wrap-state.js.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/dist/wrap.js +204 -10
- package/dist/wrap.js.map +1 -1
- package/docs/flow-guard-model-comparison-2026-10-25.es.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.fr.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.ru.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.zh.md +82 -0
- package/docs/flow-guard.es.md +307 -12
- package/docs/flow-guard.fr.md +307 -12
- package/docs/flow-guard.md +315 -13
- package/docs/flow-guard.ru.md +314 -13
- package/docs/flow-guard.zh.md +306 -8
- package/docs/scan-limits.es.md +39 -0
- package/docs/scan-limits.fr.md +39 -0
- package/docs/scan-limits.md +39 -0
- package/docs/scan-limits.ru.md +39 -0
- package/docs/scan-limits.zh.md +39 -0
- package/docs/scan.es.md +2 -0
- package/docs/scan.fr.md +2 -0
- package/docs/scan.md +2 -0
- package/docs/scan.ru.md +2 -0
- package/docs/scan.zh.md +2 -0
- 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 +18 -6
- 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.
|
|
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.
|
|
99
|
+
"@vitest/coverage-v8": "^4.1.12",
|
|
88
100
|
"typescript": "^5.7.2",
|
|
89
|
-
"vitest": "^4.1.
|
|
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
|
+
}
|