@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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { constants } from 'node:fs'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { dirname, join, resolve } from 'node:path'
|
|
8
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { findComputerClickEvidence } from './session-transcript.mjs'
|
|
11
|
+
|
|
12
|
+
const ROOT = dirname(fileURLToPath(new URL('../package.json', import.meta.url)))
|
|
13
|
+
const HELPER = join(ROOT, 'native', 'macos', 'bin', 'dsh-computer-use-helper')
|
|
14
|
+
const FIXTURE_APP = join(ROOT, 'native', 'macos', 'fixture', 'DSHComputerUseFixture.app')
|
|
15
|
+
const BUNDLE_ID = 'io.anionex.dsh-computer-use-fixture'
|
|
16
|
+
const RESULTS = []
|
|
17
|
+
const TEMPORARY = []
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const options = { keepTemp: false, output: undefined }
|
|
21
|
+
const values = [...argv]
|
|
22
|
+
while (values.length > 0) {
|
|
23
|
+
const option = values.shift()
|
|
24
|
+
if (option === '--keep-temp') options.keepTemp = true
|
|
25
|
+
else if (option === '--output') {
|
|
26
|
+
const value = values.shift()
|
|
27
|
+
if (value === undefined) throw new Error('--output needs a value')
|
|
28
|
+
options.output = resolve(value)
|
|
29
|
+
} else throw new Error(`unknown option: ${option}`)
|
|
30
|
+
}
|
|
31
|
+
return options
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requiredEnvironment(name) {
|
|
35
|
+
const value = process.env[name]?.trim()
|
|
36
|
+
if (!value) throw new Error(`${name} is required for the real-model lane`)
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tail(value, length = 8000) {
|
|
41
|
+
return value.length <= length ? value : value.slice(-length)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runCommand(name, command, args, options = {}) {
|
|
45
|
+
const startedAt = Date.now()
|
|
46
|
+
const result = await new Promise((resolveResult) => {
|
|
47
|
+
const child = spawn(command, args, {
|
|
48
|
+
cwd: options.cwd ?? ROOT,
|
|
49
|
+
env: { ...process.env, ...options.env },
|
|
50
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
+
})
|
|
52
|
+
let stdout = ''
|
|
53
|
+
let stderr = ''
|
|
54
|
+
let timedOut = false
|
|
55
|
+
child.stdout.setEncoding('utf8').on('data', value => { stdout += value })
|
|
56
|
+
child.stderr.setEncoding('utf8').on('data', value => { stderr += value })
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
timedOut = true
|
|
59
|
+
child.kill('SIGKILL')
|
|
60
|
+
}, options.timeoutMs ?? 600_000)
|
|
61
|
+
child.once('error', error => {
|
|
62
|
+
clearTimeout(timer)
|
|
63
|
+
resolveResult({ code: -1, stdout, stderr: `${stderr}${error.message}`, timedOut })
|
|
64
|
+
})
|
|
65
|
+
child.once('close', code => {
|
|
66
|
+
clearTimeout(timer)
|
|
67
|
+
resolveResult({ code: code ?? -1, stdout, stderr, timedOut })
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
const evidence = {
|
|
71
|
+
name,
|
|
72
|
+
status: result.code === 0 && !result.timedOut ? 'pass' : result.timedOut ? 'timeout' : 'fail',
|
|
73
|
+
command: [command, ...args],
|
|
74
|
+
durationMs: Date.now() - startedAt,
|
|
75
|
+
code: result.code,
|
|
76
|
+
stdout: tail(result.stdout),
|
|
77
|
+
stderr: tail(result.stderr),
|
|
78
|
+
}
|
|
79
|
+
RESULTS.push(evidence)
|
|
80
|
+
if (evidence.status !== 'pass') {
|
|
81
|
+
throw new Error(`${name} ${evidence.status} (exit ${result.code})\n${evidence.stderr || evidence.stdout}`)
|
|
82
|
+
}
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function helper(request) {
|
|
87
|
+
return await new Promise((resolveResult, reject) => {
|
|
88
|
+
const child = spawn(HELPER, [], { detached: true, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
89
|
+
let stdout = ''
|
|
90
|
+
let stderr = ''
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
child.kill('SIGKILL')
|
|
93
|
+
reject(new Error('native helper timed out'))
|
|
94
|
+
}, 15_000)
|
|
95
|
+
child.stdout.setEncoding('utf8').on('data', value => { stdout += value })
|
|
96
|
+
child.stderr.setEncoding('utf8').on('data', value => { stderr += value })
|
|
97
|
+
child.once('error', error => { clearTimeout(timer); reject(error) })
|
|
98
|
+
child.once('close', () => {
|
|
99
|
+
clearTimeout(timer)
|
|
100
|
+
try {
|
|
101
|
+
const envelope = JSON.parse(stdout)
|
|
102
|
+
if (!envelope.ok || envelope.value === undefined) {
|
|
103
|
+
reject(new Error(`${envelope.error?.code}: ${envelope.error?.message}`))
|
|
104
|
+
} else resolveResult(envelope.value)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
reject(new Error(`invalid helper response: ${stdout || stderr}`, { cause: error }))
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
child.stdin.end(`${JSON.stringify({ protocolVersion: 1, ...request })}\n`)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function fixtureApps() {
|
|
114
|
+
const apps = await helper({ command: 'list-apps' })
|
|
115
|
+
return apps.filter(app => app.bundleId === BUNDLE_ID)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function terminateFixtures() {
|
|
119
|
+
for (const app of await fixtureApps()) {
|
|
120
|
+
try { process.kill(app.pid, 'SIGTERM') } catch {}
|
|
121
|
+
}
|
|
122
|
+
const deadline = Date.now() + 5000
|
|
123
|
+
while ((await fixtureApps()).length > 0 && Date.now() < deadline) await delay(50)
|
|
124
|
+
if ((await fixtureApps()).length > 0) throw new Error('fixture processes did not terminate')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function launchFixture(transcript) {
|
|
128
|
+
await runCommand(
|
|
129
|
+
'launch deterministic fixture without foreground activation',
|
|
130
|
+
'open',
|
|
131
|
+
['-g', '-n', FIXTURE_APP, '--args', '--background', '--transcript', transcript],
|
|
132
|
+
{ timeoutMs: 10_000 },
|
|
133
|
+
)
|
|
134
|
+
const deadline = Date.now() + 10_000
|
|
135
|
+
while (Date.now() < deadline) {
|
|
136
|
+
const [app] = await fixtureApps()
|
|
137
|
+
if (app !== undefined) return app
|
|
138
|
+
await delay(50)
|
|
139
|
+
}
|
|
140
|
+
throw new Error('fixture did not launch')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function screenshotFiles(directory) {
|
|
144
|
+
try {
|
|
145
|
+
const files = await readdir(directory, { recursive: true })
|
|
146
|
+
return files.filter(file => file.endsWith('.png'))
|
|
147
|
+
} catch {
|
|
148
|
+
return []
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function findTarball(directory) {
|
|
153
|
+
const files = (await readdir(directory)).filter(file => file.endsWith('.tgz'))
|
|
154
|
+
if (files.length !== 1) throw new Error(`expected one tarball in ${directory}, found ${files.length}`)
|
|
155
|
+
return join(directory, files[0])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function realModelWorkflow() {
|
|
159
|
+
const packing = await mkdtemp(join(tmpdir(), 'dsh-computer-model-pack-'))
|
|
160
|
+
const home = await mkdtemp(join(tmpdir(), 'dsh-computer-model-home-'))
|
|
161
|
+
const workspace = await mkdtemp(join(tmpdir(), 'dsh-computer-model-workspace-'))
|
|
162
|
+
const privateState = await mkdtemp(join(tmpdir(), 'dsh-computer-model-state-'))
|
|
163
|
+
TEMPORARY.push(packing, home, workspace, privateState)
|
|
164
|
+
|
|
165
|
+
await runCommand('pack dsh-computer-use', 'pnpm', ['pack', '--pack-destination', packing], { timeoutMs: 300_000 })
|
|
166
|
+
await access(HELPER, constants.X_OK)
|
|
167
|
+
await access(join(FIXTURE_APP, 'Contents', 'MacOS', 'DSHComputerUseFixture'), constants.X_OK)
|
|
168
|
+
const health = await helper({ command: 'health' })
|
|
169
|
+
if (health.accessibility !== 'granted' || health.screenRecording !== 'granted') {
|
|
170
|
+
throw new Error(`real-model lane requires macOS TCC grants: ${JSON.stringify(health)}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const tarball = await findTarball(packing)
|
|
174
|
+
await runCommand(
|
|
175
|
+
'install dsh-computer-use tarball',
|
|
176
|
+
'dsh',
|
|
177
|
+
['plugin', '--profile', 'headless', 'add', tarball],
|
|
178
|
+
{ env: { DSH_HOME: home }, timeoutMs: 180_000 },
|
|
179
|
+
)
|
|
180
|
+
const dump = await runCommand(
|
|
181
|
+
'dump real-model Profile',
|
|
182
|
+
'dsh',
|
|
183
|
+
['--profile', 'headless', '--dump-config'],
|
|
184
|
+
{ env: { DSH_HOME: home }, timeoutMs: 60_000 },
|
|
185
|
+
)
|
|
186
|
+
if (!dump.stdout.includes("name: '@anionex/dsh-computer-use'")) {
|
|
187
|
+
throw new Error('real-model Profile does not mount dsh-computer-use')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await terminateFixtures()
|
|
191
|
+
const transcript = join(privateState, 'fixture-transcript.json')
|
|
192
|
+
const app = await launchFixture(transcript)
|
|
193
|
+
const patch = join(home, 'computer-use-model.patch.yml')
|
|
194
|
+
await writeFile(patch, [
|
|
195
|
+
'- id: computer-use',
|
|
196
|
+
' config:',
|
|
197
|
+
' settleMs: 50',
|
|
198
|
+
' maxSettleMs: 3000',
|
|
199
|
+
' grants:',
|
|
200
|
+
` - bundleId: ${BUNDLE_ID}`,
|
|
201
|
+
' read: true',
|
|
202
|
+
' control: true',
|
|
203
|
+
'- id: session-title-llm',
|
|
204
|
+
' disabled: true',
|
|
205
|
+
'',
|
|
206
|
+
].join('\n'))
|
|
207
|
+
|
|
208
|
+
const prompt = `/computer-use\n\nUse the dsh-computer-use capability to operate the running deterministic macOS fixture whose bundle id is ${BUNDLE_ID}. Load the Skill, list running applications, select the exact fixture process, observe it with a required screenshot and full Accessibility state, locate the element labelled "Targeted pointer probe", and click that current element using its observation id and element index with allowCoordinateFallback=true. Confirm from the fresh post-action observation that the status reads "Status: pointer click". Use only the focused computer-use Tools for UI observation and input; do not use shell, AppleScript, JXA, direct file edits, or coordinate guessing. Finish immediately after confirming the pointer-click state.`
|
|
209
|
+
const result = await runCommand(
|
|
210
|
+
'DeepSeek V4 computer-use fixture workflow',
|
|
211
|
+
'dsh',
|
|
212
|
+
['run', '--profile', 'headless', '--patch', patch, prompt],
|
|
213
|
+
{
|
|
214
|
+
cwd: workspace,
|
|
215
|
+
env: {
|
|
216
|
+
DSH_HOME: home,
|
|
217
|
+
DSH_TELEMETRY_DISABLED: '1',
|
|
218
|
+
DSH_PERMISSION_MODE: 'workspace-write',
|
|
219
|
+
DEEPSEEK_API_KEY: requiredEnvironment('DEEPSEEK_API_KEY'),
|
|
220
|
+
DEEPSEEK_BASE_URL: process.env.DEEPSEEK_BASE_URL?.trim() || 'https://api.deepseek.com/v1',
|
|
221
|
+
},
|
|
222
|
+
timeoutMs: 600_000,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const state = JSON.parse(await readFile(transcript, 'utf8'))
|
|
227
|
+
if (state.activationCount !== 0 || state.pointerClickCount !== 1 || state.status !== 'Status: pointer click') {
|
|
228
|
+
throw new Error(`fixture state did not prove the requested action: ${JSON.stringify(state)}`)
|
|
229
|
+
}
|
|
230
|
+
const screenshots = await screenshotFiles(join(workspace, '.dsh-computer-use', 'artifacts'))
|
|
231
|
+
if (screenshots.length === 0) throw new Error('computer_observe produced no screenshot Artifact')
|
|
232
|
+
const evidence = await findComputerClickEvidence(join(home, 'sessions'))
|
|
233
|
+
if (!evidence.allowCoordinateFallback
|
|
234
|
+
|| evidence.activation !== 'not-requested'
|
|
235
|
+
|| evidence.pointerInput !== true
|
|
236
|
+
|| evidence.pointerRouting !== 'target-process') {
|
|
237
|
+
throw new Error(`real-model transcript did not expose the target-process action evidence: ${JSON.stringify(evidence)}`)
|
|
238
|
+
}
|
|
239
|
+
RESULTS.push({
|
|
240
|
+
name: 'DeepSeek V4 computer-use assertions',
|
|
241
|
+
status: 'pass',
|
|
242
|
+
bundleId: app.bundleId,
|
|
243
|
+
pid: app.pid,
|
|
244
|
+
fixtureState: state,
|
|
245
|
+
toolEvidence: evidence,
|
|
246
|
+
screenshotArtifacts: screenshots,
|
|
247
|
+
finalResponse: tail(result.stdout.trim(), 2000),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function writeOutput(options, value) {
|
|
252
|
+
const text = `${JSON.stringify(value, null, 2)}\n`
|
|
253
|
+
if (options.output !== undefined) {
|
|
254
|
+
await mkdir(dirname(options.output), { recursive: true })
|
|
255
|
+
await writeFile(options.output, text)
|
|
256
|
+
}
|
|
257
|
+
process.stdout.write(text)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let options
|
|
261
|
+
let failure
|
|
262
|
+
let cleanupFailure
|
|
263
|
+
try {
|
|
264
|
+
options = parseArgs(process.argv.slice(2))
|
|
265
|
+
requiredEnvironment('DEEPSEEK_API_KEY')
|
|
266
|
+
await realModelWorkflow()
|
|
267
|
+
} catch (error) {
|
|
268
|
+
failure = error instanceof Error ? error.message : String(error)
|
|
269
|
+
} finally {
|
|
270
|
+
try {
|
|
271
|
+
if (await access(HELPER, constants.X_OK).then(() => true, () => false)) await terminateFixtures()
|
|
272
|
+
if (options?.keepTemp !== true) {
|
|
273
|
+
for (const path of TEMPORARY.reverse()) await rm(path, { recursive: true, force: true })
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
cleanupFailure = error instanceof Error ? error.message : String(error)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const report = {
|
|
281
|
+
schemaVersion: 1,
|
|
282
|
+
project: '@anionex/dsh-computer-use',
|
|
283
|
+
lane: 'real-model',
|
|
284
|
+
ok: failure === undefined && cleanupFailure === undefined,
|
|
285
|
+
failure: failure ?? null,
|
|
286
|
+
cleanup: cleanupFailure === undefined ? options?.keepTemp === true ? 'kept' : 'complete' : 'failed',
|
|
287
|
+
cleanupFailure: cleanupFailure ?? null,
|
|
288
|
+
results: RESULTS,
|
|
289
|
+
}
|
|
290
|
+
await writeOutput(options ?? {}, report)
|
|
291
|
+
if (!report.ok) process.exitCode = 1
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
4
|
+
import { zstdDecompress } from 'node:zlib'
|
|
5
|
+
|
|
6
|
+
const ZSTD_MAGIC = 0xFD2FB528
|
|
7
|
+
const decompressZstd = promisify(zstdDecompress)
|
|
8
|
+
|
|
9
|
+
function zstdFrames(buffer) {
|
|
10
|
+
const frames = []
|
|
11
|
+
let offset = 0
|
|
12
|
+
while (offset < buffer.length) {
|
|
13
|
+
const start = offset
|
|
14
|
+
if (buffer.length - offset < 5 || buffer.readUInt32LE(offset) !== ZSTD_MAGIC) {
|
|
15
|
+
throw new Error(`invalid Zstandard session frame at byte ${offset}`)
|
|
16
|
+
}
|
|
17
|
+
offset += 4
|
|
18
|
+
const descriptor = buffer.readUInt8(offset++)
|
|
19
|
+
if ((descriptor & 0x18) !== 0) throw new Error(`invalid Zstandard frame descriptor at byte ${offset - 1}`)
|
|
20
|
+
const contentSizeFlag = descriptor >>> 6
|
|
21
|
+
const singleSegment = (descriptor & 0x20) !== 0
|
|
22
|
+
const checksum = (descriptor & 0x04) !== 0
|
|
23
|
+
const dictionaryFlag = descriptor & 0x03
|
|
24
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag
|
|
25
|
+
const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag
|
|
26
|
+
const headerBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes
|
|
27
|
+
if (buffer.length - offset < headerBytes) throw new Error(`truncated Zstandard session frame at byte ${start}`)
|
|
28
|
+
offset += headerBytes
|
|
29
|
+
for (;;) {
|
|
30
|
+
if (buffer.length - offset < 3) throw new Error(`truncated Zstandard session frame at byte ${start}`)
|
|
31
|
+
const blockHeader = buffer.readUIntLE(offset, 3)
|
|
32
|
+
offset += 3
|
|
33
|
+
const lastBlock = (blockHeader & 1) !== 0
|
|
34
|
+
const blockType = (blockHeader >>> 1) & 0x03
|
|
35
|
+
const blockSize = blockHeader >>> 3
|
|
36
|
+
if (blockType === 0x03) throw new Error(`invalid Zstandard block at byte ${offset - 3}`)
|
|
37
|
+
const payloadBytes = blockType === 0x01 ? 1 : blockSize
|
|
38
|
+
if (buffer.length - offset < payloadBytes) throw new Error(`truncated Zstandard session frame at byte ${start}`)
|
|
39
|
+
offset += payloadBytes
|
|
40
|
+
if (lastBlock) break
|
|
41
|
+
}
|
|
42
|
+
if (checksum) {
|
|
43
|
+
if (buffer.length - offset < 4) throw new Error(`truncated Zstandard checksum at byte ${start}`)
|
|
44
|
+
offset += 4
|
|
45
|
+
}
|
|
46
|
+
frames.push(buffer.subarray(start, offset))
|
|
47
|
+
}
|
|
48
|
+
return frames
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function sessionLogs(directory) {
|
|
52
|
+
const result = []
|
|
53
|
+
for (const entry of await readdir(directory, { withFileTypes: true }).catch(() => [])) {
|
|
54
|
+
const path = join(directory, entry.name)
|
|
55
|
+
if (entry.isDirectory()) result.push(...await sessionLogs(path))
|
|
56
|
+
else if (entry.isFile() && (entry.name === 'session.jsonl' || entry.name === 'session.jsonl.zstd')) result.push(path)
|
|
57
|
+
}
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read every JSON record from one plain or concatenated-frame DSH Session log. */
|
|
62
|
+
export async function readSessionRecords(path) {
|
|
63
|
+
const encoded = await readFile(path)
|
|
64
|
+
const decoded = path.endsWith('.zstd')
|
|
65
|
+
? Buffer.concat(await Promise.all(zstdFrames(encoded).map(frame => decompressZstd(frame))))
|
|
66
|
+
: encoded
|
|
67
|
+
return decoded.toString('utf8').split('\n').filter(Boolean).map(line => JSON.parse(line))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resultValue(event) {
|
|
71
|
+
const outer = event?.data?.message?.content
|
|
72
|
+
if (!Array.isArray(outer)) return undefined
|
|
73
|
+
for (const block of outer) {
|
|
74
|
+
if (block?.type !== 'tool-result' || !Array.isArray(block.content)) continue
|
|
75
|
+
for (const content of block.content) {
|
|
76
|
+
if (content?.type !== 'text' || typeof content.text !== 'string') continue
|
|
77
|
+
try { return JSON.parse(content.text) }
|
|
78
|
+
catch { return undefined }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Locate the durable model-visible result for the target-process pointer probe. */
|
|
85
|
+
export async function findComputerClickEvidence(sessionRoot) {
|
|
86
|
+
const candidates = []
|
|
87
|
+
for (const path of await sessionLogs(sessionRoot)) {
|
|
88
|
+
const records = await readSessionRecords(path)
|
|
89
|
+
const header = records.find(record => record?.type === 'session')
|
|
90
|
+
const calls = new Map(records
|
|
91
|
+
.filter(record => record?.type === 'tool/call' && record?.data?.name === 'computer_click')
|
|
92
|
+
.map(record => [record.data.callId, record]))
|
|
93
|
+
for (const record of records) {
|
|
94
|
+
if (record?.type !== 'tool/result') continue
|
|
95
|
+
const callId = record?.data?.message?.source?.callId
|
|
96
|
+
const call = calls.get(callId)
|
|
97
|
+
if (call === undefined) continue
|
|
98
|
+
let args
|
|
99
|
+
try { args = JSON.parse(call.data.arguments) }
|
|
100
|
+
catch { continue }
|
|
101
|
+
const result = resultValue(record)
|
|
102
|
+
if (result?.action !== 'click') continue
|
|
103
|
+
candidates.push({
|
|
104
|
+
time: record.time ?? 0,
|
|
105
|
+
sessionId: header?.id,
|
|
106
|
+
callId,
|
|
107
|
+
allowCoordinateFallback: args.allowCoordinateFallback === true,
|
|
108
|
+
activation: result.activation,
|
|
109
|
+
pointerInput: result.pointerInput,
|
|
110
|
+
pointerRouting: result.pointerRouting,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const evidence = candidates.sort((left, right) => right.time - left.time)[0]
|
|
115
|
+
if (evidence === undefined) throw new Error('durable Session transcript contains no completed computer_click result')
|
|
116
|
+
return evidence
|
|
117
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { spawn } from 'node:child_process'
|
|
5
|
+
import { constants } from 'node:fs'
|
|
6
|
+
import { access, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
import { dirname, join, resolve } from 'node:path'
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const ROOT = dirname(fileURLToPath(new URL('../package.json', import.meta.url)))
|
|
12
|
+
const results = []
|
|
13
|
+
const temporaryPaths = []
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const options = {
|
|
17
|
+
lane: 'all',
|
|
18
|
+
allowHubWarning: false,
|
|
19
|
+
keepTemp: false,
|
|
20
|
+
output: undefined,
|
|
21
|
+
}
|
|
22
|
+
const values = [...argv]
|
|
23
|
+
while (values.length > 0) {
|
|
24
|
+
const option = values.shift()
|
|
25
|
+
if (option === '--allow-hub-warning') options.allowHubWarning = true
|
|
26
|
+
else if (option === '--keep-temp') options.keepTemp = true
|
|
27
|
+
else if (option === '--lane' || option === '--output') {
|
|
28
|
+
const value = values.shift()
|
|
29
|
+
if (value === undefined) throw new Error(`${option} needs a value`)
|
|
30
|
+
if (option === '--lane') options.lane = value
|
|
31
|
+
else options.output = resolve(value)
|
|
32
|
+
} else throw new Error(`unknown option: ${option}`)
|
|
33
|
+
}
|
|
34
|
+
if (!['local', 'profile', 'all'].includes(options.lane)) {
|
|
35
|
+
throw new Error(`--lane must be local, profile, or all; got ${options.lane}`)
|
|
36
|
+
}
|
|
37
|
+
return options
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tail(value, length = 6000) {
|
|
41
|
+
return value.length <= length ? value : value.slice(-length)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runCommand(name, command, args, options = {}) {
|
|
45
|
+
const startedAt = Date.now()
|
|
46
|
+
let final
|
|
47
|
+
for (let attempt = 1; attempt <= (options.retries ?? 0) + 1; attempt += 1) {
|
|
48
|
+
final = await new Promise((resolveResult) => {
|
|
49
|
+
const child = spawn(command, args, {
|
|
50
|
+
cwd: options.cwd ?? ROOT,
|
|
51
|
+
env: { ...process.env, ...options.env },
|
|
52
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
+
})
|
|
54
|
+
let stdout = ''
|
|
55
|
+
let stderr = ''
|
|
56
|
+
let timedOut = false
|
|
57
|
+
child.stdout.setEncoding('utf8').on('data', value => { stdout += value })
|
|
58
|
+
child.stderr.setEncoding('utf8').on('data', value => { stderr += value })
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
timedOut = true
|
|
61
|
+
child.kill('SIGKILL')
|
|
62
|
+
}, options.timeoutMs ?? 120_000)
|
|
63
|
+
child.once('error', (error) => {
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
resolveResult({ code: -1, stdout, stderr: `${stderr}${error.message}`, timedOut, attempt })
|
|
66
|
+
})
|
|
67
|
+
child.once('close', (code) => {
|
|
68
|
+
clearTimeout(timer)
|
|
69
|
+
resolveResult({ code: code ?? -1, stdout, stderr, timedOut, attempt })
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
if (final.code === 0 && !final.timedOut) break
|
|
73
|
+
}
|
|
74
|
+
const result = {
|
|
75
|
+
name,
|
|
76
|
+
status: final.code === 0 && !final.timedOut ? 'pass' : final.timedOut ? 'timeout' : 'fail',
|
|
77
|
+
command: [command, ...args],
|
|
78
|
+
attempts: final.attempt,
|
|
79
|
+
durationMs: Date.now() - startedAt,
|
|
80
|
+
code: final.code,
|
|
81
|
+
stdout: tail(final.stdout),
|
|
82
|
+
stderr: tail(final.stderr),
|
|
83
|
+
}
|
|
84
|
+
results.push(result)
|
|
85
|
+
if (result.status !== 'pass') {
|
|
86
|
+
throw new Error(`${name} ${result.status} (exit ${result.code})\n${result.stderr || result.stdout}`)
|
|
87
|
+
}
|
|
88
|
+
return final
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function executable(name) {
|
|
92
|
+
for (const directory of (process.env.PATH ?? '').split(':').filter(Boolean)) {
|
|
93
|
+
const candidate = join(directory, name)
|
|
94
|
+
try {
|
|
95
|
+
await access(candidate, constants.X_OK)
|
|
96
|
+
return candidate
|
|
97
|
+
} catch {
|
|
98
|
+
// Continue PATH lookup.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`required executable is unavailable: ${name}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function dshSourceRoot() {
|
|
105
|
+
const dsh = await realpath(await executable('dsh'))
|
|
106
|
+
let candidate = dirname(dsh)
|
|
107
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
108
|
+
try {
|
|
109
|
+
await access(join(candidate, 'apps', 'cli', 'config', 'agent-presets', 'standard'))
|
|
110
|
+
return await realpath(candidate)
|
|
111
|
+
} catch {
|
|
112
|
+
const parent = dirname(candidate)
|
|
113
|
+
if (parent === candidate) break
|
|
114
|
+
candidate = parent
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`cannot infer DSH source root from ${dsh}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function pluginCheck(options) {
|
|
121
|
+
const sourceRoot = await dshSourceRoot()
|
|
122
|
+
const checkout = await mkdtemp(join(tmpdir(), 'dsh-computer-plugin-check-'))
|
|
123
|
+
temporaryPaths.push(checkout)
|
|
124
|
+
await runCommand(
|
|
125
|
+
'clone dsh-plugin-check',
|
|
126
|
+
'gh',
|
|
127
|
+
['repo', 'clone', 'dsh-external/dsh-plugin-check', checkout, '--', '--depth=1'],
|
|
128
|
+
{ timeoutMs: 60_000, retries: 1 },
|
|
129
|
+
)
|
|
130
|
+
const code = [
|
|
131
|
+
`const { checkRepo } = await import(${JSON.stringify(pathToFileURL(join(checkout, 'src', 'index.ts')).href)});`,
|
|
132
|
+
`const report = await checkRepo(${JSON.stringify(ROOT)}, false);`,
|
|
133
|
+
'console.log(JSON.stringify(report));',
|
|
134
|
+
].join(' ')
|
|
135
|
+
const checked = await runCommand(
|
|
136
|
+
'dsh-plugin-check',
|
|
137
|
+
process.execPath,
|
|
138
|
+
['--import', join(sourceRoot, 'node_modules', 'tsx', 'dist', 'esm', 'index.mjs'), '--input-type=module', '-e', code],
|
|
139
|
+
{ env: { TSX_TSCONFIG_PATH: join(sourceRoot, 'tsconfig.json') }, timeoutMs: 30_000 },
|
|
140
|
+
)
|
|
141
|
+
const report = JSON.parse(checked.stdout.trim())
|
|
142
|
+
const warnings = report.warnings ?? []
|
|
143
|
+
const allowed = options.allowHubWarning && warnings.every(warning => warning.code === 'not-in-hub')
|
|
144
|
+
if (report.errors?.length > 0 || (warnings.length > 0 && !allowed)) {
|
|
145
|
+
throw new Error(`dsh-plugin-check did not pass cleanly: ${JSON.stringify(report, null, 2)}`)
|
|
146
|
+
}
|
|
147
|
+
results.push({
|
|
148
|
+
name: 'dsh-plugin-check assertion',
|
|
149
|
+
status: 'pass',
|
|
150
|
+
verdict: report.verdict,
|
|
151
|
+
allowedWarnings: allowed ? warnings : [],
|
|
152
|
+
checks: report.checks,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function javascriptFiles(directory) {
|
|
157
|
+
const paths = []
|
|
158
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
159
|
+
const path = join(directory, entry.name)
|
|
160
|
+
if (entry.isDirectory()) paths.push(...await javascriptFiles(path))
|
|
161
|
+
else if (entry.isFile() && entry.name.endsWith('.js')) paths.push(path)
|
|
162
|
+
}
|
|
163
|
+
return paths
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function portableAssertions() {
|
|
167
|
+
const startedAt = Date.now()
|
|
168
|
+
const pkg = JSON.parse(await readFile(join(ROOT, 'package.json'), 'utf8'))
|
|
169
|
+
if (pkg.dsh?.bundle?.patch !== './cordis.patch.yml') throw new Error('package is missing dsh.bundle.patch')
|
|
170
|
+
if (pkg.dsh?.client?.platform !== 'web' || pkg.dshClient !== undefined) throw new Error('package must use dsh.client and omit dshClient')
|
|
171
|
+
for (const group of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
172
|
+
for (const [name, spec] of Object.entries(pkg[group] ?? {})) {
|
|
173
|
+
if (typeof spec !== 'string' || /^(?:file:|link:|workspace:|\/|[A-Za-z]:[\\/])/u.test(spec) || spec.includes('/Users/')) {
|
|
174
|
+
throw new Error(`${group}.${name} is machine-local: ${String(spec)}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const path of await javascriptFiles(join(ROOT, 'lib'))) {
|
|
179
|
+
const source = await readFile(path, 'utf8')
|
|
180
|
+
if (/from ['"]\.\.?\/[^'"]+\.ts['"]/u.test(source)) throw new Error(`built JavaScript imports TypeScript: ${path}`)
|
|
181
|
+
}
|
|
182
|
+
const client = await readFile(join(ROOT, 'lib', 'client.js'), 'utf8')
|
|
183
|
+
if (!client.includes('window.__ModuleLoader__.load')) throw new Error('lib/client.js is not loader-compatible')
|
|
184
|
+
const manifest = JSON.parse(await readFile(join(ROOT, 'native', 'macos', 'manifest.json'), 'utf8'))
|
|
185
|
+
const helper = await readFile(join(ROOT, 'native', 'macos', manifest.binary.path))
|
|
186
|
+
const digest = createHash('sha256').update(helper).digest('hex')
|
|
187
|
+
if (digest !== manifest.binary.sha256) throw new Error('native helper hash does not match manifest')
|
|
188
|
+
if (JSON.stringify(manifest.binary.architectures) !== JSON.stringify(['arm64', 'x86_64'])) {
|
|
189
|
+
throw new Error(`native helper architectures are not universal: ${JSON.stringify(manifest.binary.architectures)}`)
|
|
190
|
+
}
|
|
191
|
+
results.push({
|
|
192
|
+
name: 'portable artifact assertions',
|
|
193
|
+
status: 'pass',
|
|
194
|
+
durationMs: Date.now() - startedAt,
|
|
195
|
+
helperSha256: digest,
|
|
196
|
+
architectures: manifest.binary.architectures,
|
|
197
|
+
minimumMacOS: manifest.binary.minimumMacOS,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function localLane(options) {
|
|
202
|
+
await runCommand('build', 'pnpm', ['run', 'build'], { timeoutMs: 300_000 })
|
|
203
|
+
await runCommand(
|
|
204
|
+
'unit and native fixture tests',
|
|
205
|
+
'pnpm',
|
|
206
|
+
['exec', 'vitest', 'run', 'tests', '--exclude', 'tests/profile-install.e2e.spec.ts'],
|
|
207
|
+
{ env: { DSH_COMPUTER_USE_REQUIRE_TCC: '1' }, timeoutMs: 600_000 },
|
|
208
|
+
)
|
|
209
|
+
await portableAssertions()
|
|
210
|
+
await runCommand('pack dry run', 'pnpm', ['pack', '--dry-run'], { timeoutMs: 300_000 })
|
|
211
|
+
await pluginCheck(options)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function profileLane() {
|
|
215
|
+
await runCommand(
|
|
216
|
+
'clean Web/Headless Profile fixture E2E',
|
|
217
|
+
'pnpm',
|
|
218
|
+
['exec', 'vitest', 'run', 'tests/profile-install.e2e.spec.ts'],
|
|
219
|
+
{ env: { DSH_COMPUTER_USE_REQUIRE_TCC: '1' }, timeoutMs: 420_000 },
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function writeOutput(options, value) {
|
|
224
|
+
const text = `${JSON.stringify(value, null, 2)}\n`
|
|
225
|
+
if (options.output !== undefined) {
|
|
226
|
+
await mkdir(dirname(options.output), { recursive: true })
|
|
227
|
+
await writeFile(options.output, text)
|
|
228
|
+
}
|
|
229
|
+
process.stdout.write(text)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let options
|
|
233
|
+
let failure
|
|
234
|
+
try {
|
|
235
|
+
options = parseArgs(process.argv.slice(2))
|
|
236
|
+
if (options.lane === 'local' || options.lane === 'all') await localLane(options)
|
|
237
|
+
if (options.lane === 'profile' || options.lane === 'all') await profileLane()
|
|
238
|
+
} catch (error) {
|
|
239
|
+
failure = error instanceof Error ? error.message : String(error)
|
|
240
|
+
} finally {
|
|
241
|
+
if (options?.keepTemp !== true) {
|
|
242
|
+
for (const path of temporaryPaths.reverse()) await rm(path, { recursive: true, force: true })
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const report = {
|
|
247
|
+
schemaVersion: 1,
|
|
248
|
+
project: '@anionex/dsh-computer-use',
|
|
249
|
+
lane: options?.lane ?? null,
|
|
250
|
+
ok: failure === undefined,
|
|
251
|
+
failure: failure ?? null,
|
|
252
|
+
cleanup: options?.keepTemp === true ? 'kept' : 'complete',
|
|
253
|
+
results,
|
|
254
|
+
}
|
|
255
|
+
await writeOutput(options ?? {}, report)
|
|
256
|
+
if (failure !== undefined) process.exitCode = 1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shared DSH approval-policy read for Computer Use permission paths. */
|
|
2
|
+
|
|
3
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
4
|
+
import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The session's effective DSH approval policy: a logged override, else the
|
|
9
|
+
* approval plugin's configured default.
|
|
10
|
+
* @param ctx - context with the approval service injected.
|
|
11
|
+
* @param agent - the agent whose session policy applies.
|
|
12
|
+
* @returns `'never'` when every approval ask would auto-reject without a prompt.
|
|
13
|
+
*/
|
|
14
|
+
export function approvalPolicy(ctx: Context, agent: Agent): ApprovalPolicy {
|
|
15
|
+
return ctx.approval.overrideOf(agent.session) ?? ctx.approval.config.policy ?? 'ask'
|
|
16
|
+
}
|