dsh-vision-router 3.0.3 → 3.0.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.
@@ -80,6 +80,9 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
80
80
  VISION_CHECK_BACKEND_STALE:[text('当前模型暂时无法直接调用,请确认供应商/模型仍可用后重试','This model is not currently callable; verify the provider/model and retry')],
81
81
  VISION_CHECK_TIMEOUT:[text('测试超时,已自动结束','Test timed out and was stopped')],
82
82
  VISION_CHECK_UNSUPPORTED_IMAGE:[text('实测仅文本 · 图片请求被模型拒绝','Measured text-only · the model rejected image input')],
83
+ VISION_IMAGE_DELIVERY_UNAVAILABLE:[text('Host未声明图片输入,请在DSH模型设置中启用Image再测','Host declares text-only input; enable Image in DSH Models settings and retest')],
84
+ VISION_CHECK_STALE_CONFIG:[text('测试期间模型配置已变化,请重新测试','Model input settings changed during the test; retest required')],
85
+ VISION_LOCAL_ONLY_POLICY:[text('本地识图模式下不允许测试非本地后端','Local-only vision policy blocks testing nonlocal backends')],
83
86
  VISION_CHECK_INFRASTRUCTURE:[text('识图测试组件暂不可用','Vision check infrastructure is unavailable')],
84
87
  CAPABILITY_BENCHMARK_INFRASTRUCTURE:[text('识图测试组件暂不可用','Vision check infrastructure is unavailable')],
85
88
  CAPABILITY_BENCHMARK_VISUAL_PROOF_FAILED:[text('模型没有正确读出测试图','The model did not correctly inspect the test image')]
@@ -91,9 +94,16 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
91
94
  }
92
95
  function reset(control){
93
96
  if(!control)return;
97
+ delete control.dataset.lastResult;
94
98
  setStatus(control,text('快速自检 · 1次请求 · 只验证当前模型能否识图 · 不写入Auto能力分数','Quick check · 1 request · verifies image input only · does not write Auto capability scores'));
95
99
  setButton(control,text('测试识图','Test vision'),false);
96
100
  }
101
+ function makeRemoteNotice(row){
102
+ var note=document.createElement('div');note.setAttribute(CONTROL_ATTR,'remote');
103
+ note.style.cssText='flex:1 0 100%;font-size:12px;opacity:.7;margin-top:4px';
104
+ note.textContent=text('识图自检仅可在DSH本机界面执行;远程页面不发送测试请求。','Vision testing is local-only. Open the DSH UI on the Host to run the image check.');
105
+ row.appendChild(note);return note;
106
+ }
97
107
  function makeControl(row){
98
108
  if(!row||!row.style)return undefined;
99
109
  row.style.flexWrap='wrap';
@@ -131,15 +141,15 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
131
141
  if(!currentRun(control,token,key))return;
132
142
  if(!response.ok||!body||body.ok!==true){
133
143
  var raw=String(body&&(body.error||body.code)||('HTTP '+response.status));
134
- setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+failureMessage(body,response.status),raw);notifyChanged();return;
144
+ setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+failureMessage(body,response.status),raw);control.dataset.lastResult=String(Date.now());notifyChanged();return;
135
145
  }
136
146
  var via=transport(body.transport),latency=seconds(body.latencyMs),output=short(body.output,72);
137
147
  var suffix=(via?' · '+via:'')+(latency?' · '+latency:'')+(output?' · '+output:'');
138
- setStatus(control,'✓ '+selection.provider+'/'+selection.model+text(' · 图片识别正常',' · image verified')+suffix);notifyChanged();
148
+ setStatus(control,'✓ '+selection.provider+'/'+selection.model+text(' · 图片识别正常',' · image verified')+suffix+' · '+new Date().toLocaleTimeString());control.dataset.lastResult=String(Date.now());notifyChanged();
139
149
  }catch(error){
140
150
  if(!currentRun(control,token,key))return;
141
151
  var message=error&&error.name==='AbortError'?text('测试超时,已自动结束','Test timed out and was stopped'):text('测试失败','Vision check failed');
142
- setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+message,String(error&&error.message||error));
152
+ setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+message,String(error&&error.message||error));control.dataset.lastResult=String(Date.now());
143
153
  }finally{
144
154
  if(timer!==undefined)clearTimeout(timer);
145
155
  if(ticker!==undefined)clearInterval(ticker);
@@ -148,12 +158,13 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
148
158
  }
149
159
  }
150
160
  function scan(){
151
- if(!localPage())return;
161
+ var local=localPage();
152
162
  var rows=[];try{rows=Array.prototype.slice.call(document.querySelectorAll(ROW_SELECTOR));}catch(_){rows=[];}
153
163
  rows.forEach(function(row){
154
164
  var selection=rowSelection(row),control=row.querySelector('['+CONTROL_ATTR+']');
155
165
  if(!complete(selection)){if(control){abortActive(control);if(control.remove)control.remove();}return;}
156
- if(!control)control=makeControl(row);
166
+ if(!control)control=local?makeControl(row):makeRemoteNotice(row);
167
+ if(!local)return;
157
168
  var key=selectionKey(selection);
158
169
  if(control&&control.dataset.selection!==key){abortActive(control);control.dataset.selection=key;reset(control);}
159
170
  });
@@ -167,8 +178,15 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
167
178
  }catch(_){}
168
179
  return false;
169
180
  }
170
- function start(){
181
+ function invalidatePreviousResults(){
171
182
  if(!localPage())return;
183
+ var controls=document.querySelectorAll('['+CONTROL_ATTR+'][data-last-result]');
184
+ controls.forEach(function(control){abortActive(control);reset(control);
185
+ setStatus(control,text('配置可能已变化,请重新测试','Configuration may have changed; retest required'));
186
+ });
187
+ }
188
+ function start(){
189
+ window.addEventListener('focus',invalidatePreviousResults);
172
190
  document.addEventListener('change',function(event){var row=event&&event.target&&event.target.closest?event.target.closest(ROW_SELECTOR):undefined;if(row)schedule();},true);
173
191
  if(typeof MutationObserver==='function'&&document.documentElement){
174
192
  var observer=new MutationObserver(function(records){
@@ -5,7 +5,7 @@ import process from 'node:process'
5
5
  import { resolveDshHome } from './doctor.js'
6
6
  import { CAPABILITY_BENCHMARK_SUITE_REVISION } from './vision-capability-benchmark.js'
7
7
 
8
- const CACHE_VERSION = 1
8
+ const CACHE_VERSION = 2
9
9
  const MAX_ENTRIES = 128
10
10
 
11
11
  function cleanText(value, max = 256) {
@@ -14,7 +14,7 @@ function cleanText(value, max = 256) {
14
14
 
15
15
  function cleanVerdict(value) {
16
16
  if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
17
- if (value.state !== 'unsupported') return undefined
17
+ if (value.state !== 'unsupported' || value.hostMode !== 'native-image') return undefined
18
18
  if (Number(value.suiteRevision) !== CAPABILITY_BENCHMARK_SUITE_REVISION) return undefined
19
19
  const fingerprint = cleanText(value.fingerprint, 64)
20
20
  if (!/^ep2_[0-9a-f]{32}$/.test(fingerprint)) return undefined
@@ -29,6 +29,7 @@ function cleanVerdict(value) {
29
29
  provider,
30
30
  model,
31
31
  state: 'unsupported',
32
+ hostMode: 'native-image',
32
33
  reason: 'provider-rejected-image',
33
34
  measuredAt,
34
35
  suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
@@ -103,7 +104,7 @@ export function createImageInputVerdictStore(options = {}) {
103
104
  await ready
104
105
  return records.get(String(fingerprint ?? ''))
105
106
  },
106
- async markUnsupported({ fingerprint, key, provider, model, measuredAt = Date.now() } = {}) {
107
+ async markUnsupported({ fingerprint, key, provider, model, hostMode, measuredAt = Date.now() } = {}) {
107
108
  await ready
108
109
  const clean = cleanVerdict({
109
110
  fingerprint,
@@ -111,6 +112,7 @@ export function createImageInputVerdictStore(options = {}) {
111
112
  provider,
112
113
  model,
113
114
  state: 'unsupported',
115
+ hostMode,
114
116
  measuredAt,
115
117
  suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
116
118
  })
@@ -345,7 +345,15 @@ const CLIENT_PRELUDE = String.raw`(function(){
345
345
  })();`
346
346
 
347
347
  function replaceAll(source, before, after) {
348
- return source.includes(before) ? source.split(before).join(after) : source
348
+ let result = source.includes(before) ? source.split(before).join(after) : source
349
+ // Windows git checkouts can put CRLF inside injected client-source template
350
+ // literals. Match only the same owned seam with CRLF; do not normalize or
351
+ // rewrite the surrounding HTML or any unrelated plugin's source.
352
+ if (before.includes('\n')) {
353
+ const windowsBefore = before.replaceAll('\n', '\r\n')
354
+ if (result.includes(windowsBefore)) result = result.split(windowsBefore).join(after)
355
+ }
356
+ return result
349
357
  }
350
358
 
351
359
  export function hardenVisionClientSource(source) {
@@ -403,7 +411,7 @@ export function hardenVisionClientSource(source) {
403
411
  next = replaceAll(
404
412
  next,
405
413
  ` var message = latest && typeof latest.error === 'string' && latest.error !== ''\n ? latest.error\n : t('failedUnknown');`,
406
- ` var recovered = recovery && typeof recovery.getSnapshot === 'function' ? recovery.getSnapshot() : recoveryError;\n var message = latest && typeof latest.error === 'string' && latest.error !== ''\n ? latest.error\n : recovered && recovered.message ? recovered.message : t('failedUnknown');`,
414
+ ` var recoveryStore = typeof recovery === 'undefined' ? null : recovery;\n if (!recoveryStore && typeof window !== 'undefined') {\n var hardening = window.__dshVisionRouterRootHardening;\n if (hardening && typeof hardening.recoveryFor === 'function') recoveryStore = hardening.recoveryFor(directory);\n }\n var recovered = recoveryStore && typeof recoveryStore.getSnapshot === 'function' ? recoveryStore.getSnapshot() : null;\n var message = latest && typeof latest.error === 'string' && latest.error !== ''\n ? latest.error\n : recovered && recovered.message ? recovered.message : t('failedUnknown');`,
407
415
  )
408
416
 
409
417
  next = replaceAll(
@@ -431,10 +439,19 @@ export function hardenVisionClientSource(source) {
431
439
  )
432
440
 
433
441
  next = replaceAll(next, `function wrapStore(store, settings) {`, `function wrapStore(store, settings, recovery) {`)
442
+ // A synthetic recovery snapshot must retain referential identity until
443
+ // its Host source snapshot or recovery error actually changes. Otherwise
444
+ // React's useSyncExternalStore sees a new snapshot on every read and can
445
+ // enter a maximum-update-depth loop after a failed select + catalog refresh.
446
+ next = replaceAll(
447
+ next,
448
+ ` var lastProjected;\n function getSnapshot() {`,
449
+ ` var lastProjected;\n var lastRecoverySource;\n var lastRecoveryError;\n var lastRecoveryRaw;\n function getSnapshot() {`,
450
+ )
434
451
  next = replaceAll(
435
452
  next,
436
453
  ` var raw = typeof store.getSnapshot === 'function' ? store.getSnapshot() : undefined;\n var config = currentVisionConfig(settings);`,
437
- ` var raw = typeof store.getSnapshot === 'function' ? store.getSnapshot() : undefined;\n var recovered = recovery && typeof recovery.getSnapshot === 'function' ? recovery.getSnapshot() : null;\n if (recovered && raw && (raw.status === 'selecting' || raw.status === 'loading')) {\n raw = Object.assign({}, raw, { status: 'error', error: recovered.message || String(recovered) });\n }\n var config = currentVisionConfig(settings);`,
454
+ ` var raw = typeof store.getSnapshot === 'function' ? store.getSnapshot() : undefined;\n var recovered = recovery && typeof recovery.getSnapshot === 'function' ? recovery.getSnapshot() : null;\n if (recovered && raw && (raw.status === 'selecting' || raw.status === 'loading')) {\n if (raw !== lastRecoverySource || recovered !== lastRecoveryError) {\n lastRecoverySource = raw;\n lastRecoveryError = recovered;\n lastRecoveryRaw = Object.assign({}, raw, { status: 'error', error: recovered.message || String(recovered) });\n }\n raw = lastRecoveryRaw;\n }\n var config = currentVisionConfig(settings);`,
438
455
  )
439
456
  next = replaceAll(
440
457
  next,
@@ -126,17 +126,42 @@ function isLocalHostName(value) {
126
126
  return host === 'localhost' || host.endsWith('.localhost') || isLoopbackAddress(host)
127
127
  }
128
128
 
129
+ // Forwarding metadata can only *deny* a local-machine capability. Never
130
+ // interpret X-Forwarded-For or similar caller-controlled headers as proof
131
+ // that a request really originated at the local browser.
132
+ const PROXY_PROVENANCE_HEADERS = new Set([
133
+ 'forwarded',
134
+ 'x-forwarded-for',
135
+ 'x-forwarded-host',
136
+ 'x-forwarded-proto',
137
+ 'x-real-ip',
138
+ 'x-client-ip',
139
+ 'true-client-ip',
140
+ 'cf-connecting-ip',
141
+ 'via',
142
+ ])
143
+
144
+ function hasProxyProvenance(req) {
145
+ const headers = req?.headers
146
+ return !!headers && typeof headers === 'object' &&
147
+ Object.keys(headers).some((name) => PROXY_PROVENANCE_HEADERS.has(name.toLowerCase()))
148
+ }
149
+
129
150
  /**
130
151
  * Browser-facing local capability check. A real network request needs BOTH a
131
- * loopback TCP peer and a loopback/localhost Host header. This prevents the
132
- * common reverse-proxy shape (proxy -> 127.0.0.1, external Host preserved)
133
- * from silently acquiring local-machine capabilities. Internal direct handler
134
- * calls have no transport and remain compatible for tests/host composition.
152
+ * loopback TCP peer and a loopback/localhost Host header, with no forwarding
153
+ * metadata. This protects supported proxy deployments preserving external Host
154
+ * and fails closed when a proxy rewrites Host but keeps standard hop markers.
155
+ * A proxy that erases every originating fact is indistinguishable from a
156
+ * genuine loopback browser request; these headers are not an attestation.
157
+ * Internal direct handler calls have no socket and remain supported.
135
158
  */
136
159
  export function isLocalUiRequest(req) {
137
160
  const transport = req?.socket ?? req?.connection
138
161
  if (!transport) return true
139
- return isLoopbackAddress(transport.remoteAddress) && isLocalHostName(requestHostName(req))
162
+ return isLoopbackAddress(transport.remoteAddress) &&
163
+ isLocalHostName(requestHostName(req)) &&
164
+ !hasProxyProvenance(req)
140
165
  }
141
166
 
142
167
  export function isLocalMutationRoute(path, method) {
@@ -204,40 +229,148 @@ function redactRemoteBody(path, body) {
204
229
  return body
205
230
  }
206
231
 
207
- /** Intercept one JSON end() so local capability material is never issued remotely. */
232
+ // Remote metadata is small. Never buffer an unbounded response from a future
233
+ // handler revision: local-only diagnostics should not turn into remote memory DoS.
234
+ const MAX_REMOTE_REDACTION_BYTES = 1024 * 1024
235
+
236
+ /**
237
+ * A remote response requiring redaction must be completely validated *before*
238
+ * emitting headers or bytes. Unlike intercepting end() alone, this also covers
239
+ * write()+end(), malformed JSON, oversize bodies and async stream completion.
240
+ * No handler may transmit its raw token, path or other local fields while the
241
+ * remote body remains unvalidated.
242
+ */
208
243
  function runWithRemoteReadRedaction(path, handler, req, res) {
209
244
  const originalEnd = res?.end
210
- if (typeof originalEnd !== 'function') return handler(req, res)
211
- let active = true
212
- res.end = function redactedEnd(chunk, ...args) {
213
- if (!active) return originalEnd.call(this, chunk, ...args)
214
- let next = chunk
245
+ const originalWrite = res?.write
246
+ const originalWriteHead = res?.writeHead
247
+ const originalFlushHeaders = res?.flushHeaders
248
+ if (typeof originalEnd !== 'function' || typeof originalWriteHead !== 'function') {
249
+ throw new TypeError('vision-router: remote redaction requires a buffered HTTP response')
250
+ }
251
+
252
+ const chunks = []
253
+ let bytes = 0
254
+ let done = false
255
+ let head
256
+ const restore = () => {
257
+ if (res.end === redactedEnd) res.end = originalEnd
258
+ if (res.write === redactedWrite) {
259
+ if (originalWrite === undefined) delete res.write
260
+ else res.write = originalWrite
261
+ }
262
+ if (res.writeHead === redactedWriteHead) res.writeHead = originalWriteHead
263
+ if (res.flushHeaders === redactedFlushHeaders) {
264
+ if (originalFlushHeaders === undefined) delete res.flushHeaders
265
+ else res.flushHeaders = originalFlushHeaders
266
+ }
267
+ }
268
+ const stripStagedHeaders = () => {
269
+ // Route handlers own the pre-redaction response. Even header values may
270
+ // contain future local-only tokens, paths or Set-Cookie material.
271
+ // Never forward any unreviewed staged header to a remote client.
215
272
  try {
216
- if (typeof chunk === 'string' || Buffer.isBuffer(chunk) || chunk instanceof Uint8Array) {
217
- const parsed = JSON.parse(Buffer.isBuffer(chunk) || chunk instanceof Uint8Array ? Buffer.from(chunk).toString('utf8') : chunk)
218
- next = JSON.stringify(redactRemoteBody(path, parsed))
219
- try { this.removeHeader?.('content-length') } catch { /* best effort */ }
220
- }
221
- } catch {
222
- // If a future handler stops returning JSON, do not corrupt its response.
273
+ for (const key of res.getHeaderNames?.() ?? []) res.removeHeader?.(key)
274
+ } catch { /* a test double may not expose the header table */ }
275
+ for (const key of ['content-length', 'content-encoding', 'transfer-encoding']) {
276
+ try { res.removeHeader?.(key) } catch {}
223
277
  }
224
- return originalEnd.call(this, next, ...args)
225
278
  }
226
- const redactedEndReference = res.end
227
- const restore = () => {
228
- active = false
229
- if (res.end === redactedEndReference) res.end = originalEnd
279
+ const safeHeaders = () => ({
280
+ 'content-type': 'application/json; charset=utf-8',
281
+ 'cache-control': 'no-store',
282
+ })
283
+ const complete = (body, rejected = false, callback) => {
284
+ if (done) return res
285
+ done = true
286
+ // On rejection, deliberately retain the buffered wrappers after sending
287
+ // the safe error. A drifting async handler may continue calling write()
288
+ // or end(); restoring the raw methods here would expose those later bytes.
289
+ // The response is already ended and these wrappers swallow late writes.
290
+ if (!rejected) restore()
291
+ stripStagedHeaders()
292
+ if (rejected) {
293
+ // An invalid or oversized response is an internal gateway failure. Do
294
+ // not repeat the untrusted data or send previously staged headers.
295
+ originalWriteHead.call(res, 502, safeHeaders())
296
+ return originalEnd.call(res, JSON.stringify({
297
+ ok: false,
298
+ error: 'remote response could not be safely redacted',
299
+ }), callback)
300
+ }
301
+ if (head) {
302
+ originalWriteHead.call(res, head.status, safeHeaders())
303
+ } else {
304
+ originalWriteHead.call(res, Number.isInteger(res.statusCode) ? res.statusCode : 200, safeHeaders())
305
+ }
306
+ return originalEnd.call(res, body, callback)
230
307
  }
231
- let result
308
+ const append = (chunk, encoding) => {
309
+ if (chunk === undefined || chunk === null) return true
310
+ let value
311
+ try {
312
+ if (Buffer.isBuffer(chunk)) value = chunk
313
+ else if (typeof chunk === 'string') value = Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8')
314
+ else if (chunk instanceof Uint8Array) value = Buffer.from(chunk)
315
+ else return false
316
+ } catch { return false }
317
+ if (value.length > MAX_REMOTE_REDACTION_BYTES - bytes) return false
318
+ bytes += value.length
319
+ chunks.push(value)
320
+ return true
321
+ }
322
+ const failed = () => complete(undefined, true)
323
+ function redactedWriteHead(status, maybeMessage, maybeHeaders) {
324
+ if (done) return this
325
+ const message = typeof maybeMessage === 'string' ? maybeMessage : undefined
326
+ const headers = message === undefined ? maybeMessage : maybeHeaders
327
+ if (head || !Number.isInteger(status) || status < 100 || status > 599 ||
328
+ (headers !== undefined && (!headers || typeof headers !== 'object' || Array.isArray(headers)))) {
329
+ failed()
330
+ return this
331
+ }
332
+ head = { status, message, headers }
333
+ return this
334
+ }
335
+ function redactedFlushHeaders() {
336
+ // Deferring the header is essential: once sent, an invalid JSON body
337
+ // cannot be replaced by a safe 502.
338
+ }
339
+ function redactedWrite(chunk, encoding, callback) {
340
+ const completeWrite = typeof encoding === 'function' ? encoding : callback
341
+ if (!done && !append(chunk, encoding)) failed()
342
+ if (typeof completeWrite === 'function') queueMicrotask(completeWrite)
343
+ return !done
344
+ }
345
+ function redactedEnd(chunk, encoding, callback) {
346
+ if (done) return res
347
+ const completeEnd = typeof encoding === 'function' ? encoding : callback
348
+ if (!append(chunk, encoding)) return failed()
349
+ try {
350
+ const body = JSON.parse(Buffer.concat(chunks, bytes).toString('utf8'))
351
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return failed()
352
+ const redacted = JSON.stringify(redactRemoteBody(path, body))
353
+ if (Buffer.byteLength(redacted, 'utf8') > MAX_REMOTE_REDACTION_BYTES) return failed()
354
+ return complete(redacted, false, completeEnd)
355
+ } catch {
356
+ return failed()
357
+ }
358
+ }
359
+
360
+ res.writeHead = redactedWriteHead
361
+ res.write = redactedWrite
362
+ res.end = redactedEnd
363
+ res.flushHeaders = redactedFlushHeaders
232
364
  try {
233
- result = handler(req, res)
234
- } catch (error) {
235
- restore()
236
- throw error
365
+ const result = handler(req, res)
366
+ if (result && typeof result.then === 'function') {
367
+ return Promise.resolve(result).catch(() => { if (!done) failed() })
368
+ }
369
+ return result
370
+ } catch {
371
+ if (!done) failed()
372
+ return undefined
237
373
  }
238
- if (result && typeof result.then === 'function') return result.finally(restore)
239
- restore()
240
- return result
241
374
  }
242
375
 
243
376
  function guardedRoute(route, getConnection) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain + pixel-level tools. Remote vision providers, including the default free fallback, receive image content unless strict local-only use is configured.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,8 @@
35
35
  "presets"
36
36
  ],
37
37
  "engines": {
38
- "node": "^22.19.0 || >=24.0.0"
38
+ "node": "^22.19.0 || >=24.0.0",
39
+ "dsh": ">=0.1.5-rc.1 <0.2.0-0 || >=0.2.0-rc.2 <0.3.0-0"
39
40
  },
40
41
  "packageManager": "pnpm@11.20.0",
41
42
  "keywords": [
@@ -89,13 +90,13 @@
89
90
  "@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
90
91
  },
91
92
  "scripts": {
92
- "test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.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/cache-atomic-write-concurrency.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/degraded-local-evidence.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/vision-mode-toggle-injection.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/clipboard-image-paste-compat.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/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/release-registry-identity.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/activation-shim-contract.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.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/provider-transport-threading.test.js tests/architecture-2.3-gate.test.js tests/host-settings-seam.test.js tests/drift-canary-settings-seam.test.js tests/esm-cycle-audit.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/local-only-vision.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/vision-diagnostics-routes.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/free-cloud-first.test.js tests/dsh-host-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/issue-512-tool-restriction.test.js tests/issue-606-session-source-ownership.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/provider-probe-proxy-scope.test.js tests/lifecycle-invariants.test.js tests/vision-tool-settings-generation.test.js tests/release-package-content-identity.test.js tests/release-state-machine.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/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/long-ocr-empty-retry.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/session-vision-policy.test.js tests/session-vision-policy-domain.test.js tests/session-vision-delegation.test.js tests/session-vision-delegation-boundary.test.js tests/subagent-vision-inheritance-characterization.test.js tests/subagent-vision-runtime-integration.test.js tests/subagent-vision-cold-resume.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-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 && pnpm test:regression-closure && node --test tests/vision-backend-runtime-policy.test.js tests/vision-router-adversarial-review-fixes.test.js tests/web-panel-delegation.test.js",
93
+ "test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.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/cache-atomic-write-concurrency.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/degraded-local-evidence.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/vision-mode-toggle-injection.test.js tests/dev-staged-build.test.js tests/dev-web-reload.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/clipboard-image-paste-compat.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/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/release-registry-identity.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/activation-shim-contract.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.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/provider-transport-threading.test.js tests/architecture-2.3-gate.test.js tests/host-settings-seam.test.js tests/drift-canary-settings-seam.test.js tests/esm-cycle-audit.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/local-only-vision.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/vision-diagnostics-routes.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/free-cloud-first.test.js tests/dsh-host-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/issue-512-tool-restriction.test.js tests/issue-606-session-source-ownership.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/provider-probe-proxy-scope.test.js tests/lifecycle-invariants.test.js tests/vision-tool-settings-generation.test.js tests/release-package-content-identity.test.js tests/release-state-machine.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/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/long-ocr-empty-retry.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/session-vision-policy.test.js tests/session-vision-policy-domain.test.js tests/session-vision-delegation.test.js tests/session-vision-delegation-boundary.test.js tests/subagent-vision-inheritance-characterization.test.js tests/subagent-vision-runtime-integration.test.js tests/subagent-vision-cold-resume.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-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 && pnpm test:regression-closure && node --test tests/vision-backend-runtime-policy.test.js tests/vision-router-adversarial-review-fixes.test.js tests/web-panel-delegation.test.js",
93
94
  "test:core": "node --test tests/core.test.js tests/degraded-local-evidence.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",
94
95
  "test:routing": "node --test tests/issue-374-delegated-call-config.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/vision-routing-product.test.js tests/vision-capability-router.test.js tests/vision-capability-probe.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",
95
96
  "test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-policy.test.js tests/session-vision-delegation.test.js tests/session-vision-delegation-boundary.test.js tests/subagent-vision-inheritance-characterization.test.js tests/subagent-vision-runtime-integration.test.js tests/subagent-vision-cold-resume.test.js tests/session-vision-policy-domain.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",
96
97
  "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",
97
98
  "test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
98
- "test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.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",
99
+ "test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.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 tests/dev-staged-build.test.js tests/dev-web-reload.test.js",
99
100
  "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/issue-374-delegated-call-config.test.js tests/runtime-boundary-fixes.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/adapter-prepare-call-compat.test.js tests/dsh-host-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",
100
101
  "test:grounding": "node --test tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/vision-capability-grounding-proof-contract.test.js",
101
102
  "test:stress": "node scripts/image-resource-stress.mjs && node --test tests/large-image-resource-integration.test.js tests/resource-retention-lifecycle.test.js",
@@ -109,6 +110,7 @@
109
110
  "client:sync": "node scripts/sync-client-embedded-modules.mjs && pnpm build",
110
111
  "client:check": "node scripts/sync-client-embedded-modules.mjs --check && pnpm build:check",
111
112
  "build": "node scripts/build-source-artifacts.mjs",
113
+ "dev:web": "node scripts/dev-web-reload.mjs",
112
114
  "build:check": "node scripts/build-source-artifacts.mjs --check",
113
115
  "pretest": "pnpm build",
114
116
  "build:reproducible": "node scripts/verify-build-reproducibility.mjs",
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Private, fail-closed bridge for the two supported DSH Web trust generations.
3
+ *
4
+ * It waits for the Host-owned authority source instead of supplying a fake
5
+ * webRuntime service or guessing additional trusted hosts. Its only consumer
6
+ * is the DVR connection row; Host Connection retains all authentication and
7
+ * route ownership.
8
+ */
9
+ export const name = 'vision-router-web-connection-ready'
10
+ export const inject = ['webStartup', 'webServer']
11
+ export const SERVICE = 'visionRouterWebConnectionReady'
12
+
13
+ /** Return a detached, immutable copy of the Host-approved authority list. */
14
+ export function validatedTrustedHosts(value) {
15
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
16
+ throw new Error('Vision Router Web compatibility: Host trust list is invalid')
17
+ }
18
+ return Object.freeze([...value])
19
+ }
20
+
21
+ export function classifyHostWebGeneration(webServer) {
22
+ const protocol = webServer?.protocol
23
+ if (protocol === 'http:' || protocol === 'https:') return 'startup'
24
+ if (protocol === undefined) return 'legacy-runtime'
25
+ throw new Error('Vision Router Web compatibility: unknown Host Web protocol')
26
+ }
27
+
28
+ export function apply(ctx) {
29
+ const generation = classifyHostWebGeneration(ctx.webServer)
30
+ if (generation === 'startup') {
31
+ // 0.2.1-alpha.2+: Web Startup owns invocation trust; Web Runtime no
32
+ // longer provides a webRuntime service. Do not resurrect that service.
33
+ ctx.provide(SERVICE, Object.freeze({
34
+ trustedHosts: validatedTrustedHosts(ctx.webStartup.trustedHosts),
35
+ }))
36
+ return
37
+ }
38
+
39
+ // Older supported Hosts materialize additional bind-derived LAN addresses
40
+ // in their native webRuntime service. Wait for the real service rather than
41
+ // falling back to the earlier webStartup list (which would silently lose
42
+ // LAN authority and race asynchronous Web runtime activation).
43
+ ctx.inject(['webRuntime'], (legacyCtx) => {
44
+ legacyCtx.provide(SERVICE, Object.freeze({
45
+ trustedHosts: validatedTrustedHosts(legacyCtx.webRuntime.trustedHosts),
46
+ }))
47
+ })
48
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "dsh-vision-router-web-connection-ready",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }