dsh-codex-style-computer-use 1.2.0-rc.3
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.md +144 -0
- package/README.zh.md +132 -0
- package/cordis.patch.yml +17 -0
- package/lib/artifacts.js +35 -0
- package/lib/attention-native.js +93 -0
- package/lib/attention-worker.cs +194 -0
- package/lib/attention.js +140 -0
- package/lib/audit.js +30 -0
- package/lib/batch.js +52 -0
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +7 -0
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +7 -0
- package/lib/core/compile-worker.ps1 +58 -0
- package/lib/core/index.js +844 -0
- package/lib/core/native-runtime.js +97 -0
- package/lib/core/worker.cs +6375 -0
- package/lib/cycle.js +314 -0
- package/lib/exposure.js +91 -0
- package/lib/feedback.js +35 -0
- package/lib/image-output.js +92 -0
- package/lib/index.js +1715 -0
- package/lib/observations.js +78 -0
- package/lib/policy.js +109 -0
- package/lib/recovery.js +8 -0
- package/lib/skill-gate.js +68 -0
- package/lib/tools.js +717 -0
- package/mcp/package.json +12 -0
- package/mcp/src/index.js +335 -0
- package/package.json +88 -0
- package/scripts/build-native.mjs +7 -0
- package/scripts/smoke-native.mjs +38 -0
- package/scripts/verify-package.mjs +28 -0
- package/skills/computer-use/SKILL.md +74 -0
- package/skills/computer-use/references/lifecycle.md +180 -0
- package/skills/computer-use/references/maintenance.md +56 -0
- package/skills/computer-use/references/precision.md +297 -0
- package/skills/computer-use/references/progressive-control.md +30 -0
- package/skills/computer-use/references/shared-input.md +27 -0
package/lib/attention.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { AttentionNative } from './attention-native.js'
|
|
2
|
+
|
|
3
|
+
// Pure event wiring + a separately testable native boundary. Never handles answers or approvals.
|
|
4
|
+
export function installHumanAttention (ctx, log, options = {}) {
|
|
5
|
+
let raise = null
|
|
6
|
+
const native = options.native || new AttentionNative()
|
|
7
|
+
// Official 0.2.x launches dsh-desktop-host as Electron's Node child and does NOT
|
|
8
|
+
// register the legacy desktopRuntime Cordis service. Waiting for it disables ALL listeners.
|
|
9
|
+
const nativeHost = options.nativeHost ?? (process.platform === 'win32' &&
|
|
10
|
+
process.argv.some(arg => String(arg).replaceAll('\\', '/').includes('/dsh-desktop-host/')))
|
|
11
|
+
native.log({ stage: 'installer-enter', nativeHost, pid: process.pid, argvEntry: process.argv[1],
|
|
12
|
+
electronNodeMode: process.env.ELECTRON_RUN_AS_NODE === '1' })
|
|
13
|
+
const install = desktopCtx => {
|
|
14
|
+
if (raise) return
|
|
15
|
+
let runtime
|
|
16
|
+
// Cordis' property proxy may throw for an unregistered service; optional chaining
|
|
17
|
+
// on that property is insufficient. get() is the optional service lookup API.
|
|
18
|
+
try { runtime = typeof desktopCtx.get === 'function' ? desktopCtx.get('desktopRuntime') : desktopCtx.desktopRuntime } catch {}
|
|
19
|
+
if (typeof runtime?.show !== 'function' && !nativeHost) { log.warn('human attention: desktopRuntime.show unavailable'); return }
|
|
20
|
+
const now = options.now || Date.now
|
|
21
|
+
const pending = new Map(), inFlight = new Map(), recent = new Map(), questionKeys = new WeakMap()
|
|
22
|
+
let anonymous = 0, disposed = false
|
|
23
|
+
const record = data => native.log(data)
|
|
24
|
+
const trim = map => { while (map.size > 256) map.delete(map.keys().next().value) }
|
|
25
|
+
const bounded = (promise, ms) => new Promise((resolve, reject) => {
|
|
26
|
+
const timer = setTimeout(() => reject(new Error(`show timed out (${ms}ms)`)), ms)
|
|
27
|
+
Promise.resolve(promise).then(v => { clearTimeout(timer); resolve(v) }, e => { clearTimeout(timer); reject(e) })
|
|
28
|
+
})
|
|
29
|
+
native.start().catch(error => record({ stage: 'prewarm-failed', error: error.stack || String(error) }))
|
|
30
|
+
raise = (key, what, stillPending = () => true) => {
|
|
31
|
+
if (disposed) return Promise.resolve(false)
|
|
32
|
+
key ||= `anon:${++anonymous}`
|
|
33
|
+
if (inFlight.has(key)) return inFlight.get(key)
|
|
34
|
+
if (recent.has(key) && now() - recent.get(key) < 1500) return Promise.resolve(true)
|
|
35
|
+
const started = performance.now()
|
|
36
|
+
const work = (async () => {
|
|
37
|
+
record({ stage: 'raise-start', key, what })
|
|
38
|
+
let before, probeError
|
|
39
|
+
try { before = await native.request('probe') } catch (e) { probeError = e.message }
|
|
40
|
+
if (disposed || !stillPending()) {
|
|
41
|
+
record({ stage: 'raise-cancelled', key, what })
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
if (before?.focused && before?.sampledVisible) {
|
|
45
|
+
record({ stage: 'raise-result', key, what, status: 'already-visible', visible: true, focused: true, elapsedMs: Math.round(performance.now() - started) })
|
|
46
|
+
log.info(`Desktop already in front for ${what}`)
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
// The host request and the independent visibility fallback run together. A rejected/hung
|
|
50
|
+
// host RPC must not suppress the fallback or delay the card waterfall.
|
|
51
|
+
let show
|
|
52
|
+
try { show = typeof runtime?.show === 'function'
|
|
53
|
+
? bounded(runtime.show(), 150).then(() => 'requested', e => `error: ${e.message}`)
|
|
54
|
+
: Promise.resolve('unavailable-native-fallback') }
|
|
55
|
+
catch (e) { show = Promise.resolve(`error: ${e.message}`) }
|
|
56
|
+
let result
|
|
57
|
+
try { result = await native.request('raise') }
|
|
58
|
+
catch (error) {
|
|
59
|
+
record({ stage: 'raise-result', key, what, status: 'error', visible: false, probeError, show: await show, error: error.stack || String(error), elapsedMs: Math.round(performance.now() - started) })
|
|
60
|
+
log.warn(`could not reveal Desktop for ${what}: ${error.message}`)
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
const elapsedMs = Math.round(performance.now() - started)
|
|
64
|
+
record({ stage: 'raise-result', key, what, ...result, show: await show, probeError, elapsedMs, within1s: result.visible === true && elapsedMs <= 1000 })
|
|
65
|
+
if (result.visible === true) log.info(`Desktop ${result.focused ? 'foreground' : 'visible without focus'} CONFIRMED for ${what} (${elapsedMs}ms)`)
|
|
66
|
+
else log.warn(`could not reveal Desktop for ${what}: ${result.status}; flash=${result.flash || 'unknown'}`)
|
|
67
|
+
return result.visible === true
|
|
68
|
+
})().catch(error => { record({ stage: 'raise-error', key, error: error.stack || String(error) }); return false })
|
|
69
|
+
inFlight.set(key, work)
|
|
70
|
+
work.then(ok => { inFlight.delete(key); if (ok) { recent.set(key, now()); trim(recent) } })
|
|
71
|
+
return work
|
|
72
|
+
}
|
|
73
|
+
const schedule = (key, what) => {
|
|
74
|
+
const previous = pending.get(key)
|
|
75
|
+
if (previous && (!previous.exhausted || inFlight.has(key))) return
|
|
76
|
+
const item = { timer: null, attempts: 0 }
|
|
77
|
+
pending.set(key, item)
|
|
78
|
+
const attempt = async () => {
|
|
79
|
+
if (disposed || pending.get(key) !== item) return
|
|
80
|
+
item.attempts++
|
|
81
|
+
const ok = await raise(key, what, () => pending.get(key) === item)
|
|
82
|
+
if (!ok && item.attempts < 2 && pending.get(key) === item && !disposed) {
|
|
83
|
+
item.timer = setTimeout(attempt, options.retryMs ?? 700)
|
|
84
|
+
item.timer.unref?.()
|
|
85
|
+
record({ stage: 'retry-scheduled', key })
|
|
86
|
+
}
|
|
87
|
+
if (!ok && item.attempts >= 2) item.exhausted = true
|
|
88
|
+
}
|
|
89
|
+
void attempt()
|
|
90
|
+
}
|
|
91
|
+
const finish = key => {
|
|
92
|
+
const item = pending.get(key)
|
|
93
|
+
if (item) clearTimeout(item.timer)
|
|
94
|
+
pending.delete(key)
|
|
95
|
+
}
|
|
96
|
+
const question = (...args) => {
|
|
97
|
+
const next = args[args.length - 1]
|
|
98
|
+
const request = args.find(a => a && typeof a === 'object' && Array.isArray(a.questions)) || {}
|
|
99
|
+
let key = questionKeys.get(request)
|
|
100
|
+
if (!key) {
|
|
101
|
+
const ids = (request.questions || []).map(q => q?.id).filter(Boolean).join(',')
|
|
102
|
+
key = `q:${ids}#${++anonymous}`
|
|
103
|
+
questionKeys.set(request, key)
|
|
104
|
+
}
|
|
105
|
+
schedule(key, `pending question (${(request.questions || []).length} question(s))`)
|
|
106
|
+
// Preserve the original waterfall result, including rejecting promises.
|
|
107
|
+
let answer
|
|
108
|
+
try { answer = typeof next === 'function' ? next() : undefined }
|
|
109
|
+
catch (e) { finish(key); throw e }
|
|
110
|
+
if (answer?.then) answer.then(() => finish(key), () => finish(key))
|
|
111
|
+
else finish(key)
|
|
112
|
+
return answer
|
|
113
|
+
}
|
|
114
|
+
const approval = (session, event) => {
|
|
115
|
+
const id = event?.data?.id
|
|
116
|
+
if (typeof id !== 'string' || !id) return
|
|
117
|
+
const key = `a:${typeof session === 'string' ? session : session?.id || ''}:${id}`
|
|
118
|
+
if (event.type === 'approval/decided') { finish(key); return }
|
|
119
|
+
if (event.type === 'approval/asked') schedule(key, `pending approval ${id} (${event.data.toolName || 'tool'})`)
|
|
120
|
+
}
|
|
121
|
+
for (const target of new Set([ctx, desktopCtx])) {
|
|
122
|
+
target.on('user-questions/request', question, { prepend: true })
|
|
123
|
+
target.on('session/event', approval, { prepend: true })
|
|
124
|
+
}
|
|
125
|
+
desktopCtx.on('dispose', () => {
|
|
126
|
+
disposed = true
|
|
127
|
+
for (const key of pending.keys()) finish(key)
|
|
128
|
+
void native.close()
|
|
129
|
+
})
|
|
130
|
+
record({ stage: 'installed', hostMode: nativeHost ? 'official-desktop-native' : 'legacy-desktop-runtime', pid: process.pid, execPath: process.execPath })
|
|
131
|
+
log.info('human attention enabled: foreground, measured visibility, flash request, persistent diagnostics')
|
|
132
|
+
}
|
|
133
|
+
if (nativeHost) install(ctx)
|
|
134
|
+
else ctx.inject(['desktopRuntime'], install)
|
|
135
|
+
return (key, what) => {
|
|
136
|
+
if (raise) return raise(key, what)
|
|
137
|
+
log.warn(`human attention: desktopRuntime not ready for ${what}`)
|
|
138
|
+
return Promise.resolve(false)
|
|
139
|
+
}
|
|
140
|
+
}
|
package/lib/audit.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// audit.js — snapshot paths + JSONL audit trail for dsh-computer-use.
|
|
2
|
+
// Every actuation the core emits lands here: {ts, op, args(redacted), result?|error?}.
|
|
3
|
+
// Screenshots taken before risky ops (v1) will join the same stream.
|
|
4
|
+
|
|
5
|
+
import { appendFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { randomUUID } from 'node:crypto'
|
|
9
|
+
|
|
10
|
+
export function mkDataDir (configured) {
|
|
11
|
+
const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
12
|
+
const dir = configured && configured.length > 0
|
|
13
|
+
? configured
|
|
14
|
+
: join(dshHome, 'data', 'computer-use')
|
|
15
|
+
const shots = join(dir, 'shots')
|
|
16
|
+
if (!existsSync(shots)) mkdirSync(shots, { recursive: true })
|
|
17
|
+
return { root: dir, shots }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function snapshotPath (dataDir) {
|
|
21
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19)
|
|
22
|
+
const ms = Date.now() % 1000
|
|
23
|
+
return join(dataDir.shots, `shot-${ts}-${ms}-${randomUUID()}.jpg`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function appendAudit (dataDir, rec) {
|
|
27
|
+
try {
|
|
28
|
+
appendFileSync(join(dataDir.root, 'audit.jsonl'), JSON.stringify(rec) + '\n')
|
|
29
|
+
} catch { /* audit must never break the action */ }
|
|
30
|
+
}
|
package/lib/batch.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Serial execution and observation selection; no desktop or lifecycle side effects here.
|
|
2
|
+
const reads = new Set(['computer_state', 'computer_uia', 'computer_marks', 'computer_shot', 'computer_wait'])
|
|
3
|
+
const isRead = (step) => reads.has(step?.tool) || (step?.tool === 'computer_clip' && step.args?.text === undefined)
|
|
4
|
+
const isReadback = (step) => ['computer_state', 'computer_uia', 'computer_marks', 'computer_shot'].includes(step.tool) ||
|
|
5
|
+
(step.tool === 'computer_clip' && step.args?.text === undefined)
|
|
6
|
+
|
|
7
|
+
export function actionRefusal (step, result) {
|
|
8
|
+
if (result?.ok === false || result?.success === false) return result.error || 'action reported failure'
|
|
9
|
+
if (step.tool === 'computer_window' && (step.args?.op || 'activate') === 'activate' && result?.activated !== true) {
|
|
10
|
+
return 'target window activation was not confirmed; subsequent actions were not dispatched'
|
|
11
|
+
}
|
|
12
|
+
if (step.tool === 'computer_ctrl' && step.args?.action === 'recover' && result?.recovered !== true) return result.reason || 'temporary pause recovery refused'
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runBatch ({ steps, handlers, signal, stopped, screenshot, shot = 'auto', exec }) {
|
|
17
|
+
if (!Array.isArray(steps) || !['auto', 'always', 'never'].includes(shot)) throw new Error('invalid batch actions or shot mode')
|
|
18
|
+
const actions = []
|
|
19
|
+
let failed = false
|
|
20
|
+
for (const step of steps) {
|
|
21
|
+
const name = step?.tool
|
|
22
|
+
if (failed || signal?.aborted) {
|
|
23
|
+
actions.push({ tool: name || '', ok: false, outcome: 'not-dispatched', error: signal?.aborted ? 'cancelled' : 'earlier action failed' })
|
|
24
|
+
failed = true
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
const fn = Object.hasOwn(handlers, name) && handlers[name]
|
|
28
|
+
if (!fn) { actions.push({ tool: name || '', ok: false, outcome: 'not-dispatched', error: 'unknown tool' }); failed = true; continue }
|
|
29
|
+
try {
|
|
30
|
+
const stoppedBefore = stopped()
|
|
31
|
+
const result = await fn(step.args || {}, exec)
|
|
32
|
+
const refusal = actionRefusal(step, result)
|
|
33
|
+
actions.push({ tool: name, ok: !refusal, result, ...(refusal ? { error: refusal } : {}) })
|
|
34
|
+
failed = !!refusal || (!stoppedBefore && stopped()) ||
|
|
35
|
+
(name === 'computer_ctrl' && ['stop', 'exit'].includes(step.args?.action))
|
|
36
|
+
} catch (error) {
|
|
37
|
+
actions.push({ tool: name, ok: false, error: String(error.message || error),
|
|
38
|
+
code: error.code || 'action-error', outcome: error.outcome || 'unknown' })
|
|
39
|
+
failed = true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const needsShot = shot === 'always' || (shot === 'auto' && steps.some(s => !isRead(s)) && !isReadback(steps.at(-1) || {}))
|
|
43
|
+
const result = { actions, attachedShot: null }
|
|
44
|
+
if (!failed && !signal?.aborted && !stopped() && needsShot) {
|
|
45
|
+
try { result.attachedShot = await screenshot() }
|
|
46
|
+
catch (error) { result.observationError = String(error.message || error) }
|
|
47
|
+
}
|
|
48
|
+
// A transport/provider return does not prove that a form was saved or a message was sent.
|
|
49
|
+
if (steps.some(s => s && !isRead(s))) result.verification = 'Inspect returned readback or image before claiming the task effect; ok only means the action returned.'
|
|
50
|
+
if (result.attachedShot) result.view = 'read_image the attachedShot if visual verification is needed'
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
param(
|
|
2
|
+
# RESOLVED FROM THIS SCRIPT'S OWN LOCATION, NOT FROM A MACHINE-SPECIFIC PATH (2026-09-16).
|
|
3
|
+
# It used to default to `D:\BuyKey\dsh-computer-use\packages\computer-use-core\src\worker.cs`, which is
|
|
4
|
+
# correct on exactly one computer — and `compileWorkerSync()` in index.js calls this script WITHOUT
|
|
5
|
+
# `-Path`, so on any other machine the plugin's compile-on-demand path pointed at a file that is not
|
|
6
|
+
# there. That is a silent portability failure: the plugin only reaches it when no cached worker exists,
|
|
7
|
+
# i.e. on a fresh install, i.e. on the machine that has never worked. `$PSScriptRoot` is the directory
|
|
8
|
+
# this file lives in, and `worker.cs` ships beside it.
|
|
9
|
+
[string]$Path = '',
|
|
10
|
+
[switch]$Force,
|
|
11
|
+
# OPTIONAL NAMESPACE FOR THE CACHE DIRECTORY. The cache key is the source's sha16, which is right
|
|
12
|
+
# for the deployed worker but wrong for a TEST SANDBOX: two runs of the same test compile identical
|
|
13
|
+
# sources, so they land on the SAME exe path, and a worker left running by an earlier run holds
|
|
14
|
+
# that file open — `csc` then fails with CS0016 ("cannot write output file") and a test that cannot
|
|
15
|
+
# rebuild its fault-injected worker silently has nothing to measure. Measured exactly that:
|
|
16
|
+
# build/test-ask-release.mjs alternated pass/fail depending on whether a stale sandbox worker from
|
|
17
|
+
# its own previous run was still alive. A caller that needs isolation passes a unique token (e.g.
|
|
18
|
+
# its own pid); the default is '' so the deployed worker's cache path is unchanged.
|
|
19
|
+
[string]$CacheToken = ''
|
|
20
|
+
)
|
|
21
|
+
if (-not $Path) { $Path = Join-Path $PSScriptRoot 'worker.cs' }
|
|
22
|
+
# compile-worker.ps1 — turn worker.cs into a cached native worker.exe.
|
|
23
|
+
# Uses ONLY the C# 5 compiler shipping with Windows (Framework64 csc.exe):
|
|
24
|
+
# no SDK, no node-gyp, no network. C# source stays C#-5-compatible on purpose.
|
|
25
|
+
# Cache: %LOCALAPPDATA%\dsh-computer-use\worker\<sha16>[<CacheToken>]\worker.exe
|
|
26
|
+
$ErrorActionPreference = 'Stop'
|
|
27
|
+
|
|
28
|
+
$cacheRoot = Join-Path $env:LOCALAPPDATA 'dsh-computer-use\worker'
|
|
29
|
+
$sha = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
|
|
30
|
+
$outDir = Join-Path $cacheRoot ($sha.Substring(0, 16) + $CacheToken)
|
|
31
|
+
$outExe = Join-Path $outDir 'dsh-computer-use-worker.exe'
|
|
32
|
+
Write-Host "worker cache dir: $outDir"
|
|
33
|
+
|
|
34
|
+
if ((Test-Path -LiteralPath $outExe) -and -not $Force) {
|
|
35
|
+
Write-Host 'cached worker is current; use -Force to recompile'
|
|
36
|
+
Write-Output $outExe
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$csc = "$env:windir\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
|
|
41
|
+
if (-not (Test-Path -LiteralPath $csc)) { throw 'csc.exe not found (.NET Framework 4.x missing?)' }
|
|
42
|
+
|
|
43
|
+
# csc does not resolve GAC simple names — pass full assembly paths
|
|
44
|
+
$gac = "$env:windir\Microsoft.NET\assembly\GAC_MSIL"
|
|
45
|
+
$refs = @(
|
|
46
|
+
'/r:System.dll', '/r:System.Core.dll', '/r:System.Drawing.dll',
|
|
47
|
+
'/r:System.Web.Extensions.dll', '/r:System.Windows.Forms.dll',
|
|
48
|
+
"/r:$gac\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll",
|
|
49
|
+
"/r:$gac\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll",
|
|
50
|
+
"/r:$gac\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
|
54
|
+
& $csc /nologo /target:exe /platform:x64 /optimize+ /warn:1 /out:$outExe $refs $Path
|
|
55
|
+
if ($LASTEXITCODE -ne 0) { throw "csc failed with exit code $LASTEXITCODE" }
|
|
56
|
+
if (-not (Test-Path -LiteralPath $outExe)) { throw 'compile produced no exe' }
|
|
57
|
+
Write-Host "compiled OK -> $outExe"
|
|
58
|
+
Write-Output $outExe
|