dsh-codex-style-computer-use 2.0.0 → 2.2.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/lib/core/index.js CHANGED
@@ -119,7 +119,11 @@ export class ComputerUse extends EventEmitter {
119
119
  // safety knobs (sandraschi SAFETY.md four-piece: HITL + kill switch +
120
120
  // rate limit + audit; see DESIGN §8)
121
121
  this.dryRun = opts.dryRun ?? false // simulate actuations, log only
122
- this.kickoffRequired = opts.kickoffRequired !== false // pure observation must not require focusing
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
126
+ this.onHostBlocked = typeof opts.onHostBlocked === 'function' ? opts.onHostBlocked : null
123
127
  this.rateLimit = opts.rateLimit ?? { max: 60, windowMs: 60_000 }
124
128
  this.rateWindow = []
125
129
  this.humanApproved = opts.autoApprove ?? false // gate for mutating ops
@@ -171,7 +175,8 @@ export class ComputerUse extends EventEmitter {
171
175
  this.spawnRuntime = nativeRuntimeIdentity(this.workerExe)
172
176
  const env = { ...process.env, DSH_CU_HOST_PID: String(process.pid), DSH_CU_HOST_EXE: process.execPath || '',
173
177
  DSH_CU_PACKAGE_VERSION: this.spawnRuntime.packageVersion, DSH_CU_BUILD_ID: this.spawnRuntime.buildId,
174
- DSH_CU_SOURCE_HASH: this.spawnRuntime.sourceHash || '', DSH_CU_BINARY_HASH: this.spawnRuntime.binaryHash }
178
+ DSH_CU_SOURCE_HASH: this.spawnRuntime.sourceHash || '', DSH_CU_BINARY_HASH: this.spawnRuntime.binaryHash,
179
+ ...(this.hostDebugFile ? { DSH_CU_HOST_DEBUG_FILE: this.hostDebugFile } : {}) }
175
180
  this.log(`[core] host guard armed for pid ${process.pid} (${process.execPath})`)
176
181
  this.proc = spawn(this.workerExe, [], { env, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })
177
182
  // A CHILD THAT FAILS TO START MUST NAME ITSELF (2026-09-13). Every computer_* tool died with a
@@ -322,6 +327,8 @@ export class ComputerUse extends EventEmitter {
322
327
  msg.outcome === 'not-dispatched' ? 'not-dispatched' : 'unknown')
323
328
  if (msg.glowHealth) error.glowHealth = msg.glowHealth
324
329
  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 { } }
325
332
  p.reject(error.code === 'HUMAN_REOBSERVE' ? humanReobserveError(error) : error)
326
333
  }
327
334
  }
@@ -400,7 +407,17 @@ export class ComputerUse extends EventEmitter {
400
407
  })
401
408
  // __cycle is signed last. A caller cannot forge another input series or turn.
402
409
  const signed = { ...args, __cycle: context?.internalCleanup !== true, __kickoff: this.kickoffRequired }
403
- if (input) signed.__inputSeries = context?.inputSeries || randomUUID()
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
+ }
404
421
  return new Promise((resolve, reject) => {
405
422
  let written = false
406
423
  const read = UIA_READS.has(op) && killOnTimeout ? (this.uiaRead = { id, child, timedOut: false }) : null
@@ -719,6 +736,11 @@ export class ComputerUse extends EventEmitter {
719
736
  x: pt.x, y: pt.y, button: opts.button || 'left', clicks: opts.clicks || 1,
720
737
  settleMs: opts.settleMs ?? 40,
721
738
  ...(witness ? { verificationRegion: witness } : {}),
739
+ // Embedded renderers (a WeChat article panel, an Electron child) belong to a DIFFERENT
740
+ // process than the window that hosts them, so the occlusion check reads them as "covered"
741
+ // even though the pixel is inside the app the caller activated. The worker already
742
+ // implements the escape hatch; forward it, or a whole class of real apps is unclickable.
743
+ ...(opts.allowCovered ? { allowCovered: true } : {}),
722
744
  ...((pt.expectHwnd || opts.expectHwnd) ? { expectHwnd: pt.expectHwnd || opts.expectHwnd } : {}),
723
745
  })
724
746
  }
@@ -1031,6 +1053,32 @@ export class ComputerUse extends EventEmitter {
1031
1053
  async ocr (region) { return this.call('ocr', region ? { ...region } : {}) }
1032
1054
  async clipWrite (text) { return this.actuate('clipWrite', { text }) }
1033
1055
 
1056
+ /** Open an app/URL/folder/document through ShellExecute, as a real actuation: the halo,
1057
+ * the brake checks, the audit trail and the rate window all come from actuate() — which
1058
+ * is exactly why this is an op and not a Node-side spawn (see worker.cs Launch). */
1059
+ async launch (target, opts = {}) {
1060
+ // `args` is an ARRAY of argument values (quoted once, in the worker). Passing a ready-made
1061
+ // command line would make the launcher parse our boundaries differently than the caller meant.
1062
+ return this.actuate('launch', {
1063
+ target,
1064
+ ...(opts.args !== undefined ? { args: opts.args } : {}),
1065
+ ...(opts.tailMs !== undefined ? { tailMs: opts.tailMs } : {}),
1066
+ })
1067
+ }
1068
+
1069
+ /** Put keyboard focus on a FIELD and prove it: clicks (x,y), then reports the UIA focused
1070
+ * element and whether a caret is provable there (OS caret, else a two-frame blink check).
1071
+ * Refuse to type when `verified` is false — typing follows the focused control, not the
1072
+ * window, and a document that kept focus silently swallows the text. */
1073
+ async focusField (opts = {}) {
1074
+ return this.actuate('focusField', {
1075
+ x: opts.x,
1076
+ y: opts.y,
1077
+ ...(opts.hwnd !== undefined ? { hwnd: opts.hwnd } : {}),
1078
+ ...(opts.verifyCaret !== undefined ? { verifyCaret: opts.verifyCaret } : {}),
1079
+ })
1080
+ }
1081
+
1034
1082
  async activateWindow (sel) { return this.actuate('activate', sel) }
1035
1083
  async windowOp (sel, op, opts = {}) { return this.actuate('windowOp', { ...sel, op, ...opts }) }
1036
1084
  /** Deterministic placement (no mouse): move/resize a window to an arbitrary rect. */