dsh-codex-style-computer-use 2.2.3 → 2.2.4

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.md CHANGED
@@ -192,6 +192,14 @@ fields are detected through UI Automation's `IsPassword` and refused by default.
192
192
  further actuation is refused until a later turn opens a new one.
193
193
  - **The brake is a fact about the machine, not one process** — the engaged state is persisted, and
194
194
  every worker adopts it, including one started later from a shell.
195
+ - **The handback wait is bounded** — if a handoff makes no progress for 60 s, `waitForHuman` ends the
196
+ wait with `HANDOFF_WAIT_TIMEOUT` and tells the agent to re-observe (retryable, no input replay).
197
+ Every real input event re-arms that quiet timer, so a human who keeps using the machine is never
198
+ cut off; a `ping` recovery probe runs every 5 s to recover a lost `handoff:idle` event. A genuinely
199
+ held key/button is different: it gets five windows (about 5 minutes by default) and then ends with
200
+ `HANDOFF_HELD_TIMEOUT`, so a stale `heldCount` cannot wait out the cooperative 3,600,000 ms
201
+ deadline. A host approval card happens before `waitForHuman` and is bounded by the host's own
202
+ approval wait, not by this local limit.
195
203
  - **Audit** — every actuation is appended to `$DSH_HOME/data/computer-use/audit.jsonl` with argument
196
204
  sanitisation.
197
205
  - **Kill switch for the plugin itself** — `DSH_COMPUTER_USE_KILL=1` makes the plugin refuse to start;
@@ -211,6 +219,12 @@ fields are detected through UI Automation's `IsPassword` and refused by default.
211
219
  | `workerExe` | `""` | explicit worker path; empty = autodiscover or compile |
212
220
  | `snapshotDir` | `""` | screenshot directory; empty = `$DSH_HOME/data/computer-use/shots` |
213
221
 
