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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/README.zh.md +132 -0
  4. package/cordis.patch.yml +17 -0
  5. package/lib/artifacts.js +35 -0
  6. package/lib/attention-native.js +93 -0
  7. package/lib/attention-worker.cs +194 -0
  8. package/lib/attention.js +140 -0
  9. package/lib/audit.js +30 -0
  10. package/lib/batch.js +52 -0
  11. package/lib/bin/attention-worker.exe +0 -0
  12. package/lib/bin/attention-worker.exe.json +7 -0
  13. package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
  14. package/lib/core/bin/dsh-computer-use-worker.exe.json +7 -0
  15. package/lib/core/compile-worker.ps1 +58 -0
  16. package/lib/core/index.js +844 -0
  17. package/lib/core/native-runtime.js +97 -0
  18. package/lib/core/worker.cs +6375 -0
  19. package/lib/cycle.js +314 -0
  20. package/lib/exposure.js +91 -0
  21. package/lib/feedback.js +35 -0
  22. package/lib/image-output.js +92 -0
  23. package/lib/index.js +1715 -0
  24. package/lib/observations.js +78 -0
  25. package/lib/policy.js +109 -0
  26. package/lib/recovery.js +8 -0
  27. package/lib/skill-gate.js +68 -0
  28. package/lib/tools.js +717 -0
  29. package/mcp/package.json +12 -0
  30. package/mcp/src/index.js +335 -0
  31. package/package.json +88 -0
  32. package/scripts/build-native.mjs +7 -0
  33. package/scripts/smoke-native.mjs +38 -0
  34. package/scripts/verify-package.mjs +28 -0
  35. package/skills/computer-use/SKILL.md +74 -0
  36. package/skills/computer-use/references/lifecycle.md +180 -0
  37. package/skills/computer-use/references/maintenance.md +56 -0
  38. package/skills/computer-use/references/precision.md +297 -0
  39. package/skills/computer-use/references/progressive-control.md +30 -0
  40. package/skills/computer-use/references/shared-input.md +27 -0
