dsh-codex-style-computer-use 2.0.0 → 2.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/README.ar.md +234 -0
- package/README.de.md +234 -0
- package/README.es.md +264 -0
- package/README.fa.md +234 -0
- package/README.fr.md +243 -0
- package/README.md +41 -8
- package/README.pt.md +234 -0
- package/README.ru.md +259 -0
- package/README.zh.md +41 -8
- package/icon.svg +20 -4
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +1 -1
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/index.js +31 -0
- package/lib/core/worker.cs +380 -10
- package/lib/image-output.js +28 -6
- package/lib/launch.js +67 -0
- package/lib/policy.js +22 -2
- package/lib/tools.js +74 -8
- package/package.json +5 -1
- package/scripts/verify-package.mjs +9 -0
- package/skills/computer-use/SKILL.md +50 -50
package/lib/launch.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// launch.js — validation for computer_launch. EXECUTION lives in the worker (lib/core/worker.cs
|
|
2
|
+
// `launch` op) on purpose: a launch must go through the SAME funnel as every other actuation —
|
|
3
|
+
// brake/pause checks, the status halo, the audit JSONL and the rate window — and the human has
|
|
4
|
+
// to SEE that the machine was just acted on. A Node-side spawn skipped all of that (measured
|
|
5
|
+
// 2026-11-07: the halo never lit, so nothing told the human an app was opening).
|
|
6
|
+
//
|
|
7
|
+
// Validation still lives here so an impossible target is refused BEFORE dispatch: the receipt
|
|
8
|
+
// says outcome:"not-dispatched", and that has to be true. The policy layer also needs the string.
|
|
9
|
+
//
|
|
10
|
+
// RESOLUTION IS ShellExecute's (worker: ProcessStartInfo.UseShellExecute). Measured on this
|
|
11
|
+
// machine: `explorer.exe calc` opens a FILE EXPLORER window (a bare word is a location to
|
|
12
|
+
// explorer), while ShellExecute("calc") starts Calculator — so the launcher, not a shell
|
|
13
|
+
// wrapper, is the correct resolver for App Paths aliases, shell: verbs, URLs and associations.
|
|
14
|
+
//
|
|
15
|
+
// The target crosses as ONE value: no command line is composed here or in the worker, and no
|
|
16
|
+
// shell interpreter is involved. The deny-list below is defence in depth for anything
|
|
17
|
+
// downstream that might interpolate the string — the boundary is the single-value call.
|
|
18
|
+
const MAX_LEN = 2000
|
|
19
|
+
|
|
20
|
+
// Control characters: C0 (includes \r \n \t), DEL, C1, and the Unicode line/paragraph
|
|
21
|
+
// separators (U+2028/U+2029 still break line discipline in several parsers). None has an
|
|
22
|
+
// honest use in a path, URL or alias.
|
|
23
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/
|
|
24
|
+
|
|
25
|
+
// Metacharacters for a shell or a string interpolator. Percent is deliberately NOT here:
|
|
26
|
+
// "%20"-style percent-encoding is a legitimate, unavoidable part of URLs, and no shell is
|
|
27
|
+
// involved, so there is no %VAR% expansion to fear.
|
|
28
|
+
const INJECTIVE = /["'&|;<>$`^]/
|
|
29
|
+
|
|
30
|
+
const badTarget = (why) => {
|
|
31
|
+
throw Object.assign(new Error(`bad launch target: ${why}`), { code: 'LAUNCH_BAD_TARGET', outcome: 'not-dispatched' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Trim and validate a launch target; throws LAUNCH_BAD_TARGET (not-dispatched) when impossible.
|
|
35
|
+
* Everything is checked on the TRIMMED string, i.e. on exactly what the worker will receive. */
|
|
36
|
+
export function validateLaunchTarget (target) {
|
|
37
|
+
if (typeof target !== 'string') badTarget('must be a string')
|
|
38
|
+
const t = target.trim()
|
|
39
|
+
if (!t) badTarget('empty (or only whitespace)')
|
|
40
|
+
if (t.length > MAX_LEN) badTarget(`longer than ${MAX_LEN} characters`)
|
|
41
|
+
const c = CONTROL.exec(t)
|
|
42
|
+
if (c) badTarget(`control character U+${c[0].codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`)
|
|
43
|
+
const m = INJECTIVE.exec(t)
|
|
44
|
+
if (m) badTarget(`forbidden character ${JSON.stringify(m[0])}`)
|
|
45
|
+
return t
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const badArgs = (why) => {
|
|
49
|
+
throw Object.assign(new Error(`bad launch arguments: ${why}`), { code: 'LAUNCH_BAD_ARGS', outcome: 'not-dispatched' })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Validate the optional argument array. Each entry is ONE argument VALUE — never a command
|
|
53
|
+
* line, never shell syntax: the worker quotes each entry exactly once. Spaces, backslashes and
|
|
54
|
+
* `--flag=value` are ordinary; only NUL, control characters and empties are impossible. */
|
|
55
|
+
export function validateLaunchArgs (args) {
|
|
56
|
+
if (args === undefined || args === null) return undefined
|
|
57
|
+
if (!Array.isArray(args)) badArgs('must be an array of strings')
|
|
58
|
+
if (args.length > 64) badArgs('more than 64 arguments')
|
|
59
|
+
return args.map((value, index) => {
|
|
60
|
+
if (typeof value !== 'string') badArgs(`argument ${index} must be a string`)
|
|
61
|
+
if (value.length === 0) badArgs(`argument ${index} is empty`)
|
|
62
|
+
if (value.length > MAX_LEN) badArgs(`argument ${index} longer than ${MAX_LEN} characters`)
|
|
63
|
+
const c = CONTROL.exec(value)
|
|
64
|
+
if (c) badArgs(`argument ${index} contains control character U+${c[0].codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`)
|
|
65
|
+
return value
|
|
66
|
+
})
|
|
67
|
+
}
|
package/lib/policy.js
CHANGED
|
@@ -27,9 +27,18 @@ export const OBSERVATION_TOOLS = new Set([
|
|
|
27
27
|
export const ACTUATION_TOOLS = new Set([
|
|
28
28
|
'computer_click', 'computer_move', 'computer_drag', 'computer_scroll', 'computer_select',
|
|
29
29
|
'computer_key', 'computer_type', 'computer_uia_act', 'computer_window', 'computer_batch',
|
|
30
|
+
'computer_launch', 'computer_focus',
|
|
30
31
|
])
|
|
31
32
|
|
|
32
33
|
export const HIGH_RISK_NOTE = 'high-risk action (destructive key / closing a window / password field) — always requires human approval'
|
|
34
|
+
export const LAUNCH_HIGH_RISK_NOTE = 'high-risk launch (executable / script / installer / UNC / file: target) — explorer will run what it resolves; always requires human approval'
|
|
35
|
+
|
|
36
|
+
// A launch target that IS code (or points at it) escalates everywhere it can: the launcher
|
|
37
|
+
// executes what it resolves, so argv discipline cannot make an .exe/.bat/UNC target "just a
|
|
38
|
+
// file". Extensions only by design: bare App-Paths aliases (calc, cmd) stay allowed — the
|
|
39
|
+
// measured trap here was the opposite direction (explorer opened a FOLDER for an alias), and
|
|
40
|
+
// a path spelling of the same binary asks while an alias does not carry arguments either way.
|
|
41
|
+
const LAUNCH_CODE_TARGET = /\.(exe|scr|msi|hta|bat|cmd|com|pif|cpl|msc|reg|jse|vbe|vbs|ws|wsf|wsh|ps1|lnk|url|jar|application|appref-ms|appx|msix|gadget|sct)$/i
|
|
33
42
|
|
|
34
43
|
/** @returns {{kind:'allow'|'deny'|'ask', reason?: string}} */
|
|
35
44
|
export function policyDecision (name, args, mode) {
|
|
@@ -76,14 +85,15 @@ export function policyDecision (name, args, mode) {
|
|
|
76
85
|
function decideActuation (name, a, mode) {
|
|
77
86
|
let hr
|
|
78
87
|
try { hr = highRisk(name, a) } catch (error) { return { kind: 'deny', reason: error.message } }
|
|
88
|
+
const note = name === 'computer_launch' ? LAUNCH_HIGH_RISK_NOTE : HIGH_RISK_NOTE
|
|
79
89
|
switch (mode) {
|
|
80
90
|
case 'read-only':
|
|
81
91
|
return { kind: 'deny', reason: `automationMode=read-only: ${name} drives the machine` }
|
|
82
92
|
case 'standard':
|
|
83
|
-
return hr ? { kind: 'ask', reason:
|
|
93
|
+
return hr ? { kind: 'ask', reason: note + riskSuffix(name, a) }
|
|
84
94
|
: { kind: 'ask', reason: `automationMode=standard: actuation ${name}` }
|
|
85
95
|
case 'autonomous':
|
|
86
|
-
return hr ? { kind: 'ask', reason:
|
|
96
|
+
return hr ? { kind: 'ask', reason: note + riskSuffix(name, a) } : { kind: 'allow' }
|
|
87
97
|
case 'unrestricted':
|
|
88
98
|
return { kind: 'allow' }
|
|
89
99
|
default:
|
|
@@ -98,6 +108,7 @@ function riskSuffix (name, a) {
|
|
|
98
108
|
if (name === 'computer_key') return ` [combo=${a.combo}]`
|
|
99
109
|
if (name === 'computer_window') return ` [op=${a.op}]`
|
|
100
110
|
if (name === 'computer_type') return ' [allowPassword]'
|
|
111
|
+
if (name === 'computer_launch') return ` [target=${String(a.target || '').slice(0, 80)}]`
|
|
101
112
|
if (name === 'computer_batch') {
|
|
102
113
|
const actions = Array.isArray(a.actions) ? a.actions : []
|
|
103
114
|
for (let i = 0; i < actions.length; i++) {
|
|
@@ -118,6 +129,15 @@ export function highRisk (name, args) {
|
|
|
118
129
|
}
|
|
119
130
|
if (name === 'computer_window' && String(a.op || 'activate') === 'close') return true
|
|
120
131
|
if (name === 'computer_type' && a.allowPassword) return true
|
|
132
|
+
if (name === 'computer_launch') {
|
|
133
|
+
// Win32 strips trailing dots/spaces/ADS-colons when resolving, so "x.exe." RUNS as x.exe —
|
|
134
|
+
// normalize before matching or the $ anchor is bypassable. http(s) targets lose ?query and
|
|
135
|
+
// #fragment first so "x.exe?dl=1" is judged exactly like "x.exe". Forward slashes map to
|
|
136
|
+
// backslashes at resolve time, so //server/share is the UNC it will become.
|
|
137
|
+
let target = String(a.target || '').trim().replace(/[.\s:]+$/, '')
|
|
138
|
+
if (/^\w+:\/\//i.test(target)) target = target.replace(/[?#].*$/, '')
|
|
139
|
+
if (LAUNCH_CODE_TARGET.test(target) || /^\\\\|^\/\//.test(target) || /^file:/i.test(target)) return true
|
|
140
|
+
}
|
|
121
141
|
if (name === 'computer_batch') {
|
|
122
142
|
const actions = Array.isArray(a.actions) ? a.actions : []
|
|
123
143
|
for (const step of actions) {
|
package/lib/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// tools.js — defineTool registrations for dsh-computer-use.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 20 TOOLS. The previous 28 were merged, not deleted; every capability still exists and the
|
|
4
4
|
// behavioural detail lives in the skill (loaded only for computer-use work, while these
|
|
5
5
|
// definitions are paid for on EVERY request).
|
|
6
6
|
//
|
|
@@ -22,11 +22,13 @@ import { ObservationStore } from './observations.js'
|
|
|
22
22
|
import { feedbackPolicy, feedbackActions, feedbackVerification, selectFeedbackDetail } from './feedback.js'
|
|
23
23
|
import { collectCompactState, limitCompactFeedback, feedbackViewport } from './compact-feedback.js'
|
|
24
24
|
import { handoffResult } from './continuation.js'
|
|
25
|
+
import { validateLaunchArgs, validateLaunchTarget } from './launch.js'
|
|
25
26
|
import { join } from 'node:path'
|
|
26
27
|
|
|
27
28
|
const P = (type, description, extra = {}) => ({ type, description, ...extra })
|
|
28
29
|
const cooperativeTools = new Set(['computer_click', 'computer_move', 'computer_drag', 'computer_scroll',
|
|
29
|
-
'computer_select', 'computer_key', 'computer_type', 'computer_window', 'computer_uia_act', 'computer_batch', 'computer_clip'
|
|
30
|
+
'computer_select', 'computer_key', 'computer_type', 'computer_window', 'computer_uia_act', 'computer_batch', 'computer_clip',
|
|
31
|
+
'computer_focus'])
|
|
30
32
|
const usesInput = (name, args = {}) => name === 'computer_clip' ? args.text !== undefined :
|
|
31
33
|
name === 'computer_batch' ? (args.actions || []).some(step => usesInput(step.tool, step.args)) :
|
|
32
34
|
cooperativeTools.has(name)
|
|
@@ -73,8 +75,21 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
73
75
|
return saveOwnedShot(dataDir, p, buf)
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// Per-call image delivery. `auto` is the existing behaviour and stays the default; `path`
|
|
79
|
+
// withholds the image block without withholding the file. An unknown value refuses here
|
|
80
|
+
// rather than silently behaving like auto — a typo must not quietly reintroduce the cost the
|
|
81
|
+
// caller asked to avoid.
|
|
82
|
+
const deliveryMode = (value) => {
|
|
83
|
+
if (value === undefined || value === 'auto') return 'auto'
|
|
84
|
+
if (value === 'path') return 'path'
|
|
85
|
+
throw Object.assign(new Error('delivery must be "auto" or "path"'), { code: 'BAD_DELIVERY', outcome: 'not-dispatched' })
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
// Shared by direct calls, explicit batch steps, and the batch checkpoint.
|
|
77
89
|
const takeScreenshot = async (args, exec) => {
|
|
90
|
+
// Refuse a bad value BEFORE the capture: a refused request that already wrote a file and
|
|
91
|
+
// reports outcome:"not-dispatched" is a false receipt.
|
|
92
|
+
const delivery = deliveryMode(args.delivery)
|
|
78
93
|
const r = rect(args.region)
|
|
79
94
|
const shot = await cu.screenshot({
|
|
80
95
|
x: r && r.x, y: r && r.y, width: r && r.width, height: r && r.height,
|
|
@@ -83,7 +98,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
83
98
|
})
|
|
84
99
|
const path = saveShot(shot.image, shot.mime === 'image/png' ? 'png' : 'jpg')
|
|
85
100
|
const res = { path, mime: shot.mime, region: shot.region, ...screenshotMetadata(shot) }
|
|
86
|
-
return attachScreenshot(ctx, exec, res, shot.image, { rules: config.feedbackRules })
|
|
101
|
+
return attachScreenshot(ctx, exec, res, shot.image, { rules: config.feedbackRules, delivery })
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
const clickTarget = async (args) => {
|
|
@@ -149,7 +164,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
149
164
|
if (exec?.signal?.aborted || cu.stopped || cu.exited || cu.cooperation?.active) return true
|
|
150
165
|
try { return !!hooks?.askBlocked?.('computer_shot') } catch { return true }
|
|
151
166
|
}
|
|
152
|
-
const captureFeedback = async (exec, policy, context = {}) => {
|
|
167
|
+
const captureFeedback = async (exec, policy, context = {}, delivery = 'auto') => {
|
|
153
168
|
// Route resolution is async: the human can brake while it is pending.
|
|
154
169
|
if (feedbackBlocked(exec)) return { observationSkipped: 'cancelled-or-paused' }
|
|
155
170
|
const compact = policy.detail === 'compact'
|
|
@@ -160,7 +175,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
160
175
|
const observation = policy.kind === 'state'
|
|
161
176
|
? compact ? await collectCompactState(cu, { ...context, checkAllowed,
|
|
162
177
|
archive: raw => hooks?.archiveFeedback ? hooks.archiveFeedback(exec, raw) : archiveFeedback(exec, raw) }) : await observeState({}, exec)
|
|
163
|
-
: await takeScreenshot(compact ? { region: feedbackViewport(context.args, context.receipt).region } : {}, exec)
|
|
178
|
+
: await takeScreenshot(compact ? { region: feedbackViewport(context.args, context.receipt).region, delivery } : { delivery }, exec)
|
|
164
179
|
const envelope = { observation, feedback: policy, verification: feedbackVerification }
|
|
165
180
|
if (compact && policy.kind === 'state' && observation.inlineEvidence) {
|
|
166
181
|
// Resource fallback deliberately favors recoverable evidence over the
|
|
@@ -354,6 +369,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
354
369
|
preset: P('string', 'taskbar'),
|
|
355
370
|
png: P('boolean', 'PNG instead of JPEG'),
|
|
356
371
|
wait: P('string', 'auto (default) | none'),
|
|
372
|
+
delivery: P('string', 'auto (default) | path — path writes the file and returns its absolute path with no image block'),
|
|
357
373
|
},
|
|
358
374
|
output: out((a, v) => renderScreenshot(v)),
|
|
359
375
|
isConcurrencySafe: () => false,
|
|
@@ -496,6 +512,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
496
512
|
clicks: P('number', '1 | 2 (double) | 3 (triple)'),
|
|
497
513
|
shift: P('boolean', 'hold SHIFT (range selection)'),
|
|
498
514
|
verificationRegion: P('string', 'expected effect region "x,y,w,h" in screen pixels; may differ from click point; a change alone does not prove task success'),
|
|
515
|
+
allowCovered: P('boolean', 'click a pixel owned by the target app\'s OWN embedded renderer (a descendant process of the target, e.g. a WeChat article panel); an unrelated window on top is still refused and the receipt says COVERED-UNRELATED'),
|
|
499
516
|
},
|
|
500
517
|
output: outJSON(),
|
|
501
518
|
isConcurrencySafe: () => false,
|
|
@@ -626,6 +643,49 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
626
643
|
async execute (args) { return args.text === undefined ? cu.clipRead() : cu.clipWrite(args.text) },
|
|
627
644
|
})
|
|
628
645
|
|
|
646
|
+
// ---------------- launch ----------------
|
|
647
|
+
// ONE call replaces the Win+R route (activate, combo, type, enter, verify): the worker resolves
|
|
648
|
+
// the target with ShellExecute, so App Paths aliases (calc), shell: folders, URLs, folders and
|
|
649
|
+
// document associations all work. The launch is a real actuation — brake checks, the status
|
|
650
|
+
// halo (with a short self-expiring tail, because a launch returns in milliseconds) and the
|
|
651
|
+
// audit trail all come from the worker op, not from this file.
|
|
652
|
+
reg({
|
|
653
|
+
name: 'computer_launch',
|
|
654
|
+
description: 'Open an app/URL/folder/document in one call — replaces the Win+R route (activate, combo, type, enter). Aliases (calc), shell: folders, http/https URLs, absolute paths. Pass `args` to start an executable with arguments (a dedicated browser profile: target "…\\msedge.exe", args ["--user-data-dir=…", "--new-window", "<url>"]); each entry is one argument value, quoted once in the worker. Launched means the target reached the launcher, not that the app is ready; verify with computer_state.',
|
|
655
|
+
parameters: {
|
|
656
|
+
target: P('string', 'app alias (calc) | shell: folder | http/https URL | absolute path', { required: true }),
|
|
657
|
+
args: P('array', 'optional argument values for an executable target, one array entry per argument (e.g. ["--user-data-dir=D:\\\\profiles\\\\job", "--new-window", "https://…"]); no shell quoting — spaces and backslashes are ordinary'),
|
|
658
|
+
tailMs: P('number', 'how long to keep the status halo lit after the launch, 0–10000 ms (default 2500)'),
|
|
659
|
+
},
|
|
660
|
+
output: outJSON(),
|
|
661
|
+
isConcurrencySafe: () => false,
|
|
662
|
+
timeoutMs: 15_000,
|
|
663
|
+
async execute (args) { return cu.launch(validateLaunchTarget(args.target), { args: validateLaunchArgs(args.args) }) },
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
// ---------------- focus before typing ----------------
|
|
667
|
+
reg({
|
|
668
|
+
name: 'computer_focus',
|
|
669
|
+
description: 'Put keyboard focus on a FIELD and PROVE it before typing: clicks x,y, then reports the focused UIA control and whether a caret is provable there (OS caret, else a two-frame blink check). Typing follows the focused control, not the window — activating a dialog can leave focus in the document, where text is silently swallowed. Type only when verified:true.',
|
|
670
|
+
parameters: {
|
|
671
|
+
x: P('number', 'field centre x in screen pixels', { required: true }),
|
|
672
|
+
y: P('number', 'field centre y in screen pixels', { required: true }),
|
|
673
|
+
hwnd: P('number', 'target window; a modal dialog of the same app is accepted'),
|
|
674
|
+
verifyCaret: P('boolean', 'when the OS reports no caret, fall back to a two-frame blink check (default true)'),
|
|
675
|
+
},
|
|
676
|
+
output: outJSON(),
|
|
677
|
+
isConcurrencySafe: () => false,
|
|
678
|
+
timeoutMs: 15_000,
|
|
679
|
+
async execute (args) {
|
|
680
|
+
return cu.focusField({
|
|
681
|
+
x: args.x,
|
|
682
|
+
y: args.y,
|
|
683
|
+
...(args.hwnd !== undefined ? { hwnd: args.hwnd } : {}),
|
|
684
|
+
...(args.verifyCaret !== undefined ? { verifyCaret: args.verifyCaret } : {}),
|
|
685
|
+
})
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
|
|
629
689
|
// ---------------- the agent asks ----------------
|
|
630
690
|
reg({
|
|
631
691
|
name: 'computer_ask',
|
|
@@ -686,6 +746,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
686
746
|
parameters: {
|
|
687
747
|
actions: P('array', '[{tool:"computer_click", args:{mark:"M3"}}, ...]', { required: true }),
|
|
688
748
|
shot: P('string', 'auto (default) | always | never; no automatic image after failure, cancel or pause'),
|
|
749
|
+
delivery: P('string', 'auto (default) | path — the checkpoint keeps its absolute path but attaches no image'),
|
|
689
750
|
},
|
|
690
751
|
output: out((a, v) => {
|
|
691
752
|
const { observation, ...receipt } = v
|
|
@@ -699,6 +760,9 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
699
760
|
isConcurrencySafe: () => false,
|
|
700
761
|
timeoutMs: 120_000,
|
|
701
762
|
async execute (args, exec) {
|
|
763
|
+
// Validate before any step runs: a bad delivery value must not be discovered only at the
|
|
764
|
+
// checkpoint, after the actions have already been dispatched.
|
|
765
|
+
const delivery = deliveryMode(args.delivery)
|
|
702
766
|
let automatic, feedback, lastContext = {}
|
|
703
767
|
const detail = selectFeedbackDetail(args.feedback,
|
|
704
768
|
config.actionFeedback === false ? 'none' : config.actionFeedbackDetail ?? 'compact')
|
|
@@ -719,15 +783,15 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
719
783
|
if (feedbackBlocked(exec)) { feedback = { observationSkipped: 'cancelled-or-paused' }; return null }
|
|
720
784
|
if (!policy.kind) { feedback = { feedback: policy, observationSkipped: policy.reason || 'disabled' }; return null }
|
|
721
785
|
hooks?.assertOwnership?.(exec)
|
|
722
|
-
if (policy.kind === 'state') { feedback = await captureFeedback(exec, policy, lastContext); return null }
|
|
723
|
-
const captured = await captureFeedback(exec, policy, lastContext)
|
|
786
|
+
if (policy.kind === 'state') { feedback = await captureFeedback(exec, policy, lastContext, delivery); return null }
|
|
787
|
+
const captured = await captureFeedback(exec, policy, lastContext, delivery)
|
|
724
788
|
const { observation, ...details } = captured
|
|
725
789
|
automatic = observation
|
|
726
790
|
feedback = details
|
|
727
791
|
if (!automatic) return null
|
|
728
792
|
return automatic.path
|
|
729
793
|
} })
|
|
730
|
-
return { ...selectBatchScreenshot(result, feedback?.observation ?? automatic, feedback?.feedback?.kind), ...feedback }
|
|
794
|
+
return { ...selectBatchScreenshot(result, feedback?.observation ?? automatic, feedback?.feedback?.kind, delivery), ...feedback }
|
|
731
795
|
},
|
|
732
796
|
})
|
|
733
797
|
|
|
@@ -798,6 +862,8 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
798
862
|
computer_type: (a) => cu.type(a.text, { mode: a.mode, allowPassword: a.allowPassword }),
|
|
799
863
|
computer_window: (a) => ((a.op || 'activate') === 'activate' ? cu.activateWindow({ hwnd: a.hwnd }) : cu.windowOp({ hwnd: a.hwnd }, a.op, rect(a.rect) || {})),
|
|
800
864
|
computer_clip: (a) => (a.text === undefined ? cu.clipRead() : cu.clipWrite(a.text)),
|
|
865
|
+
computer_launch: (a) => cu.launch(validateLaunchTarget(a.target), { args: validateLaunchArgs(a.args) }),
|
|
866
|
+
computer_focus: (a) => cu.focusField({ x: a.x, y: a.y, ...(a.hwnd !== undefined ? { hwnd: a.hwnd } : {}), ...(a.verifyCaret !== undefined ? { verifyCaret: a.verifyCaret } : {}) }),
|
|
801
867
|
computer_uia_act: actUia,
|
|
802
868
|
computer_ask: (a, exec) => askQuestion(a, exec),
|
|
803
869
|
computer_wait: (a) => a.mode === 'stable' ? cu.waitStable({ timeoutMs: a.timeoutMs, hwnd: a.hwnd })
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-style-computer-use",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Codex-style Computer Use for DeepSeek Harness: Windows desktop control, screenshots, UI Automation, human input handoff, approval gates and prebuilt native workers. Install and update from dsh-market without PowerShell 7 or build steps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dsh-codex-style-computer-use": "mcp/src/index.js",
|
|
9
|
+
"dsh-computer-use-mcp": "mcp/src/index.js"
|
|
10
|
+
},
|
|
7
11
|
"exports": {
|
|
8
12
|
".": "./lib/index.js",
|
|
9
13
|
"./package.json": "./package.json",
|
|
@@ -23,6 +23,15 @@ for (const [source, binary] of [
|
|
|
23
23
|
assert.equal(manifest.architecture, 'x64')
|
|
24
24
|
}
|
|
25
25
|
for (const file of ['lib/index.js', 'lib/approval-attribution.js', 'lib/core/native-runtime.js', 'lib/attention-native.js', 'skills/computer-use/SKILL.md', 'mcp/src/index.js']) assert(fs.existsSync(path.join(root, file)), file)
|
|
26
|
+
// The npx entry is model-facing documentation: a bin that is absent, points at a missing file,
|
|
27
|
+
// or lacks the node shebang is a broken promise that the marketplace cannot detect.
|
|
28
|
+
const bins = pkg.bin ?? {}
|
|
29
|
+
assert(Object.keys(bins).length > 0, 'package must expose a bin for the embedded MCP server')
|
|
30
|
+
for (const [name, target] of Object.entries(bins)) {
|
|
31
|
+
const file = path.join(root, target)
|
|
32
|
+
assert(fs.existsSync(file), `bin ${name} target missing: ${target}`)
|
|
33
|
+
assert(fs.readFileSync(file, 'utf8').startsWith('#!/usr/bin/env node'), `bin ${name} target needs a node shebang`)
|
|
34
|
+
}
|
|
26
35
|
const attention = fs.readFileSync(path.join(root, 'lib/attention-native.js'), 'utf8')
|
|
27
36
|
assert(attention.includes("'./core/native-runtime.js'"), 'attention helper must use the bundled core')
|
|
28
37
|
console.log(`ok package: ${pkg.name}@${pkg.version}; prebuilt helpers verified; no install hooks`)
|