dsh-codex-style-computer-use 1.3.1 → 2.0.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.
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "provider": "bigmodel-anthropic",
4
+ "model": "glm-5.3",
5
+ "adapterVersion": "unversioned",
6
+ "grounding": "failed",
7
+ "testedAt": "2026-10-02T00:00:00Z",
8
+ "expiresAt": "2026-12-01T00:00:00Z",
9
+ "reason": "Historical Desktop grounding test failed; adapter version was not recorded. Conservative text fallback expires after 60 days and must be revalidated."
10
+ }
11
+ ]
package/lib/feedback.js CHANGED
@@ -1,5 +1,8 @@
1
- // Route-specific observations, verified against the Desktop adapters on 2026-10-02.
2
- // A model catalog's image flag is transport permission, not a grounding test.
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ // Transport capability and grounding are separate evidence. Route exceptions
4
+ // are dated data, can be replaced in settings, and expire.
5
+ export const DEFAULT_FEEDBACK_RULES = Object.freeze(JSON.parse(readFileSync(new URL('./feedback-routes.json', import.meta.url), 'utf8')))
3
6
  export function feedbackRoute (exec) {
4
7
  const routed = exec?.agent?.session?.requestHeader?.()?.config
5
8
  return {
@@ -8,23 +11,53 @@ export function feedbackRoute (exec) {
8
11
  }
9
12
  }
10
13
 
11
- export function unverifiedVisionRoute (exec) {
12
- const { provider, model } = feedbackRoute(exec)
13
- return provider === 'bigmodel-anthropic' && model === 'glm-5.3'
14
+ export function visionEvidence (exec, { rules = DEFAULT_FEEDBACK_RULES, adapterVersion = 'unversioned', now = Date.now() } = {}) {
15
+ const route = feedbackRoute(exec)
16
+ for (const rule of [...(Array.isArray(rules) ? rules : [])].reverse()) {
17
+ const tested = Date.parse(rule.testedAt), expires = Date.parse(rule.expiresAt)
18
+ if (rule.provider !== route.provider || rule.model !== route.model ||
19
+ rule.adapterVersion !== adapterVersion || !Number.isFinite(tested) || !Number.isFinite(expires) ||
20
+ tested > now || expires <= now || expires <= tested || !['failed', 'verified'].includes(rule.grounding)) continue
21
+ // Unversioned historical tests may select a conservative text fallback,
22
+ // but cannot certify the current adapter's visual grounding.
23
+ if (rule.grounding === 'verified' && adapterVersion === 'unversioned') continue
24
+ return { grounding: rule.grounding, adapterVersion, testedAt: rule.testedAt, expiresAt: rule.expiresAt,
25
+ reason: rule.reason || 'configured route evidence' }
26
+ }
27
+ return { grounding: 'unverified', adapterVersion, reason: 'no current matching grounding record' }
28
+ }
29
+
30
+ export function unverifiedVisionRoute (exec, options) {
31
+ return visionEvidence(exec, options).grounding === 'failed'
14
32
  }
15
33
 
16
- export async function feedbackMode (ctx, exec, enabled = true) {
17
- if (!enabled || exec?.signal?.aborted) return null
18
- if (unverifiedVisionRoute(exec)) return 'state'
19
- const { provider, model } = feedbackRoute(exec)
20
- const tested = (provider === 'bigmodel-anthropic' && model === 'glm-5.3-flash') ||
21
- (provider === 'deepseek-official' && model === 'deepseek-flash')
22
- if (!tested) return null
34
+ export function selectFeedbackDetail (requested, fallback = 'compact') {
35
+ const detail = requested ?? fallback
36
+ if (!['compact', 'full', 'none'].includes(detail)) throw Object.assign(new Error('feedback must be compact, full or none'),
37
+ { code: 'INVALID_FEEDBACK', outcome: 'not-dispatched' })
38
+ return detail
39
+ }
40
+
41
+ export async function feedbackPolicy (ctx, exec, enabled = true, options = {}) {
42
+ const route = feedbackRoute(exec)
43
+ const detail = selectFeedbackDetail(options.detail, 'full')
44
+ if (!enabled || detail === 'none' || exec?.signal?.aborted) return { kind: null, detail, reason: exec?.signal?.aborted ? 'cancelled' : !enabled ? 'disabled' : 'feedback-none' }
45
+ let active
23
46
  try {
24
- if (!ctx.get?.('attachments')?.saveImage) return null
25
- const active = await ctx.get?.('llm')?.resolveModelInfo?.(provider, model, exec?.signal)
26
- return !exec?.signal?.aborted && active?.inputModalities?.includes('image') ? 'image' : null
27
- } catch { return null }
47
+ if (route.provider && route.model) active = await ctx.get?.('llm')?.resolveModelInfo?.(route.provider, route.model, exec?.signal)
48
+ } catch { /* missing capability evidence selects text, not silence */ }
49
+ if (exec?.signal?.aborted) return { kind: null, reason: 'cancelled' }
50
+ const evidence = visionEvidence(exec, { ...options, adapterVersion: active?.adapterVersion || 'unversioned' })
51
+ const base = { route, detail, ...evidence }
52
+ if (evidence.grounding === 'failed') return { ...base, kind: 'state', reason: 'current grounding record requires UIA/text readback' }
53
+ if (!active) return { ...base, kind: 'state', reason: 'model-capabilities-unavailable' }
54
+ if (!active.inputModalities?.includes('image')) return { ...base, kind: 'state', reason: 'model-text-only' }
55
+ if (!ctx.get?.('attachments')?.saveImage) return { ...base, kind: 'state', reason: 'attachments-unavailable' }
56
+ return { ...base, kind: 'image', reason: 'image transport declared; inspect returned evidence to verify effect' }
57
+ }
58
+
59
+ export async function feedbackMode (ctx, exec, enabled = true, options) {
60
+ return (await feedbackPolicy(ctx, exec, enabled, options)).kind
28
61
  }
29
62
 
30
63
  export const feedbackActions = new Set([
@@ -1,10 +1,44 @@
1
1
  import { basename } from 'node:path'
2
- import { unverifiedVisionRoute } from './feedback.js'
2
+ import { visionEvidence } from './feedback.js'
3
+ import { captureCoordinates } from './media-contract.js'
4
+
5
+ export function screenshotMetadata (shot) {
6
+ const value = {}
7
+ for (const key of ['capturedAt', 'ts', 'imageSize', 'settled', 'settleMs', 'settleDiffPct', 'settleNote',
8
+ 'cursor', 'cursorRequested', 'cursorDrawn', 'cursorScreenPos', 'cursorInImage', 'cursorNote']) {
9
+ if (shot[key] !== undefined) value[key] = shot[key]
10
+ }
11
+ return value
12
+ }
13
+
14
+ function setCoordinates (value, size) {
15
+ const coordinates = captureCoordinates(value.region, size)
16
+ if (!coordinates) { delete value.coordinates; delete value.coordinateRule; return }
17
+ value.coordinates = coordinates
18
+ value.coordinateRule = 'screen = screenOrigin + imagePixel * screenPerImagePixel (per axis). Do not apply DPI or browser zoom again. After resize/move/zoom, obtain fresh evidence.'
19
+ }
20
+
21
+ const validImageSize = size => Array.isArray(size) && size.length === 2 && size.every(n => Number.isFinite(n) && n > 0)
22
+ const validPoint = point => Number.isFinite(point?.x) && Number.isFinite(point?.y)
23
+
24
+ function restoreFileGeometry (value) {
25
+ if (!value.sourceImageSize) return
26
+ value.imageSize = value.sourceImageSize
27
+ if (value.sourceCursorInImage) value.cursorInImage = value.sourceCursorInImage
28
+ else delete value.cursorInImage
29
+ delete value.sourceImageSize
30
+ delete value.sourceCursorInImage
31
+ setCoordinates(value, value.imageSize)
32
+ }
3
33
 
4
34
  // DSH image blocks reference the host's durable attachment store. Paths and raw
5
35
  // base64 are not image blocks. Keep a usable file when a route cannot accept one.
6
- export async function attachScreenshot (ctx, exec, value, data) {
36
+ export async function attachScreenshot (ctx, exec, value, data, options = {}) {
37
+ // The local file has geometry even when the model route cannot accept images.
38
+ setCoordinates(value, value.imageSize)
7
39
  const fallback = (reason) => {
40
+ delete value.image
41
+ restoreFileGeometry(value)
8
42
  value.imageStatus = 'path-only'
9
43
  value.imageReason = reason
10
44
  value.view = reason === 'model-text-only'
@@ -14,6 +48,8 @@ export async function attachScreenshot (ctx, exec, value, data) {
14
48
  return value
15
49
  }
16
50
  try {
51
+ if (!validImageSize(value.imageSize)) return fallback('invalid-source-image-size')
52
+ const sourceSize = [...value.imageSize]
17
53
  const attachments = ctx.get?.('attachments')
18
54
  const llm = ctx.get?.('llm')
19
55
  if (!attachments?.saveImage) return fallback('attachments-unavailable')
@@ -27,21 +63,32 @@ export async function attachScreenshot (ctx, exec, value, data) {
27
63
  if (!attachments.imageLimits?.mediaTypes?.includes(value.mime)) return fallback('media-type-unavailable')
28
64
  const image = await attachments.saveImage({ data, mediaType: value.mime, name: basename(value.path) })
29
65
  if (exec?.signal?.aborted) return fallback('cancelled')
66
+ const attachedSize = [image?.width, image?.height]
67
+ if (!validImageSize(attachedSize)) return fallback('invalid-attachment-image-size')
68
+ const evidence = visionEvidence(exec, { ...options, adapterVersion: active.adapterVersion || 'unversioned' })
69
+ const resized = attachedSize.some((n, axis) => n !== sourceSize[axis])
70
+ if (resized) {
71
+ value.sourceImageSize = sourceSize
72
+ if (validPoint(value.cursorInImage)) {
73
+ value.sourceCursorInImage = { ...value.cursorInImage }
74
+ value.cursorInImage = { x: value.cursorInImage.x * attachedSize[0] / sourceSize[0],
75
+ y: value.cursorInImage.y * attachedSize[1] / sourceSize[1] }
76
+ } else delete value.cursorInImage
77
+ value.imageSize = attachedSize
78
+ }
30
79
  value.image = image
31
80
  value.imageStatus = 'attached'
32
- const r = value.region
33
- if (r && [r.x, r.y, r.width, r.height].every(Number.isFinite) && image.width > 0 && image.height > 0) {
34
- value.coordinates = {
35
- screenOrigin: [r.x, r.y], imageSize: [image.width, image.height],
36
- screenPerImagePixel: [r.width / image.width, r.height / image.height],
37
- }
38
- }
81
+ setCoordinates(value, attachedSize)
82
+ if (resized && value.coordinateRule) value.coordinateRule += ' imageSize, cursorInImage and coordinates describe the attached image. path retains the original capture: use sourceImageSize and sourceCursorInImage for that file; map its pixels using region.width/sourceImageSize[0] and region.height/sourceImageSize[1].'
39
83
  value.view = value.settled === false
40
84
  ? 'NOT settled — may be a partial render; re-shoot.'
41
85
  : 'Image attached. Inspect it directly; no read_image needed. Coordinates for clicks are screen pixels.'
42
- if (unverifiedVisionRoute(exec)) {
86
+ value.visionEvidence = evidence
87
+ if (value.visionEvidence.grounding === 'failed') {
43
88
  value.visionStatus = 'unverified-route'
44
- value.view = 'Image transport succeeded, but this GLM 5.3 route failed grounding tests. Use UIA/text readback; do not infer coordinates or success from unseen pixels. The image and path are retained for inspection.'
89
+ value.view = 'Image transport succeeded, but a current route record reports failed grounding. Use UIA/text readback; the image and path remain available. See visionEvidence for the scope and expiry.'
90
+ } else {
91
+ value.visionStatus = value.visionEvidence.grounding === 'verified' ? 'grounding-verified' : 'grounding-unverified'
45
92
  }
46
93
  return value
47
94
  } catch (error) {
@@ -75,6 +122,7 @@ export function selectBatchScreenshot (result, automatic, kind = 'image') {
75
122
  const shot = action.result
76
123
  if (shot?.image && shot !== latest) {
77
124
  delete shot.image
125
+ restoreFileGeometry(shot)
78
126
  shot.imageStatus = 'path-only'
79
127
  shot.imageReason = 'earlier-batch-image'
80
128
  shot.view = kind === 'state'
package/lib/index.js CHANGED
@@ -31,8 +31,8 @@ import Schema from '@deepseek-ai/schemastery'
31
31
  import { ComputerUse } from './core/index.js'
32
32
  import { policyDecision, HIGH_RISK_NOTE } from './policy.js'
33
33
  import { acknowledge, isAcked, ACK_HINT, isDrivingCall } from './skill-gate.js'
34
- import { mkDataDir, snapshotPath, appendAudit } from './audit.js'
35
- import { registerTools } from './tools.js'
34
+ import { mkDataDir, snapshotPath, installAudit } from './audit.js'
35
+ import { registerTools, isCycleStateOnly } from './tools.js'
36
36
  import { installToolExposure, ACTIVATION_TOOL } from './exposure.js'
37
37
  import { scopeOf } from '@deepseek-ai/dsh-scope'
38
38
  import { createCycleGate, agentKeyOf } from './cycle.js'
@@ -382,7 +382,14 @@ export const inject = ['tools', 'userQuestions']
382
382
  export const Config = Schema.object({
383
383
  enabled: Schema.boolean().default(true).description('enable the computer-use toolset'),
384
384
  progressiveTools: Schema.boolean().default(true).description('expose desktop execution tools only to Agents that load/activate computer-use; brakes remain available'),
385
- actionFeedback: Schema.boolean().default(true).description('post-input feedback for tested GLM 5.3 Flash / DeepSeek Flash routes; GLM 5.3 uses UIA state until its vision is verified'),
385
+ actionFeedback: Schema.boolean().default(true).description('automatic post-input observation; per-call feedback can explicitly select compact, full or none'),
386
+ actionFeedbackDetail: Schema.union([Schema.const('compact'), Schema.const('full'), Schema.const('none')]).default('compact').description('compact bounds local text readback to 6 KiB; vision keeps its image and crops only an explicitly requested verificationRegion. full retains complete route-specific feedback'),
387
+ feedbackRules: Schema.array(Schema.object({
388
+ provider: Schema.string().required(), model: Schema.string().required(),
389
+ adapterVersion: Schema.string().required().description('exact adapter version, or unversioned for a conservative failed-test fallback'),
390
+ grounding: Schema.union([Schema.const('failed'), Schema.const('verified')]).required(),
391
+ testedAt: Schema.string().required(), expiresAt: Schema.string().required(), reason: Schema.string(),
392
+ })).default(undefined).description('optional dated route grounding evidence; defaults to bundled expiring records; [] removes historical overrides'),
386
393
  automationMode: Schema.union([
387
394
  Schema.const('read-only').description('observation only'),
388
395
  Schema.const('standard').description('actuations require approval'),
@@ -436,7 +443,7 @@ export function apply (ctx, config) {
436
443
  rateLimit: { max: config.maxActionsPerMinute, windowMs: 60_000 },
437
444
  log: (m) => log.info(m),
438
445
  })
439
- cu.on('action', (rec) => appendAudit(dataDir, rec))
446
+ const detachAudit = installAudit(cu, dataDir)
440
447
 
441
448
  // ---- the live agent of the session that drives this machine ------------------------------
442
449
  // Captured from the tool execution itself (`exec.agent` is the live Agent object); a session
@@ -560,7 +567,14 @@ export function apply (ctx, config) {
560
567
  const expect = exitRecordText()
561
568
  // Rejections ride the SHARED qualification promise: every later call in this turn awaits the
562
569
  // same outcome instead of retrying it into a resurrection.
563
- await cu.resume(expect)
570
+ const cycleId = gate.snapshot().id
571
+ const openingContext = { signal: exec?.signal, cycleId, sessionKey: cl.sessionKey, agentKey: cl.agentKey,
572
+ assertOwnership: () => gate.assertOwnership(cl, cycleId) }
573
+ const resume = async () => {
574
+ await cu.resume(expect)
575
+ }
576
+ if (cu.withInputContext) await cu.withInputContext(openingContext, resume)
577
+ else await resume()
564
578
  if (!cu.exited && !exitedLatch()) {
565
579
  log.info('opened a new computer-use cycle for this turn (cleared the Ctrl+Alt+Q record left by an earlier cycle)')
566
580
  return { opened: true }
@@ -596,6 +610,10 @@ export function apply (ctx, config) {
596
610
  paused: pausedOrAsking, report: error => log.warn(`computer-use terminal cleanup failed: ${error.message}`),
597
611
  })
598
612
  const hooks = {
613
+ contextOf: (exec) => {
614
+ const owner = exec && typeof exec === 'object' ? callOwners.get(exec) : null
615
+ return owner ? { cycleId: owner.cycleId, sessionKey: owner.claim.sessionKey, agentKey: owner.claim.agentKey } : {}
616
+ },
599
617
  assertOwnership: (exec) => {
600
618
  const owner = exec && typeof exec === 'object' ? callOwners.get(exec) : null
601
619
  if (!owner) {
@@ -608,7 +626,7 @@ export function apply (ctx, config) {
608
626
  releaseCycle: (exec, why) => lifecycle.release(exec, why),
609
627
  onStart: (exec) => {
610
628
  inFlight++
611
- if (exec?.name === 'computer_ctrl' && exec.arguments?.action === 'release') {
629
+ if (isCycleStateOnly(exec?.name, exec?.arguments)) {
612
630
  callOwners.set(exec, { claim: claimOf(exec), cycleId: gate.snapshot().id })
613
631
  }
614
632
  },
@@ -1154,7 +1172,7 @@ export function apply (ctx, config) {
1154
1172
  })
1155
1173
 
1156
1174
  ctx.effect(() => {
1157
- const disposer = () => { cu.kill().catch(() => {}) }
1175
+ const disposer = () => { return cu.kill().catch(() => {}).finally(detachAudit) }
1158
1176
  return disposer
1159
1177
  })
1160
1178
 
@@ -0,0 +1,37 @@
1
+ // One command grammar for approval and dispatch. Native parsing implements the
2
+ // same contract for direct worker clients; cross-language fixtures guard parity.
3
+ export const KEY_ALIASES = Object.freeze({
4
+ control: 'ctrl', lwin: 'win', windows: 'win', meta: 'win',
5
+ return: 'enter', escape: 'esc', bksp: 'backspace', del: 'delete',
6
+ ins: 'insert', pageup: 'pgup', pagedown: 'pgdn', capslock: 'caps', printscreen: 'prtsc',
7
+ })
8
+ const modifierOrder = ['ctrl', 'alt', 'shift', 'win', 'rwin']
9
+ const named = new Set([...modifierOrder, 'enter', 'esc', 'tab', 'space', 'backspace', 'delete',
10
+ 'insert', 'home', 'end', 'pgup', 'pgdn', 'up', 'down', 'left', 'right', 'caps', 'apps',
11
+ 'prtsc', 'numlock', 'plus', 'minus', 'comma', 'period'])
12
+
13
+ export function parseKeyCommand (combo) {
14
+ if (typeof combo !== 'string' || !combo.trim()) throw new Error('KEY_COMMAND_INVALID: nonempty key combo required')
15
+ const unique = []
16
+ for (const part of combo.split('+')) {
17
+ const raw = part.trim().toLowerCase()
18
+ const key = KEY_ALIASES[raw] || raw
19
+ if (!key || !(named.has(key) || /^[a-z0-9]$/.test(key) || /^f(?:[1-9]|1\d|2[0-4])$/.test(key))) {
20
+ throw new Error(`KEY_COMMAND_INVALID: unknown or empty key ${JSON.stringify(raw)}`)
21
+ }
22
+ if (!unique.includes(key)) unique.push(key)
23
+ }
24
+ const modifiers = modifierOrder.filter(key => unique.includes(key))
25
+ const keys = unique.filter(key => !modifierOrder.includes(key))
26
+ const tokens = [...modifiers, ...keys]
27
+ return { combo: tokens.join('+'), tokens, modifiers, keys }
28
+ }
29
+
30
+ export function keyCommandRisk (combo) {
31
+ const { tokens } = parseKeyCommand(combo)
32
+ const has = key => tokens.includes(key)
33
+ const win = has('win') || has('rwin')
34
+ return has('delete') || has('backspace') ||
35
+ (win && ['r', 'l', 'd'].some(has)) || (has('alt') && has('f4')) ||
36
+ (has('ctrl') && has('shift') && has('esc'))
37
+ }
@@ -0,0 +1,43 @@
1
+ // Coordinate metadata is transport-independent; attachments and MCP content
2
+ // must describe the same captured pixels without serializing binary bytes as text.
3
+ export function captureCoordinates (region, imageSize) {
4
+ if (!region || ![region.x, region.y, region.width, region.height].every(Number.isFinite) ||
5
+ region.width <= 0 || region.height <= 0 || !Array.isArray(imageSize) || imageSize.length !== 2 ||
6
+ !imageSize.every(n => Number.isFinite(n) && n > 0)) return null
7
+ return { screenOrigin: [region.x, region.y], imageSize,
8
+ screenPerImagePixel: [region.width / imageSize[0], region.height / imageSize[1]] }
9
+ }
10
+
11
+ export function imageMime (bytes, declared) {
12
+ if (bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return 'image/png'
13
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return 'image/jpeg'
14
+ if (declared === 'image/png' || declared === 'image/jpeg') return declared
15
+ throw new Error('IMAGE_FORMAT_UNKNOWN: missing supported image signature or MIME')
16
+ }
17
+
18
+ export function splitMediaResult (value) {
19
+ const images = []
20
+ const clean = (input, location) => {
21
+ if (Buffer.isBuffer(input)) return undefined
22
+ if (Array.isArray(input)) return input.map((child, i) => clean(child, `${location}[${i}]`))
23
+ if (!input || typeof input !== 'object') return input
24
+ const output = {}
25
+ for (const [key, child] of Object.entries(input)) {
26
+ if (!Buffer.isBuffer(child)) output[key] = clean(child, `${location}.${key}`)
27
+ }
28
+ const bytes = Buffer.isBuffer(input.annotated) ? input.annotated
29
+ : Buffer.isBuffer(input.image) ? input.image : Buffer.isBuffer(input.raw) ? input.raw : null
30
+ if (bytes) {
31
+ const mime = imageMime(bytes, input.mime)
32
+ output.mime = mime
33
+ const coordinates = captureCoordinates(input.region, input.imageSize)
34
+ if (coordinates) output.coordinates = coordinates
35
+ images.push({ bytes, mime, location, metadata: output })
36
+ }
37
+ return output
38
+ }
39
+ const text = clean(value, 'result')
40
+ const latest = images.at(-1)
41
+ for (const entry of images) entry.metadata.imageStatus = entry === latest ? 'attached' : 'omitted-earlier-batch-image'
42
+ return { value: text, ...(latest ? { image: latest.bytes, mime: latest.mime, imageSource: latest.location } : {}) }
43
+ }