@@ -0,0 +1,78 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ const clone = value => JSON.parse(JSON.stringify(value))
4
+ const encode = value => JSON.stringify(value)
5
+ const elementKey = e => e.target || (e.runtimeId ? `uia:${e.pid ?? ''}:${e.runtimeId}` : null)
6
+ const windowKey = w => w.hwnd ? `window:${w.pid ?? ''}:${w.hwnd}` : null
7
+
8
+ // An explicit base prevents implicit deltas after compaction or in an unrelated Agent.
9
+ // Full immutable observations are retained, including every provider field; only the wire view differs.
10
+ export class ObservationStore {
11
+ constructor ({ capacity = 64 } = {}) {
12
+ this.capacity = capacity
13
+ this.scopes = new WeakMap()
14
+ this.unscoped = {}
15
+ this.sequence = 0
16
+ this.prefix = randomBytes(4).toString('hex')
17
+ }
18
+ scope (exec) {
19
+ const owner = exec?.agent || this.unscoped
20
+ let cache = this.scopes.get(owner)
21
+ if (!cache) { cache = new Map(); this.scopes.set(owner, cache) }
22
+ return cache
23
+ }
24
+ get (exec, id, kind) {
25
+ const entry = this.scope(exec).get(id)
26
+ if (!entry || entry.kind !== kind) throw new Error('OBSERVATION_UNAVAILABLE: snapshot expired or belongs to another Agent/tool; request a fresh full observation')
27
+ return clone(entry.full)
28
+ }
29
+ ownsTarget (exec, target) {
30
+ return [...this.scope(exec).values()].some(entry => (entry.full.elements || []).some(e => e.target === target))
31
+ }
32
+ publish (exec, kind, query, value, since) {
33
+ const cache = this.scope(exec)
34
+ const fields = kind === 'uia' ? { elements: elementKey } : { windows: windowKey, uiaFlat: elementKey }
35
+ const full = clone({ ...value, observation: `O${this.prefix}-${++this.sequence}`, mode: 'full' })
36
+ // The scope includes the exact query and observed window/process. A change forces a full view.
37
+ const scope = encode([query, value.hwnd ?? value.active?.hwnd ?? null, value.pid ?? value.active?.pid ?? null])
38
+ const previous = since && cache.get(since)
39
+ let wire = full
40
+ if (since) {
41
+ let reason = !previous ? 'base-unavailable' : previous.kind !== kind || previous.scope !== scope ? 'scope-changed'
42
+ : previous.full.truncated || previous.full.uiaTruncated || full.truncated || full.uiaTruncated ? 'incomplete-observation' : null
43
+ if (!reason) {
44
+ const delta = {}
45
+ for (const [field, keyOf] of Object.entries(fields)) {
46
+ const before = previous.full[field] || [], after = full[field] || []
47
+ const keyMap = rows => {
48
+ const map = new Map()
49
+ for (const row of rows) {
50
+ const key = keyOf(row)
51
+ if (!key || map.has(key)) return null
52
+ map.set(key, row)
53
+ }
54
+ return map
55
+ }
56
+ const oldMap = keyMap(before), newMap = keyMap(after)
57
+ if (!oldMap || !newMap) { reason = 'ambiguous-identity'; break }
58
+ delta[field] = { added: [], changed: [], removed: [] }
59
+ for (const [key, row] of newMap) {
60
+ if (!oldMap.has(key)) delta[field].added.push({ key, element: row })
61
+ else if (encode(oldMap.get(key)) !== encode(row)) delta[field].changed.push({ key, element: row })
62
+ }
63
+ for (const key of oldMap.keys()) if (!newMap.has(key)) delta[field].removed.push(key)
64
+ if (encode([...oldMap.keys()]) !== encode([...newMap.keys()])) delta[field].order = [...newMap.keys()]
65
+ }
66
+ if (!reason) {
67
+ wire = { ...full, mode: 'diff', base: since, delta }
68
+ for (const field of Object.keys(fields)) delete wire[field]
69
+ if (encode(wire).length >= encode(full).length) reason = 'delta-not-smaller'
70
+ }
71
+ }
72
+ if (reason) wire = { ...full, resetReason: reason }
73
+ }
74
+ cache.set(full.observation, { full, kind, scope })
75
+ while (cache.size > this.capacity) cache.delete(cache.keys().next().value)
76
+ return wire
77
+ }
78
+ }
package/lib/policy.js ADDED
@@ -0,0 +1,109 @@
1
+ // policy.js — pure approval decision for dsh-computer-use.
2
+ // Modeled on @anweat/dsh-browser's approval-policy.js: a pure function mapping
3
+ // (toolName, args, automationMode) -> {kind:'allow'|'deny'|'ask', reason}.
4
+ // Fail-closed by construction: unknown tool names are denied.
5
+ //
6
+ // ⚠️ KEEP IN SYNC WITH THE TOOL SURFACE. A fail-closed list is only safe if it is also COMPLETE.
7
+ // 2026-09-13: the tool set was rewritten from 28 names to 17 (merged + renamed) and this
8
+ // allow-list was NOT updated, so the plugin's own gate would have denied most of its own tools
9
+ // with "unknown computer-use tool" — before they ever ran. `build/test-policy.mjs` now fails if
10
+ // any registered tool falls through to that branch, so the next rename cannot silently do this.
11
+ //
12
+ // Two tools cannot be classified by name alone and are decided by their ARGUMENTS:
13
+ // computer_clip — no text = read the clipboard; text = write it
14
+ // computer_ctrl — selftest = health; stop = the brake; exit = end the session;
15
+ // calibrate / indicator = control
16
+ // (`resume` is NOT among them any more: it was removed from the tool surface on 2026-09-13 because
17
+ // it un-latched Ctrl+Alt+Q, i.e. a TOOL CALL could re-open a cycle the human had ended. Only the
18
+ // plugin's session/event turn/start opens a cycle, and only the human's Ctrl+Alt+R releases a pause.)
19
+ // computer_ask is argument-independent and BRAKE-LIKE — see its branch below.
20
+
21
+ export const OBSERVATION_TOOLS = new Set([
22
+ 'computer_shot', 'computer_state', 'computer_marks', 'computer_uia', 'computer_wait',
23
+ ])
24
+
25
+ export const ACTUATION_TOOLS = new Set([
26
+ 'computer_click', 'computer_move', 'computer_drag', 'computer_scroll', 'computer_select',
27
+ 'computer_key', 'computer_type', 'computer_uia_act', 'computer_window', 'computer_batch',
28
+ ])
29
+
30
+ // combos that escalate: run dialog, window close, signout/lock/taskmgr, delete-key
31
+ const HIGH_RISK_KEY = /(^|\+)(win\+r|alt\+f4|ctrl\+shift\+esc|win\+l|win\+d)(\+|$)/i
32
+ const DESTRUCTIVE_KEY = /(^|\+)(del|delete|bksp|backspace)(\+|$)/i
33
+
34
+ export const HIGH_RISK_NOTE = 'high-risk action (destructive key / closing a window / password field) — always requires human approval'
35
+
36
+ /** @returns {{kind:'allow'|'deny'|'ask', reason?: string}} */
37
+ export function policyDecision (name, args, mode) {
38
+ const a = args || {}
39
+
40
+ // ---- the two argument-dependent tools -------------------------------------------------
41
+ if (name === 'computer_ctrl') {
42
+ const act = String(a.action || 'selftest')
43
+ // The brake is available in EVERY mode, including read-only, and it drives nothing.
44
+ if (act === 'stop') return { kind: 'allow' }
45
+ if (act === 'recover') return mode === 'read-only'
46
+ ? { kind: 'deny', reason: 'read-only mode cannot recover control' } : { kind: 'allow' }
47
+ if (act === 'selftest') return { kind: 'allow' } // input-path health, never moves the pointer
48
+ // ends the session: it drives nothing, and the human's own Ctrl+Alt+Q is unconditional. It is
49
+ // the second answer of the ask protocol ("I cannot proceed") and the ask notice promises the
50
+ // human the agent can give it — denying it made that answer dead in all four modes.
51
+ if (act === 'exit') return { kind: 'allow' }
52
+ if (act === 'calibrate' || act === 'indicator') return decideActuation(name, a, mode)
53
+ return { kind: 'deny', reason: `unknown computer_ctrl action: ${act}` }
54
+ }
55
+
56
+ // ---- BRAKE-LIKE, and therefore NEVER gated -------------------------------------------------
57
+ // computer_ask actuates NOTHING: it stops the machine, raises the host window and asks the human
58
+ // a question. That is the SAFETY DIRECTION — it is the one call that must never sit behind an
59
+ // approval prompt, because what it is asking for IS human attention. Gating it is exactly
60
+ // backwards. Until 2026-09-13 it fell through to the fail-closed "unknown computer-use tool"
61
+ // branch and was DENIED in all four modes, which silently killed the human's own "ask + 5 秒"
62
+ // rule (their spec: "一旦出现 computer ask, 这时候返回到 DSH desktop 窗口. 若5秒规则内有回应,
63
+ // 则进入暂停态").
64
+ if (name === 'computer_ask') return { kind: 'allow' }
65
+ if (name === 'computer_clip') {
66
+ if (a.text === undefined) return { kind: 'allow' } // read
67
+ return decideActuation(name, a, mode) // write
68
+ }
69
+
70
+ if (OBSERVATION_TOOLS.has(name)) return { kind: 'allow' }
71
+ if (ACTUATION_TOOLS.has(name)) return decideActuation(name, a, mode)
72
+ return { kind: 'deny', reason: `unknown computer-use tool: ${name}` }
73
+ }
74
+
75
+ function decideActuation (name, a, mode) {
76
+ const hr = highRisk(name, a)
77
+ switch (mode) {
78
+ case 'read-only':
79
+ return { kind: 'deny', reason: `automationMode=read-only: ${name} drives the machine` }
80
+ case 'standard':
81
+ return hr ? { kind: 'ask', reason: HIGH_RISK_NOTE }
82
+ : { kind: 'ask', reason: `automationMode=standard: actuation ${name}` }
83
+ case 'autonomous':
84
+ return hr ? { kind: 'ask', reason: HIGH_RISK_NOTE } : { kind: 'allow' }
85
+ case 'unrestricted':
86
+ return { kind: 'allow' }
87
+ default:
88
+ return { kind: 'deny', reason: `unknown automationMode: ${mode}` }
89
+ }
90
+ }
91
+
92
+ export function highRisk (name, args) {
93
+ const a = args || {}
94
+ if (name === 'computer_key') {
95
+ const combo = String(a.combo || '')
96
+ if (HIGH_RISK_KEY.test(combo) || DESTRUCTIVE_KEY.test(combo)) return true
97
+ }
98
+ if (name === 'computer_window' && String(a.op || 'activate') === 'close') return true
99
+ if (name === 'computer_type' && a.allowPassword) return true
100
+ if (name === 'computer_batch') {
101
+ const actions = Array.isArray(a.actions) ? a.actions : []
102
+ for (const step of actions) {
103
+ if (!step) continue
104
+ const t = String(step.tool || '')
105
+ if ((ACTUATION_TOOLS.has(t) || t === 'computer_clip' || t === 'computer_ctrl') && highRisk(t, step.args || {})) return true
106
+ }
107
+ }
108
+ return false
109
+ }
@@ -0,0 +1,8 @@
1
+ // Host facts checked independently of the native pause credential.
2
+ export function recoveryProblem (permit, claim, cycle, pendingApprovals, exited) {
3
+ if (!permit || !claim.sessionKey || !claim.agentKey) return 'no temporary pause owned by this execution'
4
+ if (pendingApprovals) return 'human approval is pending'
5
+ if (exited || cycle.state !== 'paused' || cycle.id !== permit.cycleId) return 'the original paused cycle changed'
6
+ if (cycle.ownerIdent !== claim.ident || permit.ownerIdent !== claim.ident) return 'the pause belongs to another owner'
7
+ return null
8
+ }
@@ -0,0 +1,68 @@
1
+ // skill-gate.js — "READ THE PROCEDURE BEFORE DRIVING".
2
+ //
3
+ // The human's rule (2026-09-13, verbatim): "未先读 skill 时拒绝驱动类 op". It exists because an
4
+ // agent that skipped the manual improvised its way through a two-call job: it never looked at the
5
+ // window list, spent three rounds attacking an occlusion that did not exist (the windows were
6
+ // MINIMIZED), guessed taskbar coordinates from a scaled screenshot, and then reported all of it to
7
+ // the human as "the environment blocked it".
8
+ //
9
+ // No tool layer can SEE whether a model read a document. So the receipt is a PHRASE THAT ONLY EXISTS
10
+ // INSIDE SKILL.md — the model has to open the file to produce it. That is the strongest honest
11
+ // version of "force it to read": not guessable by accident, verifiable by exact string equality, and
12
+ // free. build/test-skill-gate.mjs asserts this constant against SKILL.md itself, so the manual and
13
+ // the gate can never drift apart (a phrase that lived in only one of the two would either lock the
14
+ // agent out forever or let it in without reading anything).
15
+ //
16
+ // Deliberately NOT in here: any way for the gate to block the safety direction. `computer_ask`, and
17
+ // `computer_ctrl {action:"stop"|"exit"}`, are decided by the caller and never gated — a gate that can
18
+ // block a brake is worse than no gate.
19
+
20
+ import { ACTUATION_TOOLS } from './policy.js'
21
+
22
+ export const REQUIRED_PHRASE = 'A MINIMIZED window is not an occluded window'
23
+
24
+ export const ACK_HINT =
25
+ 'Before the first DRIVING call of a session, read the computer-use skill ' +
26
+ '(skills/computer-use/SKILL.md) and acknowledge it once: ' +
27
+ 'computer_ctrl {action:"acknowledge", text:"' + REQUIRED_PHRASE + '"}. ' +
28
+ 'Observation tools are never gated, and neither is the safety direction (computer_ask, ' +
29
+ 'computer_ctrl stop/exit).'
30
+
31
+ const acked = new Set()
32
+
33
+ /** @returns {boolean} true when `text` is the exact phrase from the skill. */
34
+ export function acknowledge (text, key) {
35
+ if (String(text == null ? '' : text).trim() !== REQUIRED_PHRASE) return false
36
+ acked.add(String(key == null ? 'default' : key))
37
+ return true
38
+ }
39
+
40
+ export function isAcked (key) {
41
+ return acked.has(String(key == null ? 'default' : key))
42
+ }
43
+
44
+ /** Tests only. */
45
+ export function resetAcks () { acked.clear() }
46
+
47
+ /**
48
+ * Does this call DRIVE the machine — i.e. does it need the receipt?
49
+ * It mirrors policy.js's own classification instead of keeping a second list: a drifted list either
50
+ * locks the agent out of its own tools or lets it through without reading anything.
51
+ * Never driving: observation, `computer_ask` (the safety direction), and computer_ctrl
52
+ * stop/selftest/exit/indicator.
53
+ * Driving: everything in ACTUATION_TOOLS, a clipboard WRITE, calibrate (it moves the pointer) — and a
54
+ * BATCH only when something INSIDE it drives. Classifying the wrapper alone blocked batches built
55
+ * from pure observation steps: a false positive that buys no safety and costs a round trip. Recursion
56
+ * means a nested batch is judged by its innermost steps.
57
+ */
58
+ export function isDrivingCall (name, args) {
59
+ const a = args || {}
60
+ if (name === 'computer_ask') return false
61
+ if (name === 'computer_ctrl') return String(a.action || 'selftest') === 'calibrate'
62
+ if (name === 'computer_clip') return a.text !== undefined
63
+ if (name === 'computer_batch') {
64
+ const steps = Array.isArray(a.actions) ? a.actions : []
65
+ return steps.some((step) => step && isDrivingCall(String(step.tool || ''), step.args || {}))
66
+ }
67
+ return ACTUATION_TOOLS.has(name)
68
+ }