222
+ **When the desktop host does not render the plugin settings card:** set `config` directly on the
223
+ `- id: computer-use` row in the target profile's `cordis.patch.yml` (for example
224
+ `automationMode: autonomous`, `dryRun: false`), preserving existing keys; restart the host. If a CLI
225
+ install did not reconcile into `dsh.profile.bundles`, the same `- insert:` row can mount it manually
226
+ (issue #3).
227
+
214
228
  ## MCP server
215
229
 
216
230
  `mcp/` is a zero-dependency MCP stdio server over the same core, for clients that speak MCP rather
package/README.zh.md CHANGED
@@ -183,6 +183,7 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
183
183
  - **人工暂停** —— 只有实体 `Ctrl+Esc` 触发持续输入刹车,`Ctrl+Alt+R` 恢复。普通 Esc、打字、滚动、鼠标距离/频率和屏幕左上角不再触发这种暂停;显式提问、诊断暂停保持独立。
184
184
  - **退出** —— `Ctrl+Alt+Q` 结束会话:覆盖层消失、监听全部拆除,后续注入被拒,直到之后某一轮重新开启。
185
185
  - **刹车是关于机器的事实,不属于某一个进程** —— 闩锁状态落盘,每个 worker 都会遵守,包括之后从命令行启动的。
186
+ - **等待有上限** —— 人机交接若静默 60 秒仍没有任何进展,`waitForHuman` 会以 `HANDOFF_WAIT_TIMEOUT` 结束本次等待并提示重新观察当前目标(可重试,不会重放输入)。每次真实输入事件都会重置这个静默计时,所以人持续使用电脑不会被误判;期间每 5 秒还会用 `ping` 探测一次,以恢复丢失的 `handoff:idle` 事件。若物理键/按钮仍被按住,普通的 3 秒静默本来就不应结束等待,因此按住状态按 5 个静默窗口(默认约 5 分钟)封顶,陈旧 `heldCount` 会以 `HANDOFF_HELD_TIMEOUT` 结束,不会一直等到协作工具 3,600,000 ms。宿主审批卡发生在 `waitForHuman` 之前,由宿主自己的审批等待/截止时间管理,不在本上限内。
186
187
  - **审计** —— 每次操作连同脱敏后的参数追加到 `$DSH_HOME/data/computer-use/audit.jsonl`。
187
188
  - **插件级开关** —— `DSH_COMPUTER_USE_KILL=1` 让插件拒绝启动;`dryRun: true` 只模拟并记录。
188
189
 
@@ -200,6 +201,11 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
200
201
  | `workerExe` | `""` | 显式指定 worker 路径;留空则自动发现或编译 |
201
202
  | `snapshotDir` | `""` | 截图目录;留空为 `$DSH_HOME/data/computer-use/shots` |
202
203
 
204
+ **桌面版设置卡不出现时:** 部分桌面宿主不渲染插件的设置卡。此时可直接在目标 profile 的
205
+ `cordis.patch.yml` 中给 `- id: computer-use` 行写 `config`,例如
206
+ `automationMode: autonomous`、`dryRun: false`,保留已有键、不要整段覆盖;改完重启宿主。
207
+ CLI 安装若未自动进入 `dsh.profile.bundles`,也可用同一条 `- insert:` 行手工挂载(issue #3)。
208
+
203
209
  ## MCP 服务
204
210
 
205
211
  `mcp/` 是建立在同一核心之上的零依赖 MCP stdio 服务,供只讲 MCP、不讲 Cordis 的客户端使用:
@@ -1,6 +1,6 @@
1
1
  // Temporary ownership handoff is a task checkpoint, not a failed/cancelled task.
2
2
  export function handoffResult (error) {
3
- if (!['HUMAN_REOBSERVE', 'WORKER_REOBSERVE'].includes(error?.code) || !error.continuation?.required) return null
3
+ if (!['HUMAN_REOBSERVE', 'WORKER_REOBSERVE', 'HANDOFF_WAIT_TIMEOUT', 'HANDOFF_HELD_TIMEOUT'].includes(error?.code) || !error.continuation?.required) return null
4
4
  return {
5
5
  ok: false,
6
6
  status: 'continuation-required',
package/lib/core/index.js CHANGED
@@ -104,6 +104,8 @@ export class ComputerUse extends EventEmitter {
104
104
  * @param {object} [opts]
105
105
  * @param {string} [opts.workerExe] explicit worker path; discovered otherwise
106
106
  * @param {(msg: string) => void} [opts.log]
107
+ * @param {number} [opts.handoffWaitLimitMs] inactivity deadline for a handoff (default 60000)
108
+ * @param {number} [opts.handoffRecoveryMs] interval for the lost-idle ping probe (default 5000)
107
109
  */
108
110
  constructor (opts = {}) {
109
111
  super()
@@ -119,10 +121,10 @@ export class ComputerUse extends EventEmitter {
119
121
  // safety knobs (sandraschi SAFETY.md four-piece: HITL + kill switch +
120
122
  // rate limit + audit; see DESIGN §8)
121
123
  this.dryRun = opts.dryRun ?? false // simulate actuations, log only
122
- this.kickoffRequired = opts.kickoffRequired !== false // pure observation must not require focusing
123
- // ---- host-driving debug gates (lib/host-debug.js): two independent switches, both fail closed
124
- this.hostDebugFile = typeof opts.hostDebugFile === 'string' ? opts.hostDebugFile : ''
125
- this.hostGate = typeof opts.hostGate === 'function' ? opts.hostGate : null
124
+ this.kickoffRequired = opts.kickoffRequired !== false // pure observation must not require focusing
125
+ // ---- host-driving debug gates (lib/host-debug.js): two independent switches, both fail closed
126
+ this.hostDebugFile = typeof opts.hostDebugFile === 'string' ? opts.hostDebugFile : ''
127
+ this.hostGate = typeof opts.hostGate === 'function' ? opts.hostGate : null
126
128
  this.onHostBlocked = typeof opts.onHostBlocked === 'function' ? opts.onHostBlocked : null
127
129
  this.rateLimit = opts.rateLimit ?? { max: 60, windowMs: 60_000 }
128
130
  this.rateWindow = []
@@ -136,6 +138,14 @@ export class ComputerUse extends EventEmitter {
136
138
  this.runtime = null
137
139
  this.inputCleanupGraceMs = INPUT_CLEANUP_GRACE_MS
138
140
  this.shutdownGraceMs = INPUT_CLEANUP_GRACE_MS
141
+ // A handoff must be able to end even when a worker event is lost or the native
142
+ // poll thread is blocked. The deadline is measured from the LAST active handoff
143
+ // event (not from the start of the wait), so a human who keeps using the machine
144
+ // keeps the wait alive; only a silent, non-progressing handoff expires.
145
+ this.handoffWaitLimitMs = Number.isFinite(opts.handoffWaitLimitMs) && opts.handoffWaitLimitMs >= 50
146
+ ? opts.handoffWaitLimitMs : 60_000
147
+ this.handoffRecoveryMs = Number.isFinite(opts.handoffRecoveryMs) && opts.handoffRecoveryMs >= 10
148
+ ? opts.handoffRecoveryMs : 5_000
139
149
  this.activeActions = new Map()
140
150
  this.inputContext = new AsyncLocalStorage()
141
151
  // Monotonic counter of UNSOLICITED state transitions (panic/ask/resume/exit). Anything that
@@ -175,7 +185,7 @@ export class ComputerUse extends EventEmitter {
175
185
  this.spawnRuntime = nativeRuntimeIdentity(this.workerExe)
176
186
  const env = { ...process.env, DSH_CU_HOST_PID: String(process.pid), DSH_CU_HOST_EXE: process.execPath || '',
177
187
  DSH_CU_PACKAGE_VERSION: this.spawnRuntime.packageVersion, DSH_CU_BUILD_ID: this.spawnRuntime.buildId,
178
- DSH_CU_SOURCE_HASH: this.spawnRuntime.sourceHash || '', DSH_CU_BINARY_HASH: this.spawnRuntime.binaryHash,
188
+ DSH_CU_SOURCE_HASH: this.spawnRuntime.sourceHash || '', DSH_CU_BINARY_HASH: this.spawnRuntime.binaryHash,
179
189
  ...(this.hostDebugFile ? { DSH_CU_HOST_DEBUG_FILE: this.hostDebugFile } : {}) }
180
190
  this.log(`[core] host guard armed for pid ${process.pid} (${process.execPath})`)
181
191
  this.proc = spawn(this.workerExe, [], { env, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })
@@ -327,8 +337,8 @@ export class ComputerUse extends EventEmitter {
327
337
  msg.outcome === 'not-dispatched' ? 'not-dispatched' : 'unknown')
328
338
  if (msg.glowHealth) error.glowHealth = msg.glowHealth
329
339
  for (const key of ['dragStarted', 'expectedCursor', 'actualCursor', 'cursorCheck']) if (msg[key] !== undefined) error[key] = msg[key]
330
- // A gate refusal is an ACTIONABLE state, not a crash: the settings page surfaces the last one.
331
- if (/HOST_DEBUG_GATE/.test(String(error.message || ''))) { try { this.onHostBlocked?.() } catch { } }
340
+ // A gate refusal is an ACTIONABLE state, not a crash: the settings page surfaces the last one.
341
+ if (/HOST_DEBUG_GATE/.test(String(error.message || ''))) { try { this.onHostBlocked?.() } catch { } }
332
342
  p.reject(error.code === 'HUMAN_REOBSERVE' ? humanReobserveError(error) : error)
333
343
  }
334
344
  }
@@ -407,16 +417,16 @@ export class ComputerUse extends EventEmitter {
407
417
  })
408
418
  // __cycle is signed last. A caller cannot forge another input series or turn.
409
419
  const signed = { ...args, __cycle: context?.internalCleanup !== true, __kickoff: this.kickoffRequired }
410
- if (input) signed.__inputSeries = context?.inputSeries || randomUUID()
411
- // The two host-driving gates travel with EVERY call so the worker can decide per request:
412
- // gate 1 = the calling conversation is full permission, gate 2 = debug mode was switched on by
413
- // hand. Both unknown/absent => false (fail closed). Tools cannot forge these: they are stamped
414
- // here, after the caller's args, exactly like __cycle.
415
- if (this.hostGate) {
416
- let gate = null
417
- try { gate = this.hostGate(context) } catch { gate = null }
418
- signed.__hostDebug = !!(gate && gate.debug === true)
419
- signed.__hostFull = !!(gate && gate.full === true)
420
+ if (input) signed.__inputSeries = context?.inputSeries || randomUUID()
421
+ // The two host-driving gates travel with EVERY call so the worker can decide per request:
422
+ // gate 1 = the calling conversation is full permission, gate 2 = debug mode was switched on by
423
+ // hand. Both unknown/absent => false (fail closed). Tools cannot forge these: they are stamped
424
+ // here, after the caller's args, exactly like __cycle.
425
+ if (this.hostGate) {
426
+ let gate = null
427
+ try { gate = this.hostGate(context) } catch { gate = null }
428
+ signed.__hostDebug = !!(gate && gate.debug === true)
429
+ signed.__hostFull = !!(gate && gate.full === true)
420
430
  }
421
431
  return new Promise((resolve, reject) => {
422
432
  let written = false
@@ -586,14 +596,23 @@ export class ComputerUse extends EventEmitter {
586
596
  }
587
597
  return new Promise((resolve, reject) => {
588
598
  let finished = false
599
+ let limitTimer = null
600
+ let recoveryTimer = null
601
+ const limitMs = this.handoffWaitLimitMs
602
+ const recoveryMs = this.handoffRecoveryMs
589
603
  const events = ['handoff', 'panic', 'ask', 'exit', 'dead', 'input-cancel']
590
- const cleanup = () => { for (const event of events) this.off(event, listeners[event]); signal?.removeEventListener('abort', cancelled) }
604
+ const cleanup = () => {
605
+ for (const event of events) this.off(event, listeners[event])
606
+ signal?.removeEventListener('abort', cancelled)
607
+ clearTimeout(limitTimer)
608
+ clearTimeout(recoveryTimer)
609
+ }
591
610
  const finish = (error) => {
592
611
  if (finished) return
593
612
  finished = true
594
613
  cleanup()
595
614
  const record = report(error ? 'ended-with-error' : 'handback', error ? { code: error.code, cancelSource: error.cancelSource || null } : {})
596
- if (error) { error.handoffWait = record; reject(error) } else resolve()
615
+ if (error) { if (!error.handoffWait) error.handoffWait = record; reject(error) } else resolve()
597
616
  }
598
617
  const cancelled = () => {
599
618
  const source = signal?.aborted ? 'host-signal' : 'core-input-cancel'
@@ -604,10 +623,78 @@ export class ComputerUse extends EventEmitter {
604
623
  error.cancelSource = source
605
624
  finish(error)
606
625
  }
626
+ // A silent, non-progressing handoff must not consume the whole cooperative tool
627
+ // deadline (3,600,000 ms for cooperative tools). Every ACTIVE handoff event
628
+ // re-arms this timer, so a human who keeps using the machine is never cut off;
629
+ // only a lost/stuck idle event expires.
630
+ let heldExpiries = 0
631
+ const timeoutError = (code, reason, message) => {
632
+ const snapshot = state()
633
+ const error = workerCallError(message, code, null, null, 'not-dispatched')
634
+ error.transient = true
635
+ error.taskStatus = 'continuing'
636
+ error.continuation = {
637
+ required: true,
638
+ taskStatus: 'continuing',
639
+ reason,
640
+ nextAction: 'observe-current-target-then-continue-original-task',
641
+ replayInterruptedInput: false,
642
+ outcome: 'not-dispatched',
643
+ instruction: 'The handoff wait made no progress and was closed locally. Obtain fresh target evidence, inspect any partial effect, and continue the original authorized task; never replay a partial write.',
644
+ }
645
+ error.handoffWait = report('timeout', { code, limitMs, cooperation: snapshot })
646
+ finish(error)
647
+ }
648
+ const expire = () => {
649
+ if (finished) return
650
+ // A physically held key/button is not "no progress" — the human is still
651
+ // using the machine, and the worker intentionally waits for release + 3 s.
652
+ // A genuinely held button gets a 5x longer cap so a STALE held counter
653
+ // (lost key-up / blocked poll thread) cannot hold the wait for the whole
654
+ // 3,600,000 ms cooperative deadline.
655
+ if (Number(this.cooperation.heldCount) > 0) {
656
+ if (++heldExpiries >= 5) {
657
+ timeoutError('HANDOFF_HELD_TIMEOUT', 'handoff-held-timeout',
658
+ `worker still reports heldCount=${this.cooperation.heldCount} after ${heldExpiries} inactivity windows (${limitMs}ms each); inspect the held key/button and re-observe before continuing.`)
659
+ return
660
+ }
661
+ armLimit()
662
+ return
663
+ }
664
+ heldExpiries = 0
665
+ timeoutError('HANDOFF_WAIT_TIMEOUT', 'handoff-wait-timeout',
666
+ `waiting for the human made no progress for ${limitMs}ms; worker reports phase=${state().phase}, heldCount=0, quietRemainingMs=${state().quietRemainingMs ?? 'unknown'}. Re-observe the current target and continue the authorized task; never replay a partial write.`)
667
+ }
668
+ const armLimit = () => {
669
+ if (finished) return
670
+ clearTimeout(limitTimer)
671
+ limitTimer = setTimeout(expire, limitMs)
672
+ }
673
+ // A worker that lost the final idle event can still answer `ping`. This is a
674
+ // recovery probe, not a substitute for the event path: it runs only after
675
+ // recoveryMs and never re-arms the inactivity limit without fresh input evidence.
676
+ const recover = async () => {
677
+ if (finished) return
678
+ let fresh = false
679
+ try {
680
+ const data = await this.call('ping', {}, 4000)
681
+ if (finished) return
682
+ if (data?.cooperation) {
683
+ const beforeEpoch = this.cooperation.inputEpoch
684
+ const accepted = this.syncCooperation(data.cooperation, false)
685
+ if (accepted && !this.cooperation.active) { finish(); return }
686
+ fresh = accepted && Number.isFinite(data.cooperation.inputEpoch) && data.cooperation.inputEpoch !== beforeEpoch
687
+ }
688
+ } catch { /* a busy worker is not evidence either way */ }
689
+ if (finished) return
690
+ if (fresh) armLimit()
691
+ recoveryTimer = setTimeout(recover, recoveryMs)
692
+ }
607
693
  const check = () => {
608
694
  if (signal?.aborted) return cancelled()
609
695
  if (this.stopped || this.exited) return finish(workerCallError('manual pause/exit remains authoritative', 'ABORTED', null, null, 'not-dispatched'))
610
- if (!this.cooperation.active) finish()
696
+ if (!this.cooperation.active) return finish()
697
+ armLimit()
611
698
  }
612
699
  const listeners = Object.fromEntries(events.map(event => [event, event === 'dead' ?
613
700
  () => finish(workerCallError('worker exited while waiting', 'worker-exited', null, null, 'not-dispatched')) :
@@ -616,6 +703,10 @@ export class ComputerUse extends EventEmitter {
616
703
  signal?.addEventListener('abort', cancelled, { once: true })
617
704
  report('started')
618
705
  check()
706
+ if (!finished) {
707
+ armLimit()
708
+ recoveryTimer = setTimeout(recover, recoveryMs)
709
+ }
619
710
  })
620
711
  }
621
712
 
@@ -1149,11 +1240,22 @@ export class ComputerUse extends EventEmitter {
1149
1240
  this.inputContext.getStore()?.assertOwnership?.()
1150
1241
  err = humanReobserveError({ ...err, op })
1151
1242
  } catch (waitError) {
1152
- err = waitError.code === 'WORKER_REOBSERVE' ? waitError : Object.assign(workerCallError(waitError.detail || waitError.message || String(waitError), waitError.code || 'handoff-wait-failed',
1153
- op, err.requestId ?? null, err.outcome === 'not-dispatched' ? 'not-dispatched' : 'unknown'),
1154
- { ...(waitError.handoffWait ? { handoffWait: waitError.handoffWait } : {}),
1155
- ...(waitError.cancelSource ? { cancelSource: waitError.cancelSource } : {}),
1156
- ...(waitError.cleanup ? { cleanup: waitError.cleanup } : {}) })
1243
+ // A bounded-handoff timeout is already a continuation-required checkpoint.
1244
+ // Rebuilding it with workerCallError() would drop continuation/transient/
1245
+ // taskStatus and turn the tool/MCP/batch result into a hard error.
1246
+ if (waitError?.continuation?.required) {
1247
+ err = Object.assign(waitError, {
1248
+ op: waitError.op ?? op,
1249
+ requestId: err.requestId ?? waitError.requestId ?? null,
1250
+ outcome: err.outcome === 'not-dispatched' ? 'not-dispatched' : (waitError.outcome || 'unknown'),
1251
+ })
1252
+ } else {
1253
+ err = waitError.code === 'WORKER_REOBSERVE' ? waitError : Object.assign(workerCallError(waitError.detail || waitError.message || String(waitError), waitError.code || 'handoff-wait-failed',
1254
+ op, err.requestId ?? null, err.outcome === 'not-dispatched' ? 'not-dispatched' : 'unknown'),
1255
+ { ...(waitError.handoffWait ? { handoffWait: waitError.handoffWait } : {}),
1256
+ ...(waitError.cancelSource ? { cancelSource: waitError.cancelSource } : {}),
1257
+ ...(waitError.cleanup ? { cleanup: waitError.cleanup } : {}) })
1258
+ }
1157
1259
  }
1158
1260
  }
1159
1261
  if (err.code === 'HUMAN_REOBSERVE' || err.code === 'WORKER_REOBSERVE') {
package/lib/index.js CHANGED
@@ -543,8 +543,8 @@ export function apply (ctx, config) {
543
543
  try { return { debug: readHostDebug().enabled, full: sessionPermission(gateSessionId(context)).fullAccess } } catch { return null }
544
544
  }
545
545
  : null,
546
- onHostBlocked: typeof noteHostBlocked === 'function'
547
- ? () => { try { noteHostBlocked() } catch { } notifyHostGate(log) }
546
+ onHostBlocked: typeof noteHostBlocked === 'function'
547
+ ? () => { try { noteHostBlocked() } catch { } notifyHostGate(log) }
548
548
  : null,
549
549
  })
550
550
  const detachAudit = installAudit(cu, dataDir)
package/package.json CHANGED
@@ -1,101 +1,101 @@
1
- {
2
- "name": "dsh-codex-style-computer-use",
3
- "version": "2.2.3",
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
- "type": "module",
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
- },
11
- "exports": {
12
- ".": "./lib/index.js",
13
- "./package.json": "./package.json",
14
- "./cordis.patch.yml": "./cordis.patch.yml"
15
- },
16
- "files": [
17
- "icon.svg",
18
- "lib",
19
- "mcp",
20
- "skills",
21
- "cordis.patch.yml",
22
- "README.md",
23
- "README.zh.md",
24
- "LICENSE",
25
- "scripts",
26
- "client.js"
27
- ],
28
- "dsh": {
29
- "bundle": {
30
- "patch": "./cordis.patch.yml"
31
- },
32
- "client": {
33
- "platform": "web",
34
- "immediately": false,
35
- "inject": []
36
- }
37
- },
38
- "peerDependencies": {
39
- "@deepseek-ai/cordis": "^4.0.0",
40
- "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.2.0-rc.1",
41
- "@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.2.0-rc.1",
42
- "@deepseek-ai/schemastery": "^3.0.0",
43
- "@deepseek-ai/dsh-scope": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.2.0-rc.1"
44
- },
45
- "peerDependenciesMeta": {
46
- "@deepseek-ai/cordis": {
47
- "optional": true
48
- },
49
- "@deepseek-ai/dsh-tools": {
50
- "optional": true
51
- },
52
- "@deepseek-ai/dsh-llm": {
53
- "optional": true
54
- },
55
- "@deepseek-ai/schemastery": {
56
- "optional": true
57
- },
58
- "@deepseek-ai/dsh-scope": {
59
- "optional": true
60
- }
61
- },
62
- "engines": {
63
- "node": ">=22"
64
- },
65
- "os": [
66
- "win32"
67
- ],
68
- "license": "MIT",
69
- "repository": {
70
- "type": "git",
71
- "url": "git+https://github.com/ltmroberthk915/dsh-computer-use.git"
72
- },
73
- "keywords": [
74
- "deepseek-harness",
75
- "dsh",
76
- "dsh-plugin",
77
- "computer-use",
78
- "windows",
79
- "gui-automation",
80
- "set-of-marks",
81
- "uia",
82
- "mcp",
83
- "codex-style",
84
- "desktop-automation",
85
- "dsh-market"
86
- ],
87
- "publishConfig": {
88
- "access": "public",
89
- "registry": "https://registry.npmjs.org/"
90
- },
91
- "scripts": {
92
- "test": "node scripts/check-behavior.mjs",
93
- "build:native": "node scripts/build-native.mjs",
94
- "verify:package": "node scripts/verify-package.mjs"
95
- },
96
- "homepage": "https://github.com/ltmroberthk915/dsh-computer-use#readme",
97
- "bugs": {
98
- "url": "https://github.com/ltmroberthk915/dsh-computer-use/issues"
99
- },
100
- "icon": "./icon.svg"
1
+ {
2
+ "name": "dsh-codex-style-computer-use",
3
+ "version": "2.2.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
+ "type": "module",
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
+ },
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
+ "./package.json": "./package.json",
14
+ "./cordis.patch.yml": "./cordis.patch.yml"
15
+ },
16
+ "files": [
17
+ "icon.svg",
18
+ "lib",
19
+ "mcp",
20
+ "skills",
21
+ "cordis.patch.yml",
22
+ "README.md",
23
+ "README.zh.md",
24
+ "LICENSE",
25
+ "scripts",
26
+ "client.js"
27
+ ],
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ },
32
+ "client": {
33
+ "platform": "web",
34
+ "immediately": false,
35
+ "inject": []
36
+ }
37
+ },
38
+ "peerDependencies": {
39
+ "@deepseek-ai/cordis": "^4.0.0",
40
+ "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.2.0-rc.1",
41
+ "@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.2.0-rc.1",
42
+ "@deepseek-ai/schemastery": "^3.0.0",
43
+ "@deepseek-ai/dsh-scope": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.2.0-rc.1"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "@deepseek-ai/cordis": {
47
+ "optional": true
48
+ },
49
+ "@deepseek-ai/dsh-tools": {
50
+ "optional": true
51
+ },
52
+ "@deepseek-ai/dsh-llm": {
53
+ "optional": true
54
+ },
55
+ "@deepseek-ai/schemastery": {
56
+ "optional": true
57
+ },
58
+ "@deepseek-ai/dsh-scope": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "engines": {
63
+ "node": ">=22"
64
+ },
65
+ "os": [
66
+ "win32"
67
+ ],
68
+ "license": "MIT",
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/ltmroberthk915/dsh-computer-use.git"
72
+ },
73
+ "keywords": [
74
+ "deepseek-harness",
75
+ "dsh",
76
+ "dsh-plugin",
77
+ "computer-use",
78
+ "windows",
79
+ "gui-automation",
80
+ "set-of-marks",
81
+ "uia",
82
+ "mcp",
83
+ "codex-style",
84
+ "desktop-automation",
85
+ "dsh-market"
86
+ ],
87
+ "publishConfig": {
88
+ "access": "public",
89
+ "registry": "https://registry.npmjs.org/"
90
+ },
91
+ "scripts": {
92
+ "test": "node scripts/check-behavior.mjs",
93
+ "build:native": "node scripts/build-native.mjs",
94
+ "verify:package": "node scripts/verify-package.mjs"
95
+ },
96
+ "homepage": "https://github.com/ltmroberthk915/dsh-computer-use#readme",
97
+ "bugs": {
98
+ "url": "https://github.com/ltmroberthk915/dsh-computer-use/issues"
99
+ },
100
+ "icon": "./icon.svg"
101
101
  }
@@ -36,7 +36,7 @@ const reviewed = new Set([
36
36
  'coactive', 'compact-feedback', 'control-targets', 'core-system-recovery', 'cursor', 'cycle-completion',
37
37
  'cycle-no-timeout', 'cycle-open', 'desktop-acceptance-io', 'desktop-ownership', 'drag-options', 'evidence-protocol',
38
38
  'exit-coherence', 'feedback-cache-integration', 'feedback-policy', 'glow-pump', 'glow-visual', 'handoff-continuation',
39
- 'handoff-lock', 'host-gate', 'human-handoff', 'indicator-hold', 'input-stamp', 'key-command',
39
+ 'handoff-lock', 'handoff-runtime', 'handoff-watchdog', 'host-gate', 'human-handoff', 'indicator-hold', 'input-stamp', 'key-command',
40
40
  'kickoff-focus', 'mcp-contracts', 'mcp-execution', 'modifier-vk', 'monitor-lifetime', 'native-hardening', 'native-respawn',
41
41
  'native-runtime', 'native-fault-lifecycle', 'observation-archive', 'observations', 'op-classification', 'pointer-lease', 'policy', 'preparation',
42
42
  'release-channel', 'renders', 'root-integration', 'session-identity', 'skill-gate', 'steer-message-shape',
@@ -90,7 +90,7 @@ Before a new input call or coherent batch, a purple notice says "即将开始操
90
90
 
91
91
  During actuation or a retained hold, physical typing/clicks/scrolling yield control immediately; pointer movement has a 16-screen-pixel tolerance so jitter does not interrupt. Free-state input only invalidates old evidence and lights no takeover cue. After yielding, input continuing 2 seconds enters automatic waiting, and a held key/button is still active input; control returns only after all physical keys/buttons are released and 3 seconds pass with no physical events. The pale-yellow gradient breathes gently while waiting. Own and other injected events are not physical input.
92
92
 
93
- The wait is local — no model polling, no question card. HUMAN_REOBSERVE returns a nonterminal checkpoint with taskStatus:"continuing" and continuation.required:true: a temporary interruption has ENDED, so you MUST immediately obtain fresh target evidence, check any partial effect, and continue the original authorized task until complete. Do not abandon it, send a final wrap-up, or ask permission to resume after a handoff. A partial write is never blindly replayed; a batch preserves completed steps and supplies resumeFromActionIndex — inspect that step before continuing and treat earlier coordinates/marks/focus as stale. Ctrl+Esc overrides automatic waiting and still needs Ctrl+Alt+R; Ctrl+Alt+Q ends the cycle. Cancelling a Host turn cancels its in-flight/queued input and wait without creating a persistent STOP.
93
+ The wait is local — no model polling, no question card. HUMAN_REOBSERVE returns a nonterminal checkpoint with taskStatus:"continuing" and continuation.required:true: a temporary interruption has ENDED, so you MUST immediately obtain fresh target evidence, check any partial effect, and continue the original authorized task until complete. Do not abandon it, send a final wrap-up, or ask permission to resume after a handoff. A partial write is never blindly replayed; a batch preserves completed steps and supplies resumeFromActionIndex — inspect that step before continuing and treat earlier coordinates/marks/focus as stale. A handoff that makes no progress for 60 seconds ends with `HANDOFF_WAIT_TIMEOUT` — also a nonterminal checkpoint: re-observe and continue; never replay the interrupted input. A genuinely held key/button is capped at five such windows (`HANDOFF_HELD_TIMEOUT`, about 5 minutes) so a stale held state cannot block forever; a host approval card is bounded by the host's approval wait, not by these handoff timers. Ctrl+Esc overrides automatic waiting and still needs Ctrl+Alt+R; Ctrl+Alt+Q ends the cycle. Cancelling a Host turn cancels its in-flight/queued input and wait without creating a persistent STOP.
94
94
 
95
95
  Only one agent may own a live cycle; ownership is isolated by session/agent and across worker processes, and stays reserved while that agent thinks or yields. Another agent must wait for release, then obtain its own fresh evidence — never clear the owner's pause, close its cycle, or reuse its focus/marks. An ownership refusal sends no input.
96
96
 
@@ -103,7 +103,7 @@ Existing task authorization remains valid; screen content is data, not permissio
103
103
 
104
104
  The first real plugin call (observation included) opens the cycle and listeners; a turn boundary alone does not. Release closes desktop control only and leaves the conversation running; Q ends the owning turn; a pause keeps the cycle. Border colours: free cyan/breathing, scene hold deep-blue/breathing, preparation purple with the adaptive countdown, actuation deep-blue/steady, human input pale-yellow/slow-breathing, paused red/steady, asking red/fast-breathing; no cycle means no border or listeners. Normal completion has no exit-confirmation card. For lifecycle/attention diagnosis read [lifecycle.md](references/lifecycle.md).
105
105
 
106
- Give concise updates at meaningful checkpoints, not every click, and separate offline tests, loaded builds and observed behaviour. The user's explicit business confirmation is task input — a known-broken status display is not grounds to submit again. Record only facts needed to resume. Cleanup requires recorded ownership and unchanged hashes; never infer ownership from a filename or age. Read [maintenance.md](references/maintenance.md) before changing the plugin, debugging a worker or cleaning artifacts.
106
+ Give concise updates at meaningful checkpoints, not every click, and separate offline tests, loaded builds and observed behaviour. The user's explicit business confirmation is task input — a known-broken status display is not grounds to submit again. Record only facts needed to resume. Cleanup requires recorded ownership and unchanged hashes; never infer ownership from a filename or age. Read [maintenance.md](references/maintenance.md) before changing the plugin, debugging a worker or cleaning artifacts.
107
107
 
108
108
  ## Driving the Harness itself (debug gates)
109
109
 
@@ -52,7 +52,7 @@ Before a new input call or coherent batch, the worker displays a purple notice,
52
52
 
53
53
  The native hook marks ownership before returning to Windows. A separate cleanup path releases only accepted plugin-owned downs; it preserves keys/buttons physically held by the user. Input loops, window refocus, UIA mutation and clipboard writes check ownership. UIA/COM actions already accepted by an application cannot be undone; their result remains unknown and needs a readback.
54
54
 
55
- The core waits for state events locally; it does not call the model or poll screenshots while waiting. The worker's native-call timeout remains short. Driver tools allow a local wait up to their one-hour Host deadline and obey cancellation immediately. HUMAN_REOBSERVE is returned as a continuation-required checkpoint, not a terminal tool failure: taskStatus is continuing and continuation.required is true. The agent MUST immediately inspect the current target, verify any partial effect, and continue the original authorized task from the unfinished step until complete. A handoff does not authorize a final wrap-up or require a new permission question. A batch retains successful receipts and its resumeFromActionIndex. No partial input is silently replayed, and explicit cancellation/manual pause/exit still takes precedence.
55
+ The core waits for state events locally; it does not call the model or poll screenshots while waiting. The worker's native-call timeout remains short. Driver tools allow a local wait up to their one-hour Host deadline and obey cancellation immediately. HUMAN_REOBSERVE is returned as a continuation-required checkpoint, not a terminal tool failure: taskStatus is continuing and continuation.required is true. A silent, non-progressing handoff is closed after 60 s as HANDOFF_WAIT_TIMEOUT; a stale held key/button is capped at five windows (HANDOFF_HELD_TIMEOUT, about 5 minutes). Both are continuation-required checkpoints. The agent MUST immediately inspect the current target, verify any partial effect, and continue the original authorized task from the unfinished step until complete. A handoff does not authorize a final wrap-up or require a new permission question. A batch retains successful receipts and its resumeFromActionIndex. No partial input is silently replayed, and explicit cancellation/manual pause/exit still takes precedence.
56
56
 
57
57
  The automatic handoff has a pale-yellow gradient with a 2.8-second smooth breathing period. It is click-through, does not activate a window, and has precedence over acting/thinking/capture cues; manual pause/question/exit states take precedence over it.
58
58