dsh-vision-router 2.1.0 → 2.1.1

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,19 @@
1
+ # v2.1.1
2
+
3
+ This is an urgent browser-startup hotfix for DVR 2.1.0.
4
+
5
+ ## Fixed
6
+
7
+ - Fixes #367: DSH Web could fail while applying `dsh-vision-router` with `cannot get property "remote.session" without inject`.
8
+ - The 2.1.0 client Host compatibility prelude no longer probes the optional `remote.session` namespace through the traceable `ctx.remote` proxy. It resolves the optional namespace through `ctx.get('remote.session')` first and keeps a guarded fallback for older/non-Cordis harnesses.
9
+ - The new Host model-catalog bridge still uses `remote.session.modelCatalog()` when available, while older supported Hosts keep the original `connection.api.llm.models()` path and exact legacy Remote/Connection surfaces.
10
+
11
+ ## Regression coverage
12
+
13
+ - Adds a production-shaped regression where reading `ctx.remote.session` throws the exact #367 Cordis error while `ctx.get('remote.session')` succeeds.
14
+ - Adds the same test to the default Node 22/24 suite and the Web compatibility suite.
15
+ - Keeps a fail-closed legacy-host regression so the hotfix does not turn the optional alpha namespace into a hard browser dependency.
16
+
17
+ ## Upgrade
18
+
19
+ Users on 2.1.0 who hit the browser loader failure should upgrade to 2.1.1 and restart the DSH Web/Desktop process. No settings migration is required.
@@ -20,11 +20,22 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
20
20
  var LEGACY_CREDENTIAL_EVENT = 'credentials/updated';
21
21
  var HOST_CREDENTIAL_EVENT = 'credentials/reference-updated';
22
22
 
23
- function hasHostCatalog(remote) {
24
- return !!(
25
- remote && remote.session &&
26
- typeof remote.session.modelCatalog === 'function'
27
- );
23
+ function optionalHostSession(ctx, remote) {
24
+ try {
25
+ if (ctx && typeof ctx.get === 'function') {
26
+ var session = ctx.get('remote.session');
27
+ if (session) return session;
28
+ }
29
+ } catch (_) {}
30
+ try {
31
+ return remote && remote.session;
32
+ } catch (_) {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ function hasHostCatalog(session) {
38
+ return !!(session && typeof session.modelCatalog === 'function');
28
39
  }
29
40
 
30
41
  function wrapCatalogResult(value) {
@@ -37,23 +48,23 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
37
48
  return value;
38
49
  }
39
50
 
40
- function catalogModels(remote) {
51
+ function catalogModels(session) {
41
52
  return function models() {
42
- return Promise.resolve(remote.session.modelCatalog()).then(wrapCatalogResult);
53
+ return Promise.resolve(session.modelCatalog()).then(wrapCatalogResult);
43
54
  };
44
55
  }
45
56
 
46
- function compatibleApi(originalApi, remote) {
57
+ function compatibleApi(originalApi, session) {
47
58
  var originalLlm = originalApi && originalApi.llm;
48
59
  var llm = originalLlm && (typeof originalLlm === 'object' || typeof originalLlm === 'function')
49
60
  ? new Proxy(originalLlm, {
50
61
  get: function(target, property) {
51
- if (property === 'models') return catalogModels(remote);
62
+ if (property === 'models') return catalogModels(session);
52
63
  var value = Reflect.get(target, property, target);
53
64
  return typeof value === 'function' ? value.bind(target) : value;
54
65
  }
55
66
  })
56
- : { models: catalogModels(remote) };
67
+ : { models: catalogModels(session) };
57
68
  if (originalApi && (typeof originalApi === 'object' || typeof originalApi === 'function')) {
58
69
  return new Proxy(originalApi, {
59
70
  get: function(target, property) {
@@ -66,10 +77,10 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
66
77
  return { llm: llm };
67
78
  }
68
79
 
69
- function compatibleConnection(connection, remote) {
70
- if (!hasHostCatalog(remote)) return connection;
80
+ function compatibleConnection(connection, session) {
81
+ if (!hasHostCatalog(session)) return connection;
71
82
  if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
72
- var api = compatibleApi(connection.api, remote);
83
+ var api = compatibleApi(connection.api, session);
73
84
  return new Proxy(connection, {
74
85
  get: function(target, property) {
75
86
  if (property === 'api') return api;
@@ -79,8 +90,8 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
79
90
  });
80
91
  }
81
92
 
82
- function compatibleRemote(remote) {
83
- if (!hasHostCatalog(remote)) return remote;
93
+ function compatibleRemote(remote, session) {
94
+ if (!hasHostCatalog(session)) return remote;
84
95
  if (!remote || (typeof remote !== 'object' && typeof remote !== 'function')) return remote;
85
96
  return new Proxy(remote, {
86
97
  get: function(target, property) {
@@ -140,7 +151,8 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
140
151
 
141
152
  function compatibleContext(ctx) {
142
153
  if (!ctx || typeof ctx !== 'object' || typeof Proxy !== 'function') return ctx;
143
- var remote = compatibleRemote(ctx.remote);
154
+ var session = optionalHostSession(ctx, ctx.remote);
155
+ var remote = compatibleRemote(ctx.remote, session);
144
156
  var locale = compatibleLocale(ctx.locale);
145
157
  return new Proxy(ctx, {
146
158
  get: function(target, property) {
@@ -151,7 +163,7 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
151
163
  if (typeof get !== 'function') return get;
152
164
  return function(name) {
153
165
  var value = get.call(target, name);
154
- return name === 'connection' ? compatibleConnection(value, remote) : value;
166
+ return name === 'connection' ? compatibleConnection(value, session) : value;
155
167
  };
156
168
  }
157
169
  var value = Reflect.get(target, property, target);
package/lib/doctor.js CHANGED
@@ -303,12 +303,53 @@ function stripYamlComment(line) {
303
303
  return line
304
304
  }
305
305
 
306
+ function yamlIndent(line) {
307
+ return /^(\s*)/.exec(line)?.[1].length ?? 0
308
+ }
309
+
310
+ function rowIsInsideInsert(lines, rowIndex, rowIndent) {
311
+ for (let cursor = rowIndex - 1; cursor >= 0; cursor -= 1) {
312
+ const candidate = lines[cursor]
313
+ if (/^\s*$/.test(candidate)) continue
314
+ const indent = yamlIndent(candidate)
315
+ if (indent >= rowIndent) continue
316
+ return /^\s*-?\s*insert\s*:\s*$/.test(candidate)
317
+ }
318
+ return false
319
+ }
320
+
321
+ function inspectVisionRouterPatchRows(lines) {
322
+ let mountRows = 0
323
+ let overrideRows = 0
324
+
325
+ for (let index = 0; index < lines.length; index += 1) {
326
+ const idMatch = /^(\s*)-\s+id\s*:\s*['"]?([^'"\s]+)['"]?\s*$/.exec(lines[index])
327
+ if (!idMatch) continue
328
+ const rowIndent = idMatch[1].length
329
+ let pluginName
330
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
331
+ const candidate = lines[cursor]
332
+ if (/^\s*$/.test(candidate)) continue
333
+ const indent = yamlIndent(candidate)
334
+ if (indent <= rowIndent) break
335
+ const nameMatch = /^\s*name\s*:\s*['"]?([^'"\s]+)['"]?\s*$/.exec(candidate)
336
+ if (nameMatch) pluginName = nameMatch[1]
337
+ }
338
+ if (idMatch[2] !== 'vision-router' && pluginName !== PLUGIN_NAME) continue
339
+ if (rowIsInsideInsert(lines, index, rowIndent)) mountRows += 1
340
+ else overrideRows += 1
341
+ }
342
+
343
+ return { mountRows, overrideRows }
344
+ }
345
+
306
346
  export function inspectProfilePatch(patchPath) {
307
347
  if (!existsSync(patchPath)) {
308
348
  return {
309
349
  path: patchPath,
310
350
  exists: false,
311
351
  visionRouterRows: 0,
352
+ visionRouterOverrideRows: 0,
312
353
  manualVisionRouter: false,
313
354
  disablesOfficialDeepSeek: false,
314
355
  }
@@ -317,14 +358,13 @@ export function inspectProfilePatch(patchPath) {
317
358
  try {
318
359
  text = readFileBounded(patchPath).toString('utf8')
319
360
  } catch (error) {
320
- return { path: patchPath, exists: true, visionRouterRows: 0, manualVisionRouter: false, disablesOfficialDeepSeek: false, error: error instanceof Error ? error.message : String(error), oversized: error?.code === 'DOCTOR_INPUT_TOO_LARGE' }
361
+ return { path: patchPath, exists: true, visionRouterRows: 0, visionRouterOverrideRows: 0, manualVisionRouter: false, disablesOfficialDeepSeek: false, error: error instanceof Error ? error.message : String(error), oversized: error?.code === 'DOCTOR_INPUT_TOO_LARGE' }
321
362
  }
322
363
  const lines = text.split(/\r?\n/).map(stripYamlComment)
323
- let visionRouterRows = 0
364
+ const { mountRows: visionRouterRows, overrideRows: visionRouterOverrideRows } = inspectVisionRouterPatchRows(lines)
324
365
  let disablesOfficialDeepSeek = false
325
366
  for (let index = 0; index < lines.length; index += 1) {
326
367
  const line = lines[index]
327
- if (/^\s*name\s*:\s*['"]?dsh-vision-router['"]?\s*$/.test(line)) visionRouterRows += 1
328
368
  if (!/^\s*-?\s*id\s*:\s*['"]?llm-deepseek['"]?\s*$/.test(line)) continue
329
369
  for (let cursor = index + 1; cursor < Math.min(lines.length, index + 12); cursor += 1) {
330
370
  if (/^\s*-\s+id\s*:/.test(lines[cursor])) break
@@ -338,6 +378,7 @@ export function inspectProfilePatch(patchPath) {
338
378
  path: patchPath,
339
379
  exists: true,
340
380
  visionRouterRows,
381
+ visionRouterOverrideRows,
341
382
  manualVisionRouter: visionRouterRows > 0,
342
383
  disablesOfficialDeepSeek,
343
384
  }
@@ -581,4 +622,4 @@ export function doctorProfiles({ dshHome = resolveDshHome(), profile, fix = fals
581
622
  && applicable.every(profileHealthy)
582
623
  && (log.settingsSaveFailures?.length ?? 0) === 0,
583
624
  }
584
- }
625
+ }
@@ -0,0 +1,93 @@
1
+ export const GROUNDING_FRAME_SIZE = 1000
2
+
3
+ function finitePositiveInteger(value, name) {
4
+ const number = Number(value)
5
+ if (!Number.isFinite(number) || number <= 0) {
6
+ throw new TypeError(`${name} must be a positive finite number`)
7
+ }
8
+ return Math.max(1, Math.round(number))
9
+ }
10
+
11
+ /**
12
+ * Build the exact geometry used to letterbox a source raster into the
13
+ * grounding protocol's square frame. The rendered dimensions are integers so
14
+ * callers can resize to these exact values before extending the canvas; the
15
+ * inverse therefore never depends on a backend's resize policy.
16
+ */
17
+ export function createGroundingFrame(width, height, frameSize = GROUNDING_FRAME_SIZE) {
18
+ const sourceWidth = finitePositiveInteger(width, 'width')
19
+ const sourceHeight = finitePositiveInteger(height, 'height')
20
+ const size = finitePositiveInteger(frameSize, 'frameSize')
21
+ const scale = Math.min(size / sourceWidth, size / sourceHeight)
22
+ const renderedWidth = Math.max(1, Math.min(size, Math.round(sourceWidth * scale)))
23
+ const renderedHeight = Math.max(1, Math.min(size, Math.round(sourceHeight * scale)))
24
+ const left = Math.floor((size - renderedWidth) / 2)
25
+ const top = Math.floor((size - renderedHeight) / 2)
26
+ const right = size - renderedWidth - left
27
+ const bottom = size - renderedHeight - top
28
+
29
+ return Object.freeze({
30
+ frameWidth: size,
31
+ frameHeight: size,
32
+ sourceWidth,
33
+ sourceHeight,
34
+ renderedWidth,
35
+ renderedHeight,
36
+ left,
37
+ top,
38
+ right,
39
+ bottom,
40
+ scaleX: renderedWidth / sourceWidth,
41
+ scaleY: renderedHeight / sourceHeight,
42
+ })
43
+ }
44
+
45
+ function finiteCoordinate(value) {
46
+ const number = Number(value)
47
+ return Number.isFinite(number) ? number : undefined
48
+ }
49
+
50
+ function clamp(value, min, max) {
51
+ return Math.max(min, Math.min(value, max))
52
+ }
53
+
54
+ /** Map a box reported in the explicit square grounding frame back to source pixels. */
55
+ export function groundingFrameBoxToSource(box, frame) {
56
+ if (!box || !frame) return undefined
57
+ const x1 = finiteCoordinate(box.x1)
58
+ const y1 = finiteCoordinate(box.y1)
59
+ const x2 = finiteCoordinate(box.x2)
60
+ const y2 = finiteCoordinate(box.y2)
61
+ if ([x1, y1, x2, y2].some((value) => value === undefined)) return undefined
62
+
63
+ const raw = {
64
+ x1: (x1 - frame.left) / frame.scaleX,
65
+ y1: (y1 - frame.top) / frame.scaleY,
66
+ x2: (x2 - frame.left) / frame.scaleX,
67
+ y2: (y2 - frame.top) / frame.scaleY,
68
+ }
69
+ const mapped = {
70
+ x1: Math.floor(clamp(raw.x1, 0, frame.sourceWidth)),
71
+ y1: Math.floor(clamp(raw.y1, 0, frame.sourceHeight)),
72
+ x2: Math.ceil(clamp(raw.x2, 0, frame.sourceWidth)),
73
+ y2: Math.ceil(clamp(raw.y2, 0, frame.sourceHeight)),
74
+ }
75
+ if (mapped.x2 <= mapped.x1 || mapped.y2 <= mapped.y1) return undefined
76
+ return mapped
77
+ }
78
+
79
+ /** Map source-pixel geometry into the square frame, useful for protocol tests. */
80
+ export function sourceBoxToGroundingFrame(box, frame) {
81
+ if (!box || !frame) return undefined
82
+ const x1 = finiteCoordinate(box.x1)
83
+ const y1 = finiteCoordinate(box.y1)
84
+ const x2 = finiteCoordinate(box.x2)
85
+ const y2 = finiteCoordinate(box.y2)
86
+ if ([x1, y1, x2, y2].some((value) => value === undefined)) return undefined
87
+ return {
88
+ x1: frame.left + x1 * frame.scaleX,
89
+ y1: frame.top + y1 * frame.scaleY,
90
+ x2: frame.left + x2 * frame.scaleX,
91
+ y2: frame.top + y2 * frame.scaleY,
92
+ }
93
+ }
@@ -0,0 +1,244 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { unlink } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import { writeArtifactFile } from './artifact-boundary.js'
5
+ import {
6
+ createGroundingFrame,
7
+ groundingFrameBoxToSource,
8
+ } from './grounding-coordinate-frame.js'
9
+
10
+ const GROUNDING_TOOL_NAMES = new Set(['vision_ground', 'vision_detect'])
11
+ let sharpPromise
12
+
13
+ function loadSharp() {
14
+ if (!sharpPromise) {
15
+ sharpPromise = import('sharp').then((mod) => mod.default ?? mod).catch((error) => {
16
+ sharpPromise = undefined
17
+ throw error
18
+ })
19
+ }
20
+ return sharpPromise
21
+ }
22
+
23
+ function workspaceOf(exec) {
24
+ const cwd = exec?.agent?.session?.header?.cwd
25
+ return typeof cwd === 'string' && cwd !== '' ? cwd : process.cwd()
26
+ }
27
+
28
+ function artifactsDirOf(config) {
29
+ return typeof config?.artifactsDir === 'string' && config.artifactsDir !== ''
30
+ ? config.artifactsDir
31
+ : '.dsh-vision-router/artifacts'
32
+ }
33
+
34
+ function contextService(ctx, name) {
35
+ try {
36
+ if (typeof ctx?.get === 'function') return ctx.get(name)
37
+ } catch {
38
+ return undefined
39
+ }
40
+ return ctx?.[name]
41
+ }
42
+
43
+ async function readSourceBytes(ctx, core, sessionVisionIndex, exec, image) {
44
+ const source = String(image ?? '')
45
+ if (core?.isAttachmentIdInput?.(source)) {
46
+ const session = exec?.agent?.session
47
+ const ref = sessionVisionIndex?.lookupAttachment?.(session, source.trim())
48
+ if (ref === undefined) {
49
+ throw new Error(
50
+ `vision-router: unknown attachment id "${source}" (it must come from an image uploaded in this conversation)`,
51
+ )
52
+ }
53
+ const attachments = contextService(ctx, 'attachments')
54
+ if (!attachments || typeof attachments.readImage !== 'function') {
55
+ throw new Error('vision-router: the attachment service is not available in this deployment')
56
+ }
57
+ const stored = await attachments.readImage(ref, exec?.signal)
58
+ if (!stored?.data) throw new Error(`vision-router: failed to read attachment ${source}`)
59
+ return Buffer.from(stored.data)
60
+ }
61
+
62
+ const fs = contextService(ctx, 'fs')
63
+ if (!fs || typeof fs.resolve !== 'function' || typeof fs.readBytes !== 'function') {
64
+ throw new Error('vision-router: the fs service is not available')
65
+ }
66
+ const target = await fs.resolve(source)
67
+ return Buffer.from(await fs.readBytes(target, undefined, 20 * 1024 * 1024))
68
+ }
69
+
70
+ async function buildGroundingFrame(bytes) {
71
+ const sharp = await loadSharp()
72
+ const meta = await sharp(bytes, { failOn: 'none' }).metadata()
73
+ const width = Number(meta.width ?? 0)
74
+ const height = Number(meta.height ?? 0)
75
+ if (width <= 0 || height <= 0) throw new Error('could not read image dimensions')
76
+ const frame = createGroundingFrame(width, height)
77
+ const framed = await sharp(bytes, { failOn: 'none' })
78
+ .resize(frame.renderedWidth, frame.renderedHeight, { fit: 'fill' })
79
+ .extend({
80
+ top: frame.top,
81
+ bottom: frame.bottom,
82
+ left: frame.left,
83
+ right: frame.right,
84
+ background: { r: 0, g: 0, b: 0, alpha: 1 },
85
+ })
86
+ .png()
87
+ .toBuffer()
88
+ return { frame, framed, width, height }
89
+ }
90
+
91
+ function parseToolResult(raw) {
92
+ if (typeof raw !== 'string' || raw.trim() === '') return undefined
93
+ try {
94
+ const parsed = JSON.parse(raw)
95
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined
96
+ } catch {
97
+ return undefined
98
+ }
99
+ }
100
+
101
+ async function removeInternalFrame(workspace, publishedPath) {
102
+ if (typeof publishedPath !== 'string' || publishedPath === '') return
103
+ const target = path.isAbsolute(publishedPath) ? publishedPath : path.resolve(workspace, publishedPath)
104
+ try {
105
+ await unlink(target)
106
+ } catch {
107
+ // The internal frame is best-effort cleanup. Artifact retention may already
108
+ // have removed it after the delegated tool completed.
109
+ }
110
+ }
111
+
112
+ async function executeInGroundingFrame({
113
+ ctx,
114
+ core,
115
+ config,
116
+ sessionVisionIndex,
117
+ def,
118
+ execute,
119
+ args,
120
+ exec,
121
+ }) {
122
+ const source = String(args?.image ?? '')
123
+ const bytes = await readSourceBytes(ctx, core, sessionVisionIndex, exec, source)
124
+ const { frame, framed, width, height } = await buildGroundingFrame(bytes)
125
+ const workspace = workspaceOf(exec)
126
+ const artifactsDir = artifactsDirOf(config)
127
+ const internalName = `.grounding-frame-${randomUUID()}.png`
128
+ const framePath = await writeArtifactFile(workspace, artifactsDir, internalName, framed)
129
+
130
+ try {
131
+ const raw = await execute(
132
+ {
133
+ ...(args ?? {}),
134
+ image: framePath,
135
+ // Core's annotation would be in the protocol frame. Re-publish only an
136
+ // original-raster annotation after the deterministic inverse transform.
137
+ annotate: false,
138
+ },
139
+ exec,
140
+ )
141
+ const delegated = parseToolResult(raw)
142
+ if (!delegated || delegated.ok === false) return raw
143
+
144
+ if (def.name === 'vision_ground') {
145
+ const mapped = groundingFrameBoxToSource(delegated, frame)
146
+ if (!mapped) {
147
+ throw new Error('vision_ground: the model box falls entirely outside the letterboxed source raster')
148
+ }
149
+ const result = { ...mapped, width, height }
150
+ if (args?.annotate !== false) {
151
+ if (typeof core?.annotateBoxBuffer !== 'function') {
152
+ throw new Error('vision_ground: core annotation helper is unavailable')
153
+ }
154
+ const annotated = await core.annotateBoxBuffer(bytes, mapped)
155
+ const stem = core?.artifactStemOf?.(source, 'ground') ?? `image-ground-${Date.now()}`
156
+ result.annotatedPath = await writeArtifactFile(workspace, artifactsDir, `${stem}.png`, annotated)
157
+ }
158
+ return JSON.stringify(result)
159
+ }
160
+
161
+ const elements = []
162
+ for (const item of Array.isArray(delegated.elements) ? delegated.elements : []) {
163
+ const mapped = groundingFrameBoxToSource(item?.box, frame)
164
+ if (!mapped) continue
165
+ elements.push({
166
+ ...item,
167
+ number: elements.length + 1,
168
+ box: mapped,
169
+ })
170
+ }
171
+ const result = { width, height, elements }
172
+ if (args?.annotate !== false && elements.length > 0) {
173
+ if (typeof core?.annotateBoxesBuffer !== 'function') {
174
+ throw new Error('vision_detect: core annotation helper is unavailable')
175
+ }
176
+ const annotated = await core.annotateBoxesBuffer(bytes, elements.map((item) => item.box))
177
+ const stem = core?.artifactStemOf?.(source, 'detect') ?? `image-detect-${Date.now()}`
178
+ result.annotatedPath = await writeArtifactFile(workspace, artifactsDir, `${stem}.png`, annotated)
179
+ }
180
+ return JSON.stringify(result)
181
+ } finally {
182
+ await removeInternalFrame(workspace, framePath)
183
+ }
184
+ }
185
+
186
+ function wrapGroundingDefinition(ctx, options, def) {
187
+ if (!def || !GROUNDING_TOOL_NAMES.has(def.name) || typeof def.execute !== 'function') return def
188
+ const execute = def.execute
189
+ return {
190
+ ...def,
191
+ execute(args, exec) {
192
+ return executeInGroundingFrame({
193
+ ctx,
194
+ ...options,
195
+ def,
196
+ execute,
197
+ args,
198
+ exec,
199
+ })
200
+ },
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Give vision_ground / vision_detect one explicit provider-independent
206
+ * coordinate protocol: the model always receives an exact 1000x1000
207
+ * letterboxed raster, while callers always receive Host-canonical source
208
+ * pixels. Core still owns provider selection, fallback, retry and failure
209
+ * semantics; this boundary owns only raster geometry and original-image
210
+ * annotation.
211
+ */
212
+ export function contextWithGroundingCoordinateFrame(ctx, options = {}) {
213
+ if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return ctx
214
+ const sourceTools = ctx.tools ?? contextService(ctx, 'tools')
215
+ if (!sourceTools || typeof sourceTools.register !== 'function') return ctx
216
+
217
+ const tools = new Proxy(sourceTools, {
218
+ get(target, property) {
219
+ if (property !== 'register') {
220
+ const value = Reflect.get(target, property, target)
221
+ return typeof value === 'function' ? value.bind(target) : value
222
+ }
223
+ const register = Reflect.get(target, property, target)
224
+ return (def, ...rest) => register.call(
225
+ target,
226
+ wrapGroundingDefinition(ctx, options, def),
227
+ ...rest,
228
+ )
229
+ },
230
+ })
231
+
232
+ return new Proxy(ctx, {
233
+ get(target, property) {
234
+ if (property === 'tools') return tools
235
+ if (property === 'get') {
236
+ const get = Reflect.get(target, property, target)
237
+ if (typeof get !== 'function') return get
238
+ return (name, ...rest) => name === 'tools' ? tools : get.call(target, name, ...rest)
239
+ }
240
+ const value = Reflect.get(target, property, target)
241
+ return typeof value === 'function' ? value.bind(target) : value
242
+ },
243
+ })
244
+ }
@@ -28,6 +28,7 @@ import { installTesseractExecFileCompat } from './tesseract-exec-compat.js'
28
28
  import { installLocalMutationRouteBoundary } from './web-capability-boundary.js'
29
29
  import { installScreenshotSourceBoundary } from './screenshot-source-boundary.js'
30
30
  import { installVisionToolRuntimeBoundary } from './vision-tool-runtime-boundary.js'
31
+ import { contextWithGroundingCoordinateFrame } from './grounding-coordinate-runtime.js'
31
32
  import { installVisionRoutingRuntime } from './vision-routing-runtime.js'
32
33
  import { createCapabilityProfileStore } from './vision-capability-probe.js'
33
34
  import { installCapabilityBenchmarkService } from './vision-capability-benchmark-service.js'
@@ -249,11 +250,21 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
249
250
  logger: logging.logger,
250
251
  })
251
252
 
253
+ // Ground/detect own one explicit raster-coordinate boundary. It wraps only
254
+ // the tool-registration view, before runtime-performance observation. That
255
+ // leaves adapter sampling on the same execution seam while backend preflight
256
+ // remains the final outer Core-visible policy boundary.
257
+ const groundingCoordinateCtx = contextWithGroundingCoordinateFrame(executionCtx, {
258
+ core,
259
+ config: runtimeConfig,
260
+ sessionVisionIndex: sessionVisionRuntime.index,
261
+ })
262
+
252
263
  // Only real visual-tool adapter streams are timed, and only while live Auto
253
264
  // authority permits future-routing observation. Benchmark/background calls
254
265
  // have no visual-tool scope and cannot contaminate this store.
255
266
  const performanceCtx = contextWithVisionRuntimePerformance(
256
- executionCtx,
267
+ groundingCoordinateCtx,
257
268
  runtimePerformanceStore,
258
269
  {
259
270
  logger: logging.logger,
@@ -345,4 +356,4 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
345
356
  )
346
357
  throw error
347
358
  }
348
- }
359
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -69,14 +69,15 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "scripts": {
72
- "test": "node --test tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/pi-ai-bridge-wire-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/runtime-i18n.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
72
+ "test": "node --test tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/pi-ai-bridge-wire-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/runtime-i18n.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
73
73
  "test:core": "node --test tests/core.test.js tests/runtime-e2e.test.js tests/structured-bootstrap.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-execution-policy.test.js tests/vision-backend-runtime-policy.test.js",
74
74
  "test:routing": "node --test tests/vision-routing-product.test.js tests/vision-capability-router.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow.test.js tests/vision-capability-shadow-health.test.js tests/vision-runtime-performance.test.js tests/vision-background-benchmark.test.js tests/vision-image-input-verdict.test.js",
75
75
  "test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/replay-delegation.test.js tests/legacy-session-repair.test.js tests/session-repair-v2.test.js tests/issue-276-session-image-ownership.test.js",
76
76
  "test:resources": "node --test tests/artifact-path-security.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js",
77
77
  "test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/rc6-rc7-compat.test.js tests/rc6-real-settings-persistence.test.js tests/android-attachment-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
78
- "test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js",
78
+ "test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js",
79
79
  "test:contract": "node --test tests/manifest-dependencies.test.js tests/bundle-defaults.test.js tests/zero-regression-gate.test.js tests/issue-289-native-nonintervention.test.js tests/runtime-boundary-fixes.test.js tests/runtime-i18n.test.js tests/adapter-prepare-call-compat.test.js tests/rc6-rc7-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js",
80
+ "test:grounding": "node --test tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/vision-capability-grounding-proof-contract.test.js",
80
81
  "test:stress": "node scripts/image-resource-stress.mjs && node --test tests/large-image-resource-integration.test.js tests/resource-retention-lifecycle.test.js"
81
82
  },
82
83
  "pnpm": {
@@ -95,4 +96,4 @@
95
96
  },
96
97
  "bundle": { "patch": "./cordis.patch.yml" }
97
98
  }
98
- }
99
+ }