@anionex/dsh-computer-use 0.1.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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +307 -0
  4. package/README.zh.md +307 -0
  5. package/assets/computer-use-fixture.png +0 -0
  6. package/assets/cursor.png +0 -0
  7. package/cordis.patch.yml +13 -0
  8. package/docs/interaction-policy.i18n.yaml +4 -0
  9. package/docs/interaction-policy.md +140 -0
  10. package/docs/interaction-policy.zh.md +140 -0
  11. package/lib/approval-policy.js +12 -0
  12. package/lib/approval-policy.js.map +1 -0
  13. package/lib/artifacts.js +79 -0
  14. package/lib/artifacts.js.map +1 -0
  15. package/lib/backend.js +3 -0
  16. package/lib/backend.js.map +1 -0
  17. package/lib/client.js +349 -0
  18. package/lib/client.js.map +1 -0
  19. package/lib/config.js +120 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/confirmations.js +99 -0
  22. package/lib/confirmations.js.map +1 -0
  23. package/lib/diff.js +58 -0
  24. package/lib/diff.js.map +1 -0
  25. package/lib/errors.js +22 -0
  26. package/lib/errors.js.map +1 -0
  27. package/lib/exposure.js +231 -0
  28. package/lib/exposure.js.map +1 -0
  29. package/lib/index.js +56 -0
  30. package/lib/index.js.map +1 -0
  31. package/lib/leases.js +259 -0
  32. package/lib/leases.js.map +1 -0
  33. package/lib/providers/macos.js +148 -0
  34. package/lib/providers/macos.js.map +1 -0
  35. package/lib/providers/native-helper.js +356 -0
  36. package/lib/providers/native-helper.js.map +1 -0
  37. package/lib/service.js +480 -0
  38. package/lib/service.js.map +1 -0
  39. package/lib/skill.js +119 -0
  40. package/lib/skill.js.map +1 -0
  41. package/lib/target-resolver.js +180 -0
  42. package/lib/target-resolver.js.map +1 -0
  43. package/lib/tools.js +493 -0
  44. package/lib/tools.js.map +1 -0
  45. package/lib/types/approval-policy.d.ts +13 -0
  46. package/lib/types/approval-policy.d.ts.map +1 -0
  47. package/lib/types/artifacts.d.ts +10 -0
  48. package/lib/types/artifacts.d.ts.map +1 -0
  49. package/lib/types/backend.d.ts +94 -0
  50. package/lib/types/backend.d.ts.map +1 -0
  51. package/lib/types/client/index.d.ts +78 -0
  52. package/lib/types/client/index.d.ts.map +1 -0
  53. package/lib/types/config.d.ts +75 -0
  54. package/lib/types/config.d.ts.map +1 -0
  55. package/lib/types/confirmations.d.ts +23 -0
  56. package/lib/types/confirmations.d.ts.map +1 -0
  57. package/lib/types/diff.d.ts +5 -0
  58. package/lib/types/diff.d.ts.map +1 -0
  59. package/lib/types/errors.d.ts +15 -0
  60. package/lib/types/errors.d.ts.map +1 -0
  61. package/lib/types/exposure.d.ts +30 -0
  62. package/lib/types/exposure.d.ts.map +1 -0
  63. package/lib/types/index.d.ts +21 -0
  64. package/lib/types/index.d.ts.map +1 -0
  65. package/lib/types/leases.d.ts +80 -0
  66. package/lib/types/leases.d.ts.map +1 -0
  67. package/lib/types/providers/macos.d.ts +15 -0
  68. package/lib/types/providers/macos.d.ts.map +1 -0
  69. package/lib/types/providers/native-helper.d.ts +36 -0
  70. package/lib/types/providers/native-helper.d.ts.map +1 -0
  71. package/lib/types/service.d.ts +52 -0
  72. package/lib/types/service.d.ts.map +1 -0
  73. package/lib/types/skill.d.ts +9 -0
  74. package/lib/types/skill.d.ts.map +1 -0
  75. package/lib/types/target-resolver.d.ts +38 -0
  76. package/lib/types/target-resolver.d.ts.map +1 -0
  77. package/lib/types/tools.d.ts +6 -0
  78. package/lib/types/tools.d.ts.map +1 -0
  79. package/lib/types/types.d.ts +246 -0
  80. package/lib/types/types.d.ts.map +1 -0
  81. package/lib/types/web.d.ts +32 -0
  82. package/lib/types/web.d.ts.map +1 -0
  83. package/lib/types.js +9 -0
  84. package/lib/types.js.map +1 -0
  85. package/lib/web.js +163 -0
  86. package/lib/web.js.map +1 -0
  87. package/native/macos/Sources/Fixture/main.swift +366 -0
  88. package/native/macos/Sources/Helper/CursorImage.swift +11 -0
  89. package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
  90. package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
  91. package/native/macos/Sources/Helper/main.swift +1140 -0
  92. package/native/macos/Sources/Monitor/main.swift +139 -0
  93. package/native/macos/bin/dsh-computer-use-helper +0 -0
  94. package/native/macos/manifest.json +14 -0
  95. package/package.json +128 -0
  96. package/scripts/build-client.mjs +24 -0
  97. package/scripts/build-native.mjs +158 -0
  98. package/scripts/check-native.mjs +92 -0
  99. package/scripts/model-e2e.mjs +291 -0
  100. package/scripts/session-transcript.mjs +117 -0
  101. package/scripts/validate.mjs +256 -0
  102. package/src/approval-policy.ts +16 -0
  103. package/src/artifacts.ts +93 -0
  104. package/src/backend.ts +103 -0
  105. package/src/client/index.tsx +539 -0
  106. package/src/config.ts +196 -0
  107. package/src/confirmations.ts +128 -0
  108. package/src/diff.ts +52 -0
  109. package/src/errors.ts +39 -0
  110. package/src/exposure.ts +236 -0
  111. package/src/index.ts +66 -0
  112. package/src/leases.ts +337 -0
  113. package/src/providers/macos.ts +185 -0
  114. package/src/providers/native-helper.ts +393 -0
  115. package/src/service.ts +583 -0
  116. package/src/skill.ts +123 -0
  117. package/src/target-resolver.ts +237 -0
  118. package/src/tools.ts +528 -0
  119. package/src/types.ts +299 -0
  120. package/src/web.ts +180 -0
@@ -0,0 +1,139 @@
1
+ import AppKit
2
+ import CoreGraphics
3
+ import Darwin
4
+ import Foundation
5
+
6
+ private func argument(_ name: String, default fallback: Int) -> Int {
7
+ let values = ProcessInfo.processInfo.arguments
8
+ guard let index = values.firstIndex(of: name), values.indices.contains(index + 1),
9
+ let value = Int(values[index + 1]), value > 0 else { return fallback }
10
+ return value
11
+ }
12
+
13
+ private func stringArgument(_ name: String) -> String? {
14
+ let values = ProcessInfo.processInfo.arguments
15
+ guard let index = values.firstIndex(of: name), values.indices.contains(index + 1) else { return nil }
16
+ return values[index + 1]
17
+ }
18
+
19
+ private func optionalIntegerArgument(_ name: String) -> Int32? {
20
+ guard let value = stringArgument(name), let parsed = Int32(value), parsed > 0 else { return nil }
21
+ return parsed
22
+ }
23
+
24
+ private func cursorLocation() -> CGPoint {
25
+ CGEvent(source: nil)?.location ?? .zero
26
+ }
27
+
28
+ private func frontmostPid() -> pid_t? {
29
+ NSWorkspace.shared.frontmostApplication?.processIdentifier
30
+ }
31
+
32
+ private func matchingWindows(ownerPid: pid_t?, title: String?) -> [[String: Any]] {
33
+ guard let windows = CGWindowListCopyWindowInfo(
34
+ [.optionOnScreenOnly, .excludeDesktopElements],
35
+ kCGNullWindowID
36
+ ) as? [[String: Any]] else { return [] }
37
+ return windows.filter { window in
38
+ if let ownerPid,
39
+ (window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value != ownerPid { return false }
40
+ if let title,
41
+ (window[kCGWindowName as String] as? String) != title { return false }
42
+ return true
43
+ }
44
+ }
45
+
46
+ private let durationMs = argument("--duration-ms", default: 1200)
47
+ private let intervalMicros = useconds_t(argument("--interval-micros", default: 1000))
48
+ private let windowOwnerPid = stringArgument("--window-owner-pid").flatMap { Int32($0) }
49
+ private let windowTitle = stringArgument("--window-title")
50
+ private let monitoredSourcePid = optionalIntegerArgument("--source-pid")
51
+ private let baselineCursor = cursorLocation()
52
+ private let baselineFrontmostPid = frontmostPid()
53
+ private var maximumCursorDistance = 0.0
54
+ private var observedFrontmostPids = Set<Int32>()
55
+ private var maximumMatchingWindowCount = 0
56
+ private var matchingWindowFrames: [[String: Any]] = []
57
+ private var monitoredSourcePointerEvents = 0
58
+ private var pointerEventSourceCounts: [String: Int] = [:]
59
+ private var samples = 0
60
+
61
+ private let pointerMask = [
62
+ CGEventType.mouseMoved,
63
+ .leftMouseDown,
64
+ .leftMouseUp,
65
+ .leftMouseDragged,
66
+ .rightMouseDown,
67
+ .rightMouseUp,
68
+ .rightMouseDragged,
69
+ .otherMouseDown,
70
+ .otherMouseUp,
71
+ .otherMouseDragged,
72
+ .scrollWheel,
73
+ ].reduce(CGEventMask(0)) { mask, type in
74
+ mask | (CGEventMask(1) << type.rawValue)
75
+ }
76
+ private let eventTap = CGEvent.tapCreate(
77
+ tap: .cgSessionEventTap,
78
+ place: .headInsertEventTap,
79
+ options: .listenOnly,
80
+ eventsOfInterest: pointerMask,
81
+ callback: { _, _, event, _ in
82
+ let sourcePid = event.getIntegerValueField(.eventSourceUnixProcessID)
83
+ pointerEventSourceCounts[String(sourcePid), default: 0] += 1
84
+ if let monitoredSourcePid, sourcePid == Int64(monitoredSourcePid) {
85
+ monitoredSourcePointerEvents += 1
86
+ }
87
+ return Unmanaged.passUnretained(event)
88
+ },
89
+ userInfo: nil
90
+ )
91
+ private let eventTapSource = eventTap.map {
92
+ CFMachPortCreateRunLoopSource(kCFAllocatorDefault, $0, 0)
93
+ }
94
+
95
+ if let eventTap, let eventTapSource {
96
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), eventTapSource, .defaultMode)
97
+ CGEvent.tapEnable(tap: eventTap, enable: true)
98
+ }
99
+
100
+ if let baselineFrontmostPid { observedFrontmostPids.insert(baselineFrontmostPid) }
101
+ FileHandle.standardOutput.write(Data("READY\n".utf8))
102
+
103
+ let deadline = DispatchTime.now().uptimeNanoseconds + UInt64(durationMs) * 1_000_000
104
+ while DispatchTime.now().uptimeNanoseconds < deadline {
105
+ let location = cursorLocation()
106
+ let distance = hypot(location.x - baselineCursor.x, location.y - baselineCursor.y)
107
+ maximumCursorDistance = max(maximumCursorDistance, distance)
108
+ if let pid = frontmostPid() { observedFrontmostPids.insert(pid) }
109
+ let windows = matchingWindows(ownerPid: windowOwnerPid, title: windowTitle)
110
+ maximumMatchingWindowCount = max(maximumMatchingWindowCount, windows.count)
111
+ if let first = windows.first,
112
+ let bounds = first[kCGWindowBounds as String] as? [String: Any] {
113
+ matchingWindowFrames = [bounds]
114
+ }
115
+ samples += 1
116
+ if eventTapSource != nil {
117
+ CFRunLoopRunInMode(.defaultMode, Double(intervalMicros) / 1_000_000, true)
118
+ } else {
119
+ usleep(intervalMicros)
120
+ }
121
+ }
122
+
123
+ let finalCursor = cursorLocation()
124
+ let result: [String: Any] = [
125
+ "baselineCursor": ["x": baselineCursor.x, "y": baselineCursor.y],
126
+ "finalCursor": ["x": finalCursor.x, "y": finalCursor.y],
127
+ "maximumCursorDistance": maximumCursorDistance,
128
+ "baselineFrontmostPid": baselineFrontmostPid.map(Int.init) as Any,
129
+ "observedFrontmostPids": observedFrontmostPids.sorted().map(Int.init),
130
+ "samples": samples,
131
+ "maximumMatchingWindowCount": maximumMatchingWindowCount,
132
+ "matchingWindowFrames": matchingWindowFrames,
133
+ "eventTapAvailable": eventTap != nil,
134
+ "monitoredSourcePointerEvents": monitoredSourcePointerEvents,
135
+ "pointerEventSourceCounts": pointerEventSourceCounts,
136
+ ]
137
+ let data = try JSONSerialization.data(withJSONObject: result, options: [.sortedKeys])
138
+ FileHandle.standardOutput.write(data)
139
+ FileHandle.standardOutput.write(Data("\n".utf8))
@@ -0,0 +1,14 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "helperVersion": "0.1.0",
4
+ "sourceSha256": "a913bbefeffc26247fb792490e9c2ae08598c923a9746b7d4ca8b7cc9efc607c",
5
+ "binary": {
6
+ "path": "bin/dsh-computer-use-helper",
7
+ "sha256": "6dbb7d4b171d9f480e046a4c69df6e3b41c8c5bf0decfabc6e5331c1dc79af91",
8
+ "architectures": [
9
+ "arm64",
10
+ "x86_64"
11
+ ],
12
+ "minimumMacOS": "14.0"
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,128 @@
1
+ {
2
+ "name": "@anionex/dsh-computer-use",
3
+ "version": "0.1.0",
4
+ "description": "Accessibility-first macOS Computer Use capability for DeepSeek Harness with stale-observation protection, app leases, confirmations, screenshots, and Web diagnostics",
5
+ "keywords": [
6
+ "deepseek",
7
+ "deepseek-harness",
8
+ "computer-use",
9
+ "macos",
10
+ "accessibility",
11
+ "desktop-automation"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/Anionex/dsh-computer-use.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/Anionex/dsh-computer-use/issues"
19
+ },
20
+ "type": "module",
21
+ "packageManager": "pnpm@11.20.0",
22
+ "engines": {
23
+ "node": "^22.19.0 || >=24.0.0"
24
+ },
25
+ "main": "lib/index.js",
26
+ "types": "lib/types/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./lib/types/index.d.ts",
30
+ "default": "./lib/index.js"
31
+ },
32
+ "./service": {
33
+ "types": "./lib/types/service.d.ts",
34
+ "default": "./lib/service.js"
35
+ },
36
+ "./provider-macos": {
37
+ "types": "./lib/types/providers/macos.d.ts",
38
+ "default": "./lib/providers/macos.js"
39
+ },
40
+ "./client": {
41
+ "types": "./lib/types/client/index.d.ts",
42
+ "default": "./lib/client.js"
43
+ },
44
+ "./cordis.patch.yml": "./cordis.patch.yml",
45
+ "./package.json": "./package.json"
46
+ },
47
+ "files": [
48
+ "lib",
49
+ "src",
50
+ "assets",
51
+ "native/macos/Sources",
52
+ "native/macos/bin",
53
+ "native/macos/manifest.json",
54
+ "scripts",
55
+ "docs",
56
+ "cordis.patch.yml",
57
+ "README.md",
58
+ "README.zh.md",
59
+ "LICENSE"
60
+ ],
61
+ "scripts": {
62
+ "native:build": "node scripts/build-native.mjs",
63
+ "check:native": "node scripts/check-native.mjs",
64
+ "build": "pnpm run native:build && tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
65
+ "prepack": "pnpm run build",
66
+ "test": "pnpm run build && vitest run tests",
67
+ "test:model": "node scripts/model-e2e.mjs",
68
+ "test:watch": "vitest tests",
69
+ "validate": "node scripts/validate.mjs --lane all",
70
+ "validate:local": "node scripts/validate.mjs --lane local",
71
+ "validate:profile": "node scripts/validate.mjs --lane profile",
72
+ "validate:model": "node scripts/model-e2e.mjs",
73
+ "validate:release": "pnpm run validate && pnpm run validate:model"
74
+ },
75
+ "dsh": {
76
+ "bundle": {
77
+ "patch": "./cordis.patch.yml"
78
+ },
79
+ "client": {
80
+ "inject": [
81
+ "@deepseek-ai/dsh-client-runtime",
82
+ "@deepseek-ai/dsh-client-ui-settings",
83
+ "@deepseek-ai/dsh-client-locale",
84
+ "@deepseek-ai/dsh-api-remotes"
85
+ ],
86
+ "platform": "web"
87
+ }
88
+ },
89
+ "peerDependencies": {
90
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
91
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
92
+ "@deepseek-ai/dsh-brand": "^0.1.0-rc.6",
93
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
94
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
95
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
96
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
97
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
98
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
99
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
100
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
101
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
102
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
103
+ "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
104
+ "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
105
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
106
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
107
+ "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
108
+ "@deepseek-ai/cordis": "^4.0.1",
109
+ "react": "^18.2.0",
110
+ "@deepseek-ai/schemastery": "^3.18.1"
111
+ },
112
+ "peerDependenciesMeta": {
113
+ "@deepseek-ai/dsh-host-webserver": {
114
+ "optional": true
115
+ }
116
+ },
117
+ "devDependencies": {
118
+ "@types/node": "^22.20.0",
119
+ "@types/react": "~18.3.1",
120
+ "@types/react-dom": "~18.3.0",
121
+ "typescript": "^6.0.3",
122
+ "vitest": "^4.1.10"
123
+ },
124
+ "dependencies": {
125
+ "zod": "^4.4.3"
126
+ },
127
+ "license": "MIT"
128
+ }
@@ -0,0 +1,24 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const root = dirname(fileURLToPath(new URL('../package.json', import.meta.url)))
6
+ const compiledPath = join(root, '.client-build', 'index.js')
7
+ const outputPath = join(root, 'lib', 'client.js')
8
+ const source = await readFile(compiledPath, 'utf8')
9
+ const wrapped = [
10
+ 'window.__ModuleLoader__.load({ id: "@anionex/dsh-computer-use", factory: (require) => {',
11
+ 'var module = { exports: {} }; var exports = module.exports;',
12
+ source.replace(/\n?\/\/# sourceMappingURL=.*$/u, ''),
13
+ 'return module.exports; } });',
14
+ '//# sourceMappingURL=client.js.map',
15
+ '',
16
+ ].join('\n')
17
+
18
+ await mkdir(dirname(outputPath), { recursive: true })
19
+ await writeFile(outputPath, wrapped)
20
+ const rawMap = JSON.parse(await readFile(`${compiledPath}.map`, 'utf8'))
21
+ rawMap.file = 'client.js'
22
+ rawMap.sources = rawMap.sources.map(sourcePath => `../src/client/${sourcePath.replace(/^\.\.\//u, '')}`)
23
+ await writeFile(`${outputPath}.map`, `${JSON.stringify(rawMap)}\n`)
24
+ await rm(join(root, '.client-build'), { recursive: true, force: true })
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { spawn } from 'node:child_process'
5
+ import { chmod, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
6
+ import { arch } from 'node:os'
7
+ import { basename, dirname, join, resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const ROOT = dirname(fileURLToPath(new URL('../package.json', import.meta.url)))
11
+ const NATIVE = join(ROOT, 'native', 'macos')
12
+ const BUILD = join(NATIVE, 'build')
13
+ const HELPER_SOURCE = join(NATIVE, 'Sources', 'Helper')
14
+ const FIXTURE_SOURCE = join(NATIVE, 'Sources', 'Fixture', 'main.swift')
15
+ const MONITOR_SOURCE = join(NATIVE, 'Sources', 'Monitor', 'main.swift')
16
+ const HELPER_OUTPUT = join(NATIVE, 'bin', 'dsh-computer-use-helper')
17
+ const FIXTURE_APP = join(NATIVE, 'fixture', 'DSHComputerUseFixture.app')
18
+ const MONITOR_OUTPUT = join(NATIVE, 'fixture', 'dsh-computer-use-input-monitor')
19
+ const args = new Set(process.argv.slice(2))
20
+ const helperOnly = args.has('--helper-only')
21
+ const fixtureOnly = args.has('--fixture-only')
22
+ if ([...args].some(value => !['--helper-only', '--fixture-only'].includes(value)) || (helperOnly && fixtureOnly)) {
23
+ throw new Error('usage: build-native.mjs [--helper-only | --fixture-only]')
24
+ }
25
+
26
+ async function run(command, commandArgs, timeoutMs = 180000) {
27
+ const result = await new Promise((resolveResult) => {
28
+ const child = spawn(command, commandArgs, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] })
29
+ let stdout = ''
30
+ let stderr = ''
31
+ child.stdout.setEncoding('utf8').on('data', value => { stdout += value })
32
+ child.stderr.setEncoding('utf8').on('data', value => { stderr += value })
33
+ const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs)
34
+ child.once('error', error => {
35
+ clearTimeout(timer)
36
+ resolveResult({ code: -1, stdout, stderr: `${stderr}${error.message}` })
37
+ })
38
+ child.once('close', code => {
39
+ clearTimeout(timer)
40
+ resolveResult({ code: code ?? -1, stdout, stderr })
41
+ })
42
+ })
43
+ if (result.code !== 0) throw new Error(`${command} failed (${result.code})\n${result.stderr || result.stdout}`)
44
+ return result.stdout.trim()
45
+ }
46
+
47
+ async function hashFile(path) {
48
+ return createHash('sha256').update(await readFile(path)).digest('hex')
49
+ }
50
+
51
+ async function hashDirectory(directory) {
52
+ const names = (await readdir(directory)).filter(name => name.endsWith('.swift')).sort()
53
+ const hash = createHash('sha256')
54
+ for (const name of names) {
55
+ hash.update(name)
56
+ hash.update('\0')
57
+ hash.update(await readFile(join(directory, name)))
58
+ hash.update('\0')
59
+ }
60
+ return hash.digest('hex')
61
+ }
62
+
63
+ async function buildHelper() {
64
+ await mkdir(BUILD, { recursive: true })
65
+ await mkdir(dirname(HELPER_OUTPUT), { recursive: true })
66
+ const sources = (await readdir(HELPER_SOURCE)).filter(name => name.endsWith('.swift')).sort().map(name => join(HELPER_SOURCE, name))
67
+ const outputs = []
68
+ for (const architecture of ['arm64', 'x86_64']) {
69
+ const output = join(BUILD, `dsh-computer-use-helper-${architecture}`)
70
+ await run('xcrun', [
71
+ 'swiftc', '-swift-version', '5', '-parse-as-library', '-O',
72
+ '-target', `${architecture}-apple-macos14.0`,
73
+ '-framework', 'AppKit',
74
+ '-framework', 'ApplicationServices',
75
+ '-framework', 'CoreGraphics',
76
+ '-framework', 'QuartzCore',
77
+ '-framework', 'ScreenCaptureKit',
78
+ '-lproc',
79
+ ...sources,
80
+ '-o', output,
81
+ ], 300000)
82
+ outputs.push(output)
83
+ }
84
+ await run('xcrun', ['lipo', '-create', ...outputs, '-output', HELPER_OUTPUT])
85
+ await chmod(HELPER_OUTPUT, 0o755)
86
+ await run('codesign', ['--force', '--sign', '-', '--timestamp=none', HELPER_OUTPUT])
87
+ const architectures = (await run('xcrun', ['lipo', '-archs', HELPER_OUTPUT])).split(/\s+/u).filter(Boolean).sort()
88
+ if (architectures.join(',') !== 'arm64,x86_64') throw new Error(`helper is not universal: ${architectures.join(', ')}`)
89
+ const manifest = {
90
+ schemaVersion: 1,
91
+ helperVersion: '0.1.0',
92
+ sourceSha256: await hashDirectory(HELPER_SOURCE),
93
+ binary: {
94
+ path: 'bin/dsh-computer-use-helper',
95
+ sha256: await hashFile(HELPER_OUTPUT),
96
+ architectures,
97
+ minimumMacOS: '14.0',
98
+ },
99
+ }
100
+ await writeFile(join(NATIVE, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
101
+ return { path: HELPER_OUTPUT, ...manifest.binary }
102
+ }
103
+
104
+ async function buildFixture() {
105
+ const contents = join(FIXTURE_APP, 'Contents')
106
+ const executable = join(contents, 'MacOS', 'DSHComputerUseFixture')
107
+ await rm(FIXTURE_APP, { recursive: true, force: true })
108
+ await mkdir(dirname(executable), { recursive: true })
109
+ await mkdir(join(contents, 'Resources'), { recursive: true })
110
+ const targetArch = arch() === 'x64' ? 'x86_64' : 'arm64'
111
+ await run('xcrun', [
112
+ 'swiftc', '-swift-version', '5', '-O',
113
+ '-target', `${targetArch}-apple-macos14.0`,
114
+ '-framework', 'AppKit',
115
+ FIXTURE_SOURCE,
116
+ '-o', executable,
117
+ ], 180000)
118
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
119
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
120
+ <plist version="1.0"><dict>
121
+ <key>CFBundleDevelopmentRegion</key><string>en</string>
122
+ <key>CFBundleExecutable</key><string>DSHComputerUseFixture</string>
123
+ <key>CFBundleIdentifier</key><string>io.anionex.dsh-computer-use-fixture</string>
124
+ <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
125
+ <key>CFBundleName</key><string>DSH Computer Use Fixture</string>
126
+ <key>CFBundlePackageType</key><string>APPL</string>
127
+ <key>CFBundleShortVersionString</key><string>0.1.0</string>
128
+ <key>CFBundleVersion</key><string>1</string>
129
+ <key>LSMinimumSystemVersion</key><string>14.0</string>
130
+ <key>NSHighResolutionCapable</key><true/>
131
+ </dict></plist>
132
+ `
133
+ await writeFile(join(contents, 'Info.plist'), plist)
134
+ await run('codesign', ['--force', '--deep', '--sign', '-', '--timestamp=none', FIXTURE_APP])
135
+ await run('xcrun', [
136
+ 'swiftc', '-swift-version', '5', '-O',
137
+ '-target', `${targetArch}-apple-macos14.0`,
138
+ '-framework', 'AppKit',
139
+ '-framework', 'CoreGraphics',
140
+ MONITOR_SOURCE,
141
+ '-o', MONITOR_OUTPUT,
142
+ ], 180000)
143
+ await chmod(MONITOR_OUTPUT, 0o755)
144
+ await run('codesign', ['--force', '--sign', '-', '--timestamp=none', MONITOR_OUTPUT])
145
+ return {
146
+ path: FIXTURE_APP,
147
+ bundleId: 'io.anionex.dsh-computer-use-fixture',
148
+ executable: basename(executable),
149
+ monitor: MONITOR_OUTPUT,
150
+ }
151
+ }
152
+
153
+ await mkdir(BUILD, { recursive: true })
154
+ const result = {}
155
+ if (!fixtureOnly) result.helper = await buildHelper()
156
+ if (!helperOnly) result.fixture = await buildFixture()
157
+ await rm(BUILD, { recursive: true, force: true })
158
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result }, null, 2)}\n`)
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { execFile } from 'node:child_process'
5
+ import { readFile, readdir } from 'node:fs/promises'
6
+ import { dirname, join } from 'node:path'
7
+ import { promisify } from 'node:util'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const execFileAsync = promisify(execFile)
11
+ const root = dirname(fileURLToPath(new URL('../package.json', import.meta.url)))
12
+ const nativeRoot = join(root, 'native', 'macos')
13
+ const helperPath = join(nativeRoot, 'bin', 'dsh-computer-use-helper')
14
+ const manifest = JSON.parse(await readFile(join(nativeRoot, 'manifest.json'), 'utf8'))
15
+
16
+ function assert(condition, message) {
17
+ if (!condition) throw new Error(`native integrity: ${message}`)
18
+ }
19
+
20
+ async function sha256(path) {
21
+ return createHash('sha256').update(await readFile(path)).digest('hex')
22
+ }
23
+
24
+ async function sourceSha256() {
25
+ const sourceRoot = join(nativeRoot, 'Sources', 'Helper')
26
+ const names = (await readdir(sourceRoot)).filter(name => name.endsWith('.swift')).sort()
27
+ const hash = createHash('sha256')
28
+ for (const name of names) {
29
+ hash.update(name)
30
+ hash.update('\0')
31
+ hash.update(await readFile(join(sourceRoot, name)))
32
+ hash.update('\0')
33
+ }
34
+ return hash.digest('hex')
35
+ }
36
+
37
+ async function helperSource() {
38
+ const sourceRoot = join(nativeRoot, 'Sources', 'Helper')
39
+ const names = (await readdir(sourceRoot)).filter(name => name.endsWith('.swift')).sort()
40
+ return (await Promise.all(names.map(name => readFile(join(sourceRoot, name), 'utf8')))).join('\n')
41
+ }
42
+
43
+ assert(manifest.schemaVersion === 1, 'unsupported manifest schema')
44
+ assert(manifest.binary?.path === 'bin/dsh-computer-use-helper', 'unexpected helper path')
45
+ assert(manifest.binary?.minimumMacOS === '14.0', 'unexpected minimum macOS version')
46
+ assert(manifest.sourceSha256 === await sourceSha256(), 'Swift source digest does not match manifest')
47
+ assert(manifest.binary.sha256 === await sha256(helperPath), 'helper SHA-256 does not match manifest')
48
+
49
+ const architectureOutput = await execFileAsync('xcrun', ['lipo', '-archs', helperPath])
50
+ const architectures = architectureOutput.stdout.trim().split(/\s+/u).sort()
51
+ assert(JSON.stringify(architectures) === JSON.stringify(['arm64', 'x86_64']), `unexpected architectures: ${architectures.join(', ')}`)
52
+ await execFileAsync('codesign', ['--verify', '--strict', helperPath])
53
+
54
+ const source = await helperSource()
55
+ assert(!/CGWarpMouseCursorPosition|CGAssociateMouseAndMouseCursorPosition/u.test(source), 'source contains a system-cursor warp primitive')
56
+ assert(!/\.post\s*\(\s*tap:|cghidEventTap/u.test(source), 'source contains a global HID pointer-post path')
57
+ assert(source.includes('SLEventPostToPid'), 'source does not contain the target-process SkyLight pointer route')
58
+ assert(source.includes('CGEventSetWindowLocation'), 'source does not bind pointer events to window-local coordinates')
59
+ assert(source.includes('proc_pidfdinfo'), 'source does not inspect parent-owned native transports')
60
+ assert(source.includes('getpgrp() == getpid()'), 'source does not require a managed detached process group')
61
+ assert(source.includes('parentOwnsStandardTransport()'), 'source does not validate parent ownership of the three standard transports')
62
+ const dynamicSymbols = [...source.matchAll(/Self\.resolve\s*\(\s*handle\s*,\s*"([^"]+)"/gu)].map(match => match[1]).sort()
63
+ const allowedDynamicSymbols = ['CGEventSetWindowLocation', 'SLEventPostToPid', 'SLEventSetIntegerValueField'].sort()
64
+ assert(
65
+ JSON.stringify(dynamicSymbols) === JSON.stringify(allowedDynamicSymbols),
66
+ `unexpected dynamic native symbols: ${dynamicSymbols.join(', ') || '(none)'}`,
67
+ )
68
+ assert((source.match(/\bdlsym\s*\(/gu) ?? []).length === 1, 'native helper must keep one audited dlsym call site')
69
+
70
+ const symbolOutput = await execFileAsync('nm', ['-u', helperPath])
71
+ const undefinedSymbols = symbolOutput.stdout.split(/\r?\n/u).map(line => line.trim().split(/\s+/u).at(-1)).filter(Boolean)
72
+ for (const forbidden of ['_CGEventPost', '_CGWarpMouseCursorPosition', '_CGAssociateMouseAndMouseCursorPosition']) {
73
+ assert(!undefinedSymbols.includes(forbidden), `helper links forbidden global pointer symbol ${forbidden}`)
74
+ }
75
+
76
+ const stringOutput = await execFileAsync('strings', [helperPath])
77
+ const binaryStrings = stringOutput.stdout.split(/\r?\n/u)
78
+ for (const allowed of allowedDynamicSymbols) {
79
+ assert(binaryStrings.includes(allowed), `helper binary is missing audited dynamic symbol ${allowed}`)
80
+ }
81
+ for (const forbidden of ['CGEventPost', 'CGWarpMouseCursorPosition', 'CGAssociateMouseAndMouseCursorPosition']) {
82
+ assert(!binaryStrings.includes(forbidden), `helper contains forbidden dynamic symbol literal ${forbidden}`)
83
+ }
84
+
85
+ process.stdout.write(`${JSON.stringify({
86
+ ok: true,
87
+ helper: manifest.binary.path,
88
+ sha256: manifest.binary.sha256,
89
+ architectures,
90
+ minimumMacOS: manifest.binary.minimumMacOS,
91
+ pointerRouting: 'target-process-only',
92
+ }, null, 2)}\n`)