@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
package/src/config.ts ADDED
@@ -0,0 +1,196 @@
1
+ /** Validated provider, observation, settlement, artifact, and app-policy configuration. */
2
+
3
+ import z from '@deepseek-ai/schemastery'
4
+ import type Schema from '@deepseek-ai/schemastery'
5
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
6
+ import { ComputerUseError } from './errors.ts'
7
+
8
+ /** Settings document namespace owned by this package. */
9
+ export const COMPUTER_USE_SETTINGS_NAMESPACE = settingsNamespace('computer-use')
10
+
11
+ /** One persisted application grant. Wildcards are intentionally unsupported. */
12
+ export interface ComputerUseAppGrant {
13
+ bundleId: string
14
+ read?: boolean
15
+ control?: boolean
16
+ }
17
+
18
+ /** Host-owned policy for foreground activation, keyboard routing, target-process input, and the visible Agent cursor. */
19
+ export interface ComputerUseInteractionConfig {
20
+ focusPolicy?: 'preserve' | 'activate'
21
+ keyboardPolicy?: 'preserve' | 'activate'
22
+ pointerInputPolicy?: 'deny' | 'targeted'
23
+ cursorVisualization?: 'hidden' | 'visible'
24
+ cursorMotionMs?: number
25
+ cursorAutoHideMs?: number
26
+ }
27
+
28
+ /** User-facing configuration; schema defaults are repeated by {@link resolveConfig}. `observationTtlMs: 0` disables observation expiry. */
29
+ export interface ComputerUseConfig {
30
+ observationTtlMs?: number
31
+ confirmationTtlMs?: number
32
+ actionTimeoutMs?: number
33
+ settleMs?: number
34
+ maxSettleMs?: number
35
+ maxNodes?: number
36
+ maxDepth?: number
37
+ maxTextBytes?: number
38
+ maxScreenshotBytes?: number
39
+ artifactRoot?: string
40
+ helper?: {
41
+ path?: string
42
+ allowSourceBuild?: boolean
43
+ }
44
+ interaction?: ComputerUseInteractionConfig
45
+ allowAllApps?: boolean
46
+ grants?: ComputerUseAppGrant[]
47
+ }
48
+
49
+ /** Configuration schema used by Cordis and the Settings provider. */
50
+ export const Config: Schema<ComputerUseConfig> = z.object({
51
+ observationTtlMs: z.number().default(0),
52
+ confirmationTtlMs: z.number().default(300000),
53
+ actionTimeoutMs: z.number().default(15000),
54
+ settleMs: z.number().default(250),
55
+ maxSettleMs: z.number().default(5000),
56
+ maxNodes: z.number().default(500),
57
+ maxDepth: z.number().default(14),
58
+ maxTextBytes: z.number().default(64000),
59
+ maxScreenshotBytes: z.number().default(33554432),
60
+ artifactRoot: z.string().default('.dsh-computer-use/artifacts'),
61
+ helper: z.object({
62
+ path: z.string(),
63
+ allowSourceBuild: z.boolean().default(false),
64
+ }),
65
+ interaction: z.object({
66
+ focusPolicy: z.union(['preserve', 'activate']).default('preserve'),
67
+ keyboardPolicy: z.union(['preserve', 'activate']).default('preserve'),
68
+ pointerInputPolicy: z.union(['deny', 'targeted']).default('targeted'),
69
+ cursorVisualization: z.union(['hidden', 'visible']).default('visible'),
70
+ cursorMotionMs: z.number().default(180),
71
+ cursorAutoHideMs: z.number().default(0),
72
+ }),
73
+ allowAllApps: z.boolean().default(false),
74
+ grants: z.array(z.object({
75
+ bundleId: z.string(),
76
+ read: z.boolean().default(false),
77
+ control: z.boolean().default(false),
78
+ })).default([]),
79
+ })
80
+
81
+ /** Fully defaulted configuration consumed at runtime. */
82
+ export interface ResolvedComputerUseConfig {
83
+ observationTtlMs: number
84
+ confirmationTtlMs: number
85
+ actionTimeoutMs: number
86
+ settleMs: number
87
+ maxSettleMs: number
88
+ maxNodes: number
89
+ maxDepth: number
90
+ maxTextBytes: number
91
+ maxScreenshotBytes: number
92
+ artifactRoot: string
93
+ helper: {
94
+ path?: string
95
+ allowSourceBuild: boolean
96
+ }
97
+ interaction: {
98
+ focusPolicy: 'preserve' | 'activate'
99
+ keyboardPolicy: 'preserve' | 'activate'
100
+ pointerInputPolicy: 'deny' | 'targeted'
101
+ cursorVisualization: 'hidden' | 'visible'
102
+ cursorMotionMs: number
103
+ cursorAutoHideMs: number
104
+ }
105
+ allowAllApps: boolean
106
+ grants: Array<{
107
+ bundleId: string
108
+ read: boolean
109
+ control: boolean
110
+ }>
111
+ }
112
+
113
+ function integer(name: string, value: number, min: number, max: number): number {
114
+ if (!Number.isInteger(value) || value < min || value > max) {
115
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `${name} must be an integer between ${min} and ${max}`)
116
+ }
117
+ return value
118
+ }
119
+
120
+ function option<T extends string>(name: string, value: string, allowed: readonly T[]): T {
121
+ if (!allowed.includes(value as T)) {
122
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `${name} must be one of ${allowed.join(', ')}`)
123
+ }
124
+ return value as T
125
+ }
126
+
127
+ /** Validate and normalize one raw config object. */
128
+ export function resolveConfig(config: ComputerUseConfig = {}): ResolvedComputerUseConfig {
129
+ const observationTtl = config.observationTtlMs ?? 0
130
+ const observationTtlMs = observationTtl === 0 ? 0 : integer('observationTtlMs', observationTtl, 1000, 86400000)
131
+ const confirmationTtlMs = integer('confirmationTtlMs', config.confirmationTtlMs ?? 300000, 1000, 900000)
132
+ const actionTimeoutMs = integer('actionTimeoutMs', config.actionTimeoutMs ?? 15000, 1000, 120000)
133
+ const settleMs = integer('settleMs', config.settleMs ?? 250, 0, 10000)
134
+ const maxSettleMs = integer('maxSettleMs', config.maxSettleMs ?? 5000, 100, 60000)
135
+ if (settleMs > maxSettleMs) {
136
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'settleMs must be no greater than maxSettleMs')
137
+ }
138
+ const maxNodes = integer('maxNodes', config.maxNodes ?? 500, 10, 5000)
139
+ const maxDepth = integer('maxDepth', config.maxDepth ?? 14, 1, 64)
140
+ const maxTextBytes = integer('maxTextBytes', config.maxTextBytes ?? 64000, 1024, 1048576)
141
+ const maxScreenshotBytes = integer('maxScreenshotBytes', config.maxScreenshotBytes ?? 33554432, 1024, 268435456)
142
+ const artifactRoot = (config.artifactRoot ?? '.dsh-computer-use/artifacts').trim()
143
+ if (artifactRoot.length === 0 || artifactRoot.startsWith('/') || artifactRoot.split(/[\\/]+/u).includes('..')) {
144
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'artifactRoot must be a non-empty workspace-relative path without ..')
145
+ }
146
+ const helperPath = config.helper?.path?.trim()
147
+ if (helperPath !== undefined && helperPath.length === 0) {
148
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'helper.path must not be empty')
149
+ }
150
+ const focusPolicy = option('interaction.focusPolicy', config.interaction?.focusPolicy ?? 'preserve', ['preserve', 'activate'] as const)
151
+ const keyboardPolicy = option('interaction.keyboardPolicy', config.interaction?.keyboardPolicy ?? 'preserve', ['preserve', 'activate'] as const)
152
+ const pointerInputPolicy = option('interaction.pointerInputPolicy', config.interaction?.pointerInputPolicy ?? 'targeted', ['deny', 'targeted'] as const)
153
+ const cursorVisualization = option('interaction.cursorVisualization', config.interaction?.cursorVisualization ?? 'visible', ['hidden', 'visible'] as const)
154
+ const cursorMotionMs = integer('interaction.cursorMotionMs', config.interaction?.cursorMotionMs ?? 180, 0, 2000)
155
+ const cursorAutoHideMs = integer('interaction.cursorAutoHideMs', config.interaction?.cursorAutoHideMs ?? 0, 0, 30000)
156
+ const allowAllApps = config.allowAllApps ?? false
157
+ const seen = new Set<string>()
158
+ const grants = (config.grants ?? []).map((grant) => {
159
+ const bundleId = grant.bundleId.trim()
160
+ if (bundleId.length === 0 || bundleId === '*' || bundleId.includes('*')) {
161
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'grants[].bundleId must be one exact non-wildcard bundle id')
162
+ }
163
+ if (seen.has(bundleId)) {
164
+ throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `duplicate app grant for ${bundleId}`)
165
+ }
166
+ seen.add(bundleId)
167
+ const control = grant.control ?? false
168
+ return { bundleId, read: (grant.read ?? false) || control, control }
169
+ })
170
+ return {
171
+ observationTtlMs,
172
+ confirmationTtlMs,
173
+ actionTimeoutMs,
174
+ settleMs,
175
+ maxSettleMs,
176
+ maxNodes,
177
+ maxDepth,
178
+ maxTextBytes,
179
+ maxScreenshotBytes,
180
+ artifactRoot,
181
+ helper: {
182
+ ...(helperPath === undefined ? {} : { path: helperPath }),
183
+ allowSourceBuild: config.helper?.allowSourceBuild ?? false,
184
+ },
185
+ interaction: {
186
+ focusPolicy,
187
+ keyboardPolicy,
188
+ pointerInputPolicy,
189
+ cursorVisualization,
190
+ cursorMotionMs,
191
+ cursorAutoHideMs,
192
+ },
193
+ allowAllApps,
194
+ grants,
195
+ }
196
+ }
@@ -0,0 +1,128 @@
1
+ /** One-use just-in-time confirmation tokens for sensitive Computer Use actions. */
2
+
3
+ import { createHash, randomUUID } from 'node:crypto'
4
+ import type { Agent } from '@deepseek-ai/dsh-agent'
5
+ import type { Context } from '@deepseek-ai/cordis'
6
+ import { approvalPolicy } from './approval-policy.ts'
7
+ import type { ResolvedComputerUseConfig } from './config.ts'
8
+ import { ComputerUseError } from './errors.ts'
9
+ import {
10
+ ComputerConfirmationToken,
11
+ type ComputerActionRequest,
12
+ type ComputerAppIdentity,
13
+ type ComputerConfirmRequest,
14
+ type ComputerConfirmation,
15
+ type ComputerObservationId,
16
+ } from './types.ts'
17
+
18
+ interface ConfirmationRecord {
19
+ app: ComputerAppIdentity
20
+ observationId: ComputerObservationId
21
+ actionHash: string
22
+ expiresAt: number
23
+ }
24
+
25
+ function stable(value: unknown): string {
26
+ if (value === null || typeof value !== 'object') return JSON.stringify(value)
27
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`
28
+ const record = value as Record<string, unknown>
29
+ return `{${Object.keys(record).sort().filter(key => record[key] !== undefined).map(key => `${JSON.stringify(key)}:${stable(record[key])}`).join(',')}}`
30
+ }
31
+
32
+ function actionHash(action: Omit<ComputerActionRequest, 'confirmationToken'> | ComputerActionRequest): string {
33
+ const { confirmationToken: _token, ...rest } = action as ComputerActionRequest
34
+ return createHash('sha256').update(stable(rest)).digest('hex')
35
+ }
36
+
37
+ /** Issues, validates, consumes, and releases scoped sensitive-action tokens. */
38
+ export class ComputerConfirmationManager {
39
+ private readonly records = new Map<Agent, Map<ComputerConfirmationToken, ConfirmationRecord>>()
40
+
41
+ constructor(
42
+ private readonly ctx: Context,
43
+ private readonly config: () => ResolvedComputerUseConfig,
44
+ ) {}
45
+
46
+ /** Request user approval and mint one token bound to the exact action. */
47
+ async confirm(
48
+ agent: Agent,
49
+ app: ComputerAppIdentity,
50
+ request: ComputerConfirmRequest,
51
+ callId: import('@deepseek-ai/dsh-llm').CallId | undefined,
52
+ signal: AbortSignal,
53
+ ): Promise<ComputerConfirmation> {
54
+ if (approvalPolicy(this.ctx, agent) === 'never') {
55
+ throw new ComputerUseError(
56
+ 'COMPUTER_CONFIRMATION_REQUIRED',
57
+ 'sensitive action confirmation is blocked because approval prompts are disabled in this Session (approval/policy: never); do not execute the action, and ask the user to switch the permission preset to one with approval ask or run it manually',
58
+ )
59
+ }
60
+ const outcome = await this.ctx.approval.request({
61
+ agent,
62
+ toolName: 'computer_confirm',
63
+ ...(callId === undefined ? {} : { callId }),
64
+ reason: `${request.reason} Target: ${request.target}.${request.dataSummary === undefined ? '' : ` Data: ${request.dataSummary}.`}`,
65
+ signal,
66
+ })
67
+ if (outcome === 'cancelled') throw new ComputerUseError('COMPUTER_CANCELLED', 'sensitive action confirmation was cancelled')
68
+ if (outcome !== 'allowed-once') {
69
+ throw new ComputerUseError('COMPUTER_CONFIRMATION_REQUIRED', `sensitive action was not confirmed (${outcome})`)
70
+ }
71
+ const token = ComputerConfirmationToken(randomUUID())
72
+ const expiresAt = Date.now() + this.config().confirmationTtlMs
73
+ let agentRecords = this.records.get(agent)
74
+ if (agentRecords === undefined) {
75
+ agentRecords = new Map()
76
+ this.records.set(agent, agentRecords)
77
+ }
78
+ agentRecords.set(token, {
79
+ app,
80
+ observationId: request.action.observationId,
81
+ actionHash: actionHash(request.action),
82
+ expiresAt,
83
+ })
84
+ return { token, observationId: request.action.observationId, app, expiresAt: new Date(expiresAt).toISOString() }
85
+ }
86
+
87
+ /** Require and consume the one matching token when an action is marked sensitive. */
88
+ consume(agent: Agent, app: ComputerAppIdentity, action: ComputerActionRequest): void {
89
+ if (action.sensitive !== true) {
90
+ if (action.confirmationToken !== undefined) {
91
+ throw new ComputerUseError('COMPUTER_CONFIRMATION_REQUIRED', 'confirmationToken is valid only when sensitive is true')
92
+ }
93
+ return
94
+ }
95
+ const token = action.confirmationToken
96
+ if (token === undefined) {
97
+ throw new ComputerUseError('COMPUTER_CONFIRMATION_REQUIRED', 'sensitive action requires a token from computer_confirm')
98
+ }
99
+ const agentRecords = this.records.get(agent)
100
+ const record = agentRecords?.get(token)
101
+ if (record === undefined) {
102
+ throw new ComputerUseError('COMPUTER_CONFIRMATION_REQUIRED', 'confirmation token is unknown, expired, or already consumed')
103
+ }
104
+ agentRecords?.delete(token)
105
+ if (record.expiresAt < Date.now()) {
106
+ throw new ComputerUseError('COMPUTER_CONFIRMATION_REQUIRED', 'confirmation token expired')
107
+ }
108
+ if (record.app.bundleId !== app.bundleId || record.app.pid !== app.pid
109
+ || record.observationId !== action.observationId || record.actionHash !== actionHash(action)) {
110
+ throw new ComputerUseError('COMPUTER_CONFIRMATION_REQUIRED', 'confirmation token does not match this app, observation, or action')
111
+ }
112
+ }
113
+
114
+ /** Invalidate one pending token after target identity changes before input. */
115
+ invalidate(agent: Agent, token: ComputerConfirmationToken | undefined): void {
116
+ if (token !== undefined) this.records.get(agent)?.delete(token)
117
+ }
118
+
119
+ /** Release every pending token owned by one Agent. */
120
+ releaseAgent(agent: Agent): void {
121
+ this.records.delete(agent)
122
+ }
123
+
124
+ /** Release all pending tokens on provider teardown or generation replacement. */
125
+ clear(): void {
126
+ this.records.clear()
127
+ }
128
+ }
package/src/diff.ts ADDED
@@ -0,0 +1,52 @@
1
+ /** Accessibility-tree diff projection for model-context efficiency. */
2
+
3
+ import type { ComputerElement } from './types.ts'
4
+
5
+ function identity(element: ComputerElement): string {
6
+ const frame = element.frame === undefined
7
+ ? ''
8
+ : `${Math.round(element.frame.x)},${Math.round(element.frame.y)},${Math.round(element.frame.width)},${Math.round(element.frame.height)}`
9
+ return [element.role, element.subrole ?? '', element.title ?? '', element.label ?? '', frame].join('|')
10
+ }
11
+
12
+ function summary(element: ComputerElement, includeIndex: boolean): string {
13
+ const parts = [includeIndex ? `[${element.index}]` : undefined, element.role]
14
+ if (element.title !== undefined) parts.push(JSON.stringify(element.title))
15
+ else if (element.label !== undefined) parts.push(JSON.stringify(element.label))
16
+ if (element.value !== undefined) parts.push(`value=${JSON.stringify(element.value)}`)
17
+ if (element.enabled === false) parts.push('disabled')
18
+ if (element.focused === true) parts.push('focused')
19
+ if (element.selected === true) parts.push('selected')
20
+ return parts.filter((part): part is string => part !== undefined).join(' ')
21
+ }
22
+
23
+ function state(element: ComputerElement): string {
24
+ return JSON.stringify({
25
+ value: element.value,
26
+ enabled: element.enabled,
27
+ focused: element.focused,
28
+ selected: element.selected,
29
+ actions: element.actions,
30
+ })
31
+ }
32
+
33
+ /** Return a bounded full-to-full element diff whose current rows use current indexes. */
34
+ export function diffElements(previous: readonly ComputerElement[], current: readonly ComputerElement[], maxBytes: number): string {
35
+ const before = new Map(previous.map(element => [identity(element), element]))
36
+ const after = new Map(current.map(element => [identity(element), element]))
37
+ const lines: string[] = []
38
+ for (const [key, element] of before) {
39
+ if (!after.has(key)) lines.push(`- ${summary(element, false)}`)
40
+ }
41
+ for (const [key, element] of after) {
42
+ const old = before.get(key)
43
+ if (old === undefined) lines.push(`+ ${summary(element, true)}`)
44
+ else if (state(old) !== state(element)) lines.push(`~ ${summary(element, true)}`)
45
+ }
46
+ if (lines.length === 0) return '(no accessibility changes)'
47
+ const text = lines.join('\n')
48
+ const bytes = Buffer.byteLength(text)
49
+ if (bytes <= maxBytes) return text
50
+ const suffix = '\n… diff truncated'
51
+ return `${Buffer.from(text).subarray(0, Math.max(0, maxBytes - Buffer.byteLength(suffix))).toString('utf8')}${suffix}`
52
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,39 @@
1
+ /** Stable Computer Use failure codes returned across provider and Tool boundaries. */
2
+ export type ComputerUseErrorCode =
3
+ | 'COMPUTER_UNSUPPORTED_PLATFORM'
4
+ | 'COMPUTER_PERMISSION_REQUIRED'
5
+ | 'COMPUTER_APP_NOT_FOUND'
6
+ | 'COMPUTER_STALE_OBSERVATION'
7
+ | 'COMPUTER_ELEMENT_UNAVAILABLE'
8
+ | 'COMPUTER_TARGET_UNAVAILABLE'
9
+ | 'COMPUTER_TARGET_AMBIGUOUS'
10
+ | 'COMPUTER_TARGET_LOW_CONFIDENCE'
11
+ | 'COMPUTER_TARGET_REBIND_REQUIRES_CONFIRMATION'
12
+ | 'COMPUTER_CONFIRMATION_REQUIRED'
13
+ | 'COMPUTER_ACTION_BLOCKED'
14
+ | 'COMPUTER_TIMEOUT'
15
+ | 'COMPUTER_CANCELLED'
16
+ | 'COMPUTER_PROVIDER_FAILURE'
17
+
18
+ /** Error with a stable model-visible code and bounded public details. */
19
+ export class ComputerUseError extends Error {
20
+ readonly code: ComputerUseErrorCode
21
+
22
+ /**
23
+ * @param code - Stable failure category.
24
+ * @param message - Bounded correction-oriented description without UI secrets.
25
+ * @param options - Optional original cause retained outside the model-facing message.
26
+ */
27
+ constructor(code: ComputerUseErrorCode, message: string, options?: ErrorOptions) {
28
+ super(`${code}: ${message}`, options)
29
+ this.name = 'ComputerUseError'
30
+ this.code = code
31
+ }
32
+ }
33
+
34
+ /** Convert an unknown failure into the provider-failure category without leaking unbounded native text. */
35
+ export function computerUseError(error: unknown, fallback: string): ComputerUseError {
36
+ if (error instanceof ComputerUseError) return error
37
+ const message = error instanceof Error ? error.message : String(error)
38
+ return new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `${fallback}: ${message.slice(0, 1000)}`, { cause: error })
39
+ }
@@ -0,0 +1,236 @@
1
+ /** Agent-scoped progressive exposure for Computer Use execution Tools. */
2
+
3
+ import type { Agent } from '@deepseek-ai/dsh-agent'
4
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
5
+ import type { Session } from '@deepseek-ai/dsh-session'
6
+ import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
7
+ import type { Context } from '@deepseek-ai/cordis'
8
+ import { COMPUTER_USE_SKILL_CONTENT, COMPUTER_USE_SKILL_NAME } from './skill.ts'
9
+
10
+ /** One global bootstrap retained until the current Agent loads the Skill. */
11
+ export const COMPUTER_USE_ACTIVATE = 'computer_use_activate'
12
+
13
+ const VISION_TOOL_NAMES = [
14
+ 'vision_glance',
15
+ 'vision_ground',
16
+ 'vision_detect',
17
+ 'vision_crop',
18
+ 'vision_long_screenshot_ocr',
19
+ 'vision_toolkit_activate',
20
+ ] as const
21
+
22
+ const COMMAND_PREFIX = /^(?:(?:[A-Za-z_][A-Za-z0-9_]*=\S+)\s+)*(?:(?:\S*\/)?(?:sudo|env|nohup|time|command|exec)\s+(?:(?:-\S+|[A-Za-z_][A-Za-z0-9_]*=\S+)\s+)*)?/iu
23
+ const OCR_COMMAND_PROBE = /^(?:which|type|command\s+-v)\b[^\n;&|]*\b(?:tesseract|screencapture)\b/iu
24
+ const OCR_EXECUTABLE = /^(?:\S*\/)?(?:tesseract|screencapture)(?:\s|$)/iu
25
+ const OCR_STACK_SETUP = /^(?:(?:\S*\/)?(?:brew|port|apt(?:-get)?|dnf|yum|pacman)\b[^\n]*(?:install|add)\b[^\n]*\btesseract(?:-ocr)?\b|(?:\S*\/)?(?:pip(?:3(?:\.\d+)*)?|uv\s+pip|poetry\s+add|pdm\s+add)\b[^\n]*(?:install|add)?[^\n]*\b(?:pytesseract|easyocr|ocrmypdf)\b)/iu
26
+ const SCRIPTED_OCR_EXECUTABLE = /^(?:\S*\/)?(?:python(?:3(?:\.\d+)*)?|swift)(?:\s|$)/iu
27
+ const SCRIPTED_OCR_MARKER = /(?:\bpytesseract\b|\beasyocr\b|\bocrmypdf\b|VNRecognizeTextRequest|\bimport\s+Vision\b)/iu
28
+ const SHELL_META = /(?:&&|\|\||[;|\n])/u
29
+
30
+ interface AgentExposure {
31
+ active: boolean
32
+ hideActivation?: () => void
33
+ toolDisposers: Array<() => void>
34
+ toolNames: string[]
35
+ }
36
+
37
+ /** Activation result returned to the model. */
38
+ export interface ComputerUseActivationResult {
39
+ activated: boolean
40
+ tools: string[]
41
+ }
42
+
43
+ function renderJson(_args: unknown, value: unknown): ContentBlock[] {
44
+ return [{ type: 'text', text: JSON.stringify(value, null, 2) }]
45
+ }
46
+
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
49
+ }
50
+
51
+ function adHocOcrCommand(value: unknown): boolean {
52
+ if (!isRecord(value) || typeof value.command !== 'string') return false
53
+ const commands = value.command.split(SHELL_META)
54
+ .map(command => command.trimStart().replace(COMMAND_PREFIX, ''))
55
+ if (commands.some(command => OCR_COMMAND_PROBE.test(command)
56
+ || OCR_EXECUTABLE.test(command)
57
+ || OCR_STACK_SETUP.test(command))) return true
58
+ return SCRIPTED_OCR_MARKER.test(value.command)
59
+ && commands.some(command => SCRIPTED_OCR_EXECUTABLE.test(command))
60
+ }
61
+
62
+ function isSkillArguments(value: unknown): boolean {
63
+ return isRecord(value) && value.name === COMPUTER_USE_SKILL_NAME
64
+ }
65
+
66
+ function nativeSkillCall(raw: string): boolean {
67
+ try { return isSkillArguments(JSON.parse(raw)) } catch { return false }
68
+ }
69
+
70
+ function containsSkillContent(blocks: readonly unknown[]): boolean {
71
+ return blocks.some(block => isRecord(block)
72
+ && block.type === 'text'
73
+ && typeof block.text === 'string'
74
+ && block.text.includes(COMPUTER_USE_SKILL_CONTENT))
75
+ }
76
+
77
+ function isSkillResult(value: unknown): boolean {
78
+ return isRecord(value)
79
+ && value.name === COMPUTER_USE_SKILL_NAME
80
+ && value.content === COMPUTER_USE_SKILL_CONTENT
81
+ }
82
+
83
+ /** Whether durable Session history proves that the bundled Skill was loaded. */
84
+ export function hasLoadedComputerUseSkill(session: Session): boolean {
85
+ const nativeCalls = new Set<string>()
86
+ for (const event of session.events) {
87
+ if (event.type === 'user/message') {
88
+ const source = event.data.source
89
+ if (source.kind === 'skill-invocation'
90
+ && source.name === COMPUTER_USE_SKILL_NAME
91
+ && containsSkillContent(event.data.content)) return true
92
+ continue
93
+ }
94
+ if (event.type === 'tool/call') {
95
+ if (event.data.name === 'skill' && nativeSkillCall(event.data.arguments)) nativeCalls.add(String(event.data.callId))
96
+ continue
97
+ }
98
+ if (event.type === 'tool/result') {
99
+ const [block] = event.data.message.content
100
+ if (block?.type === 'tool-result'
101
+ && block.isError !== true
102
+ && nativeCalls.has(String(block.toolCallId))
103
+ && containsSkillContent(block.content)) return true
104
+ continue
105
+ }
106
+ if (event.type === 'tool/code-dispatch'
107
+ && event.data.name === 'skill'
108
+ && event.data.isError === false
109
+ && isSkillArguments(event.data.arguments)
110
+ && containsSkillContent(event.data.content)) return true
111
+ }
112
+ return false
113
+ }
114
+
115
+ /** Owns one progressive Tool-exposure generation. */
116
+ export class ComputerUseExposure {
117
+ readonly activationTool: ToolDefinition
118
+ private readonly states = new Map<Agent, AgentExposure>()
119
+ private installed = false
120
+
121
+ constructor(
122
+ private readonly ctx: Context,
123
+ private readonly createTools: () => ToolDefinition[],
124
+ ) {
125
+ this.activationTool = defineTool({
126
+ name: COMPUTER_USE_ACTIVATE,
127
+ description: `Activate the macOS Computer Use execution tools for this Agent after loading the ${COMPUTER_USE_SKILL_NAME} Skill. The Skill tool normally activates them automatically; call this only after a direct Skill invocation when the tools are still absent. This bootstrap disappears after success.`,
128
+ parameters: {},
129
+ output: {
130
+ schema: {
131
+ type: 'object',
132
+ additionalProperties: false,
133
+ properties: {
134
+ activated: { type: 'boolean', required: true },
135
+ tools: { type: 'array', items: { type: 'string' }, required: true },
136
+ },
137
+ },
138
+ render: renderJson,
139
+ },
140
+ execute: (_args, exec): Promise<ComputerUseActivationResult> => {
141
+ if (exec.agent === undefined) throw new Error(`${COMPUTER_USE_ACTIVATE}: an Agent Session is required`)
142
+ if (!hasLoadedComputerUseSkill(exec.agent.session)) {
143
+ throw new Error(`${COMPUTER_USE_ACTIVATE}: load the ${COMPUTER_USE_SKILL_NAME} Skill first`)
144
+ }
145
+ return Promise.resolve(this.activate(exec.agent))
146
+ },
147
+ presentCall: () => ({ card: 'generic', title: 'Activate Computer Use', kind: 'execute' }),
148
+ })
149
+ }
150
+
151
+ /** Install lifecycle listeners and adopt existing Agents. */
152
+ install(): () => void {
153
+ if (this.installed) throw new Error('dsh-computer-use: progressive exposure is already installed')
154
+ this.installed = true
155
+ const effects = [
156
+ this.ctx.on('agent/created', ({ agent }) => { this.attach(agent) }),
157
+ this.ctx.on('agent/disposed', ({ agent }) => { this.detach(agent) }),
158
+ this.ctx.tools.guard((exec) => {
159
+ if (exec.name !== 'bash'
160
+ || exec.agent === undefined
161
+ || !hasLoadedComputerUseSkill(exec.agent.session)
162
+ || !adHocOcrCommand(exec.arguments)
163
+ || !VISION_TOOL_NAMES.some(name => this.ctx.tools.get(name, exec.agent) !== undefined)) return undefined
164
+ return 'Computer Use screenshot analysis must use the installed Vision Toolkit instead of a shell-built OCR stack. If vision_glance is absent, call the skill tool with {"name":"vision-tools"}; then pass the existing screenshot Artifact path to vision_glance, vision_ground, vision_detect, vision_crop, or vision_long_screenshot_ocr.'
165
+ }),
166
+ this.ctx.on('tools/result', (exec, result) => {
167
+ if (result.isError === false
168
+ && exec.name === 'skill'
169
+ && exec.agent !== undefined
170
+ && isSkillArguments(exec.arguments)
171
+ && isSkillResult(result.value)) this.activate(exec.agent)
172
+ return undefined
173
+ }),
174
+ ]
175
+ try {
176
+ for (const agent of this.ctx.agents.list()) this.attach(agent)
177
+ } catch (error) {
178
+ for (const dispose of effects.reverse()) dispose()
179
+ this.disposeStates()
180
+ this.installed = false
181
+ throw error
182
+ }
183
+ return () => {
184
+ if (!this.installed) return
185
+ this.installed = false
186
+ for (const dispose of effects.reverse()) dispose()
187
+ this.disposeStates()
188
+ }
189
+ }
190
+
191
+ private attach(agent: Agent): void {
192
+ if (this.states.has(agent)) return
193
+ this.states.set(agent, { active: false, toolDisposers: [], toolNames: [] })
194
+ if (hasLoadedComputerUseSkill(agent.session)) this.activate(agent)
195
+ }
196
+
197
+ private activate(agent: Agent): ComputerUseActivationResult {
198
+ this.attach(agent)
199
+ const state = this.states.get(agent)
200
+ if (state === undefined) throw new Error(`dsh-computer-use: Agent ${String(agent.id)} has no exposure state`)
201
+ if (state.active) return { activated: false, tools: [...state.toolNames] }
202
+ const definitions = this.createTools()
203
+ const toolDisposers: Array<() => void> = []
204
+ let hideActivation: (() => void) | undefined
205
+ try {
206
+ for (const definition of definitions) toolDisposers.push(agent.ctx.tools.register(definition))
207
+ hideActivation = agent.ctx.tools.restrict({ deny: [COMPUTER_USE_ACTIVATE] })
208
+ } catch (error) {
209
+ hideActivation?.()
210
+ for (const dispose of toolDisposers.reverse()) dispose()
211
+ throw error
212
+ }
213
+ state.active = true
214
+ state.hideActivation = hideActivation
215
+ state.toolDisposers = toolDisposers
216
+ state.toolNames = definitions.map(definition => definition.name)
217
+ return { activated: true, tools: [...state.toolNames] }
218
+ }
219
+
220
+ private detach(agent: Agent): void {
221
+ const state = this.states.get(agent)
222
+ if (state === undefined) return
223
+ this.states.delete(agent)
224
+ this.disposeState(state)
225
+ }
226
+
227
+ private disposeStates(): void {
228
+ for (const state of this.states.values()) this.disposeState(state)
229
+ this.states.clear()
230
+ }
231
+
232
+ private disposeState(state: AgentExposure): void {
233
+ state.hideActivation?.()
234
+ for (const dispose of state.toolDisposers.reverse()) dispose()
235
+ }
236
+ }