@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,393 @@
1
+ /** Managed invocation and integrity checks for the fixed-command Swift helper. */
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { access, chmod, lstat, readFile, realpath, stat } from 'node:fs/promises'
5
+ import { constants, type Stats } from 'node:fs'
6
+ import type { Writable } from 'node:stream'
7
+ import { dirname, resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+ import type { SubprocessHandle, SubprocessOutcome, SubprocessOutputReader } from '@deepseek-ai/dsh-subprocess'
11
+ import type { ResolvedComputerUseConfig } from '../config.ts'
12
+ import { ComputerUseError, computerUseError, type ComputerUseErrorCode } from '../errors.ts'
13
+
14
+ interface NativeManifest {
15
+ schemaVersion: 1
16
+ helperVersion: string
17
+ sourceSha256: string
18
+ binary: {
19
+ path: string
20
+ sha256: string
21
+ architectures: string[]
22
+ minimumMacOS: string
23
+ }
24
+ }
25
+
26
+ interface HelperFailure {
27
+ ok: false
28
+ error: {
29
+ code: ComputerUseErrorCode
30
+ message: string
31
+ }
32
+ }
33
+
34
+ interface HelperSuccess<T> {
35
+ ok: true
36
+ value: T
37
+ }
38
+
39
+ type HelperEnvelope<T> = HelperFailure | HelperSuccess<T>
40
+
41
+ const CURSOR_READY_TIMEOUT_MS = 2_000
42
+ const CURSOR_PROTOCOL_MAX_BYTES = 64 * 1024
43
+
44
+ interface CursorProcess {
45
+ stdin: Writable
46
+ done: Promise<SubprocessOutcome>
47
+ terminate: () => void
48
+ waitForExit: SubprocessHandle['waitForExit']
49
+ }
50
+
51
+ function collected(reader: SubprocessOutputReader | undefined): string {
52
+ if (reader === undefined) return ''
53
+ const value = reader.readFrom(0)
54
+ if (value.lossy) throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper output exceeded its protocol limit')
55
+ return value.text
56
+ }
57
+
58
+ async function sha256(path: string): Promise<string> {
59
+ return createHash('sha256').update(await readFile(path)).digest('hex')
60
+ }
61
+
62
+ function nativeRoot(): string {
63
+ return fileURLToPath(new URL('../../native/macos/', import.meta.url))
64
+ }
65
+
66
+ /** Exact helper paths and integrity data for one active generation. */
67
+ export interface PreparedNativeHelper {
68
+ path: string
69
+ version: string
70
+ sha256: string
71
+ }
72
+
73
+ /** Invokes only the packaged JSON protocol through `ctx.subprocess`; no source or shell reaches the helper. */
74
+ export class NativeHelperClient {
75
+ private prepared?: PreparedNativeHelper
76
+ private cursor: CursorProcess | undefined
77
+ private cursorStart: { promise: Promise<CursorProcess> } | undefined
78
+
79
+ constructor(
80
+ private readonly ctx: Context,
81
+ private readonly config: ResolvedComputerUseConfig,
82
+ private readonly managedRoot = nativeRoot(),
83
+ ) {}
84
+
85
+ /** Absolute executable path selected by explicit override or the packaged managed binary. */
86
+ get helperPath(): string {
87
+ return this.prepared?.path ?? this.config.helper.path ?? resolve(this.managedRoot, 'bin', 'dsh-computer-use-helper')
88
+ }
89
+
90
+ /** Verify platform, file type, packaged hash, and executable mode before use. */
91
+ async prepare(signal: AbortSignal): Promise<PreparedNativeHelper> {
92
+ if (process.platform !== 'darwin') {
93
+ throw new ComputerUseError('COMPUTER_UNSUPPORTED_PLATFORM', `macOS provider cannot run on ${process.platform}`)
94
+ }
95
+ const managed = this.config.helper.path === undefined
96
+ let path = this.helperPath
97
+ let selectedInfo: Stats
98
+ try {
99
+ selectedInfo = await lstat(path)
100
+ } catch (error) {
101
+ const missing = (error as NodeJS.ErrnoException).code === 'ENOENT'
102
+ if (!managed || !missing || !this.config.helper.allowSourceBuild) {
103
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `native helper is missing or unreadable: ${path}`, { cause: error })
104
+ }
105
+ await this.buildManaged(signal)
106
+ selectedInfo = await lstat(path)
107
+ }
108
+ if (!selectedInfo.isFile() || selectedInfo.isSymbolicLink()) {
109
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper must be a regular non-symbolic-link executable')
110
+ }
111
+ path = await realpath(path)
112
+ const digest = await sha256(path)
113
+ let version = 'external'
114
+ if (managed) {
115
+ const manifestPath = resolve(this.managedRoot, 'manifest.json')
116
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as NativeManifest
117
+ if (manifest.schemaVersion !== 1 || manifest.binary.path !== 'bin/dsh-computer-use-helper') {
118
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper manifest is malformed')
119
+ }
120
+ if (manifest.binary.sha256 !== digest) {
121
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper hash does not match native/macos/manifest.json')
122
+ }
123
+ version = manifest.helperVersion
124
+ }
125
+ try {
126
+ await access(path, constants.X_OK)
127
+ } catch (error) {
128
+ if (!managed) {
129
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `external native helper is not executable: ${path}`, { cause: error })
130
+ }
131
+ try {
132
+ // npm-compatible tarballs normalize non-bin payloads to 0644. Restore
133
+ // only the owner's execute bit after the committed hash is verified.
134
+ await chmod(path, (selectedInfo.mode & 0o777) | 0o100)
135
+ await access(path, constants.X_OK)
136
+ } catch (chmodError) {
137
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `managed native helper cannot be marked executable: ${path}`, { cause: chmodError })
138
+ }
139
+ }
140
+ this.prepared = { path, version, sha256: digest }
141
+ return this.prepared
142
+ }
143
+
144
+ /** Invoke one fixed helper command and parse its bounded JSON envelope. */
145
+ async invoke<T>(request: Record<string, unknown>, signal: AbortSignal): Promise<T> {
146
+ const prepared = this.prepared ?? await this.prepare(signal)
147
+ const timeout = AbortSignal.timeout(this.config.actionTimeoutMs)
148
+ const combined = AbortSignal.any([signal, timeout])
149
+ const handle = this.ctx.subprocess.spawn({
150
+ argv: [prepared.path],
151
+ cwd: dirname(prepared.path),
152
+ stdio: {
153
+ stdin: { data: `${JSON.stringify({ protocolVersion: 1, ...request })}\n` },
154
+ stdout: { maxBytes: 4 * 1024 * 1024 },
155
+ stderr: { maxBytes: 64 * 1024 },
156
+ },
157
+ graceMs: 1000,
158
+ signal: combined,
159
+ env: {
160
+ LANG: process.env.LANG ?? 'en_US.UTF-8',
161
+ LC_ALL: process.env.LC_ALL ?? 'en_US.UTF-8',
162
+ },
163
+ })
164
+ let outcome
165
+ try {
166
+ outcome = await handle.done
167
+ } catch (error) {
168
+ throw computerUseError(error, 'native helper failed to start')
169
+ }
170
+ if (combined.aborted) {
171
+ if (signal.aborted) throw new ComputerUseError('COMPUTER_CANCELLED', 'native helper call was cancelled')
172
+ throw new ComputerUseError('COMPUTER_TIMEOUT', `native helper exceeded ${this.config.actionTimeoutMs} milliseconds`)
173
+ }
174
+ const stdout = collected(handle.collected.stdout)
175
+ const stderr = collected(handle.collected.stderr)
176
+ if (outcome.exitCode !== 0 && stdout.trim().length === 0) {
177
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `native helper exited ${String(outcome.exitCode)}${stderr.trim().length === 0 ? '' : `: ${stderr.trim().slice(0, 1000)}`}`)
178
+ }
179
+ let envelope: HelperEnvelope<T>
180
+ try {
181
+ envelope = JSON.parse(stdout) as HelperEnvelope<T>
182
+ } catch (error) {
183
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper returned invalid JSON', { cause: error })
184
+ }
185
+ if (envelope.ok !== true) throw new ComputerUseError(envelope.error.code, envelope.error.message.slice(0, 1000))
186
+ return envelope.value
187
+ }
188
+
189
+ /** Send one best-effort command to the persistent, click-through Agent cursor overlay. */
190
+ async cursorCommand(command: Record<string, unknown>, signal: AbortSignal): Promise<void> {
191
+ const prepared = this.prepared ?? await this.prepare(signal)
192
+ const cursor = await this.getCursor(prepared, signal)
193
+ signal.throwIfAborted()
194
+ try {
195
+ await new Promise<void>((resolveWrite, rejectWrite) => {
196
+ cursor.stdin.write(`${JSON.stringify(command)}\n`, error => {
197
+ if (error === undefined || error === null) resolveWrite()
198
+ else rejectWrite(error)
199
+ })
200
+ })
201
+ } catch (error) {
202
+ if (this.cursor === cursor) this.cursor = undefined
203
+ cursor.terminate()
204
+ throw computerUseError(error, 'native cursor overlay command failed')
205
+ }
206
+ }
207
+
208
+ /** Stop the cursor process before a provider generation is replaced or disposed. */
209
+ async dispose(): Promise<void> {
210
+ const cursor = this.cursor ?? await this.cursorStart?.promise.catch(() => undefined)
211
+ this.cursorStart = undefined
212
+ this.cursor = undefined
213
+ if (cursor === undefined) return
214
+ try {
215
+ cursor.stdin.end(`${JSON.stringify({ op: 'stop' })}\n`)
216
+ } catch {
217
+ // A child that already closed its pipe is handled by the tree-exit wait below.
218
+ }
219
+ if (await cursor.waitForExit(AbortSignal.timeout(1_000))) return
220
+ cursor.terminate()
221
+ await cursor.waitForExit()
222
+ }
223
+
224
+ /** Prepared integrity facts used by provider health. */
225
+ preparedInfo(): PreparedNativeHelper {
226
+ if (this.prepared === undefined) throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper is not prepared')
227
+ return this.prepared
228
+ }
229
+
230
+ private async buildManaged(signal: AbortSignal): Promise<void> {
231
+ const script = fileURLToPath(new URL('../../scripts/build-native.mjs', import.meta.url))
232
+ const handle = this.ctx.subprocess.spawn({
233
+ argv: [process.execPath, script, '--helper-only'],
234
+ cwd: dirname(script),
235
+ stdio: {
236
+ stdin: 'ignore',
237
+ stdout: { maxBytes: 256 * 1024 },
238
+ stderr: { maxBytes: 256 * 1024 },
239
+ },
240
+ graceMs: 1000,
241
+ signal,
242
+ env: {},
243
+ })
244
+ const outcome = await handle.done
245
+ if (outcome.exitCode !== 0) {
246
+ const stderr = collected(handle.collected.stderr)
247
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `explicit native source build failed: ${stderr.slice(0, 1000)}`)
248
+ }
249
+ const info = await stat(this.helperPath).catch(() => undefined)
250
+ if (info?.isFile() !== true) throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native source build completed without producing the helper')
251
+ }
252
+
253
+ private async getCursor(prepared: PreparedNativeHelper, signal: AbortSignal): Promise<CursorProcess> {
254
+ signal.throwIfAborted()
255
+ if (this.cursor !== undefined) return this.cursor
256
+ if (this.cursorStart === undefined) {
257
+ const start: { promise: Promise<CursorProcess> } = { promise: Promise.resolve(undefined as never) }
258
+ start.promise = this.spawnCursor(prepared).then(cursor => {
259
+ this.cursor = cursor
260
+ void cursor.done.catch(() => undefined).finally(() => {
261
+ if (this.cursor === cursor) this.cursor = undefined
262
+ })
263
+ return cursor
264
+ }).finally(() => {
265
+ if (this.cursorStart === start) this.cursorStart = undefined
266
+ })
267
+ this.cursorStart = start
268
+ }
269
+ const cursor = await this.cursorStart.promise
270
+ signal.throwIfAborted()
271
+ return cursor
272
+ }
273
+
274
+ private async spawnCursor(prepared: PreparedNativeHelper): Promise<CursorProcess> {
275
+ const cursorSignal = new AbortController()
276
+ const handle = this.ctx.subprocess.spawn({
277
+ argv: [prepared.path, '--cursor-overlay'],
278
+ cwd: dirname(prepared.path),
279
+ stdio: {
280
+ stdin: 'pipe',
281
+ stdout: 'pipe',
282
+ stderr: { maxBytes: 64 * 1024 },
283
+ },
284
+ graceMs: 1000,
285
+ signal: cursorSignal.signal,
286
+ env: {
287
+ LANG: process.env.LANG ?? 'en_US.UTF-8',
288
+ LC_ALL: process.env.LC_ALL ?? 'en_US.UTF-8',
289
+ },
290
+ })
291
+ if (handle.stdin === undefined || handle.stdout === undefined) {
292
+ handle.terminate()
293
+ await handle.waitForExit()
294
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native cursor overlay protocol pipes are unavailable')
295
+ }
296
+ handle.stdin.on('error', () => {
297
+ // Process outcome and the next command own recovery from a closed protocol pipe.
298
+ })
299
+ try {
300
+ await this.waitForCursorReady(handle)
301
+ } catch (error) {
302
+ handle.terminate()
303
+ await handle.waitForExit()
304
+ const stderr = collected(handle.collected.stderr).trim()
305
+ throw new ComputerUseError(
306
+ 'COMPUTER_PROVIDER_FAILURE',
307
+ `native cursor overlay failed to become ready${stderr.length === 0 ? '' : `: ${stderr.slice(0, 1000)}`}`,
308
+ { cause: error },
309
+ )
310
+ }
311
+ handle.stdout.resume()
312
+ return {
313
+ stdin: handle.stdin,
314
+ done: handle.done,
315
+ terminate: () => {
316
+ cursorSignal.abort()
317
+ handle.terminate()
318
+ },
319
+ waitForExit: signal => handle.waitForExit(signal),
320
+ }
321
+ }
322
+
323
+ private async waitForCursorReady(handle: SubprocessHandle): Promise<void> {
324
+ const stdout = handle.stdout
325
+ if (stdout === undefined) throw new Error('cursor stdout is unavailable')
326
+ const timeout = AbortSignal.timeout(CURSOR_READY_TIMEOUT_MS)
327
+ await new Promise<void>((resolveReady, rejectReady) => {
328
+ let buffer = ''
329
+ let settled = false
330
+ const cleanup = (): void => {
331
+ clearListeners()
332
+ timeout.removeEventListener('abort', onTimeout)
333
+ }
334
+ const succeed = (): void => {
335
+ if (settled) return
336
+ settled = true
337
+ cleanup()
338
+ resolveReady()
339
+ }
340
+ const fail = (error: unknown): void => {
341
+ if (settled) return
342
+ settled = true
343
+ cleanup()
344
+ rejectReady(error)
345
+ }
346
+ const onData = (chunk: Buffer | string): void => {
347
+ buffer += chunk.toString()
348
+ if (Buffer.byteLength(buffer) > CURSOR_PROTOCOL_MAX_BYTES) {
349
+ fail(new Error('cursor ready response exceeded its protocol limit'))
350
+ return
351
+ }
352
+ while (true) {
353
+ const newline = buffer.indexOf('\n')
354
+ if (newline < 0) return
355
+ const line = buffer.slice(0, newline)
356
+ buffer = buffer.slice(newline + 1)
357
+ if (line.length === 0) continue
358
+ let response: unknown
359
+ try {
360
+ response = JSON.parse(line)
361
+ } catch (error) {
362
+ fail(error)
363
+ return
364
+ }
365
+ if (typeof response === 'object' && response !== null
366
+ && (response as { ok?: unknown }).ok === true
367
+ && (response as { ready?: unknown }).ready === true) {
368
+ succeed()
369
+ return
370
+ }
371
+ fail(new Error('cursor overlay returned an unexpected ready response'))
372
+ return
373
+ }
374
+ }
375
+ const onEnd = (): void => { fail(new Error('cursor overlay stdout closed before ready')) }
376
+ const onError = (error: Error): void => { fail(error) }
377
+ const onTimeout = (): void => { fail(new Error(`cursor overlay ready timeout after ${CURSOR_READY_TIMEOUT_MS} milliseconds`)) }
378
+ const clearListeners = (): void => {
379
+ stdout.removeListener('data', onData)
380
+ stdout.removeListener('end', onEnd)
381
+ stdout.removeListener('error', onError)
382
+ }
383
+ stdout.on('data', onData)
384
+ stdout.once('end', onEnd)
385
+ stdout.once('error', onError)
386
+ timeout.addEventListener('abort', onTimeout, { once: true })
387
+ void handle.done.then(
388
+ outcome => { fail(new Error(`cursor overlay exited before ready (${String(outcome.exitCode ?? outcome.signal)})`)) },
389
+ error => { fail(error) },
390
+ )
391
+ })
392
+ }
393
+ }