@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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +307 -0
- package/README.zh.md +307 -0
- package/assets/computer-use-fixture.png +0 -0
- package/assets/cursor.png +0 -0
- package/cordis.patch.yml +13 -0
- package/docs/interaction-policy.i18n.yaml +4 -0
- package/docs/interaction-policy.md +140 -0
- package/docs/interaction-policy.zh.md +140 -0
- package/lib/approval-policy.js +12 -0
- package/lib/approval-policy.js.map +1 -0
- package/lib/artifacts.js +79 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/backend.js +3 -0
- package/lib/backend.js.map +1 -0
- package/lib/client.js +349 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +120 -0
- package/lib/config.js.map +1 -0
- package/lib/confirmations.js +99 -0
- package/lib/confirmations.js.map +1 -0
- package/lib/diff.js +58 -0
- package/lib/diff.js.map +1 -0
- package/lib/errors.js +22 -0
- package/lib/errors.js.map +1 -0
- package/lib/exposure.js +231 -0
- package/lib/exposure.js.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/leases.js +259 -0
- package/lib/leases.js.map +1 -0
- package/lib/providers/macos.js +148 -0
- package/lib/providers/macos.js.map +1 -0
- package/lib/providers/native-helper.js +356 -0
- package/lib/providers/native-helper.js.map +1 -0
- package/lib/service.js +480 -0
- package/lib/service.js.map +1 -0
- package/lib/skill.js +119 -0
- package/lib/skill.js.map +1 -0
- package/lib/target-resolver.js +180 -0
- package/lib/target-resolver.js.map +1 -0
- package/lib/tools.js +493 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/approval-policy.d.ts +13 -0
- package/lib/types/approval-policy.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +10 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/backend.d.ts +94 -0
- package/lib/types/backend.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +78 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/config.d.ts +75 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/confirmations.d.ts +23 -0
- package/lib/types/confirmations.d.ts.map +1 -0
- package/lib/types/diff.d.ts +5 -0
- package/lib/types/diff.d.ts.map +1 -0
- package/lib/types/errors.d.ts +15 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +30 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/leases.d.ts +80 -0
- package/lib/types/leases.d.ts.map +1 -0
- package/lib/types/providers/macos.d.ts +15 -0
- package/lib/types/providers/macos.d.ts.map +1 -0
- package/lib/types/providers/native-helper.d.ts +36 -0
- package/lib/types/providers/native-helper.d.ts.map +1 -0
- package/lib/types/service.d.ts +52 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/skill.d.ts +9 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/target-resolver.d.ts +38 -0
- package/lib/types/target-resolver.d.ts.map +1 -0
- package/lib/types/tools.d.ts +6 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/types.d.ts +246 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/web.d.ts +32 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/types.js +9 -0
- package/lib/types.js.map +1 -0
- package/lib/web.js +163 -0
- package/lib/web.js.map +1 -0
- package/native/macos/Sources/Fixture/main.swift +366 -0
- package/native/macos/Sources/Helper/CursorImage.swift +11 -0
- package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
- package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
- package/native/macos/Sources/Helper/main.swift +1140 -0
- package/native/macos/Sources/Monitor/main.swift +139 -0
- package/native/macos/bin/dsh-computer-use-helper +0 -0
- package/native/macos/manifest.json +14 -0
- package/package.json +128 -0
- package/scripts/build-client.mjs +24 -0
- package/scripts/build-native.mjs +158 -0
- package/scripts/check-native.mjs +92 -0
- package/scripts/model-e2e.mjs +291 -0
- package/scripts/session-transcript.mjs +117 -0
- package/scripts/validate.mjs +256 -0
- package/src/approval-policy.ts +16 -0
- package/src/artifacts.ts +93 -0
- package/src/backend.ts +103 -0
- package/src/client/index.tsx +539 -0
- package/src/config.ts +196 -0
- package/src/confirmations.ts +128 -0
- package/src/diff.ts +52 -0
- package/src/errors.ts +39 -0
- package/src/exposure.ts +236 -0
- package/src/index.ts +66 -0
- package/src/leases.ts +337 -0
- package/src/providers/macos.ts +185 -0
- package/src/providers/native-helper.ts +393 -0
- package/src/service.ts +583 -0
- package/src/skill.ts +123 -0
- package/src/target-resolver.ts +237 -0
- package/src/tools.ts +528 -0
- package/src/types.ts +299 -0
- 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
|
+
}
|
package/src/exposure.ts
ADDED
|
@@ -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
|
+
}
|