dsh-vision-router 3.0.0 → 3.0.2

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.
Files changed (37) hide show
  1. package/README.md +9 -2
  2. package/README.zh.md +7 -2
  3. package/docs/architecture/3.0-r11-adversarial-review.md +11 -5
  4. package/docs/architecture/ci-risk-posture.md +45 -0
  5. package/docs/architecture/dsh-021-alpha1-adaptation.md +46 -0
  6. package/docs/architecture/dsh-support-window.md +8 -0
  7. package/docs/releases/v3.0.1.md +77 -0
  8. package/docs/releases/v3.0.2.md +105 -0
  9. package/index.js +15 -3
  10. package/lib/adapter-ownership.js +51 -0
  11. package/lib/client-presentation-boundary-main.js +113 -50
  12. package/lib/client-presentation-boundary.js +36 -46
  13. package/lib/client.js +2 -1
  14. package/lib/core-primitives.js +29 -1
  15. package/lib/core-vision-surface.js +3 -1
  16. package/lib/desktop-screenshot-tool.js +11 -9
  17. package/lib/diagnostic-redaction.js +61 -12
  18. package/lib/doctor-cli-p0.js +36 -5
  19. package/lib/dsh-support-window.js +4 -1
  20. package/lib/file-logger.js +9 -9
  21. package/lib/image-offload-compat.js +10 -3
  22. package/lib/local-vision-stabilizer.js +10 -7
  23. package/lib/native-image-coexistence.js +11 -1
  24. package/lib/proxy-routing.js +18 -3
  25. package/lib/remote-settings-risk-client-boundary.js +6 -1
  26. package/lib/runtime-composition.js +8 -6
  27. package/lib/session-vision-mode-authority.js +2 -7
  28. package/lib/session-vision-policy.js +9 -4
  29. package/lib/settings-client-017-compat.js +7 -3
  30. package/lib/settings-number-contract.js +3 -1
  31. package/lib/vision-background-benchmark.js +14 -2
  32. package/lib/vision-background-stop-store-core.js +14 -2
  33. package/lib/vision-diagnostics-routes.js +9 -1
  34. package/lib/vision-model-visibility-boundary-main.js +15 -16
  35. package/lib/vision-resilience.js +31 -2
  36. package/lib/vision-tool-runtime-boundary.js +2 -7
  37. package/package.json +2 -2
@@ -24,7 +24,10 @@ export function supportWindowUpgradeAdvice(capabilities = {}) {
24
24
  })
25
25
  }
26
26
 
27
- if (batchAttachments === 'unknown' || maxImageDimension === 'unknown') {
27
+ // Capability evidence is tri-state (`true` / `false` / `'unknown'`). Only an
28
+ // explicit `true` proves the support floor, so a missing field, a wrong-typed
29
+ // value and `'unknown'` all mean "not proven" rather than "capable".
30
+ if (batchAttachments !== true || maxImageDimension !== true) {
28
31
  return Object.freeze({
29
32
  level: 'unknown',
30
33
  code: 'HOST_CURRENT_FLOOR_UNKNOWN',
@@ -4,6 +4,7 @@ import process from 'node:process'
4
4
  import { execFile } from 'node:child_process'
5
5
  import { format as formatArgs, promisify } from 'node:util'
6
6
  import { createRequire } from 'node:module'
7
+ import { redactCredentialShapes } from './diagnostic-redaction.js'
7
8
  import { resolveDshHome } from './doctor.js'
8
9
  import { isLoopbackRequest } from './web-capability-boundary.js'
9
10
 
@@ -32,20 +33,19 @@ export function resolveVisionRouterLogPaths(dshHome = resolveDshHome()) {
32
33
  * Diagnostics are intended to be shareable in bug reports. Existing runtime
33
34
  * messages should not contain secrets, but redact common credential shapes as
34
35
  * defense in depth before anything reaches disk.
36
+ *
37
+ * The shape list lives in one place now: this module used to carry its own
38
+ * subset, which leaked `Authorization: token ghp_…`, `Cookie:` headers, bare
39
+ * platform tokens and `#token=` fragments that the other copy already covered.
35
40
  */
36
41
  export function sanitizeLogText(value) {
37
- let text = String(value ?? '')
38
- text = text.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, 'Bearer [REDACTED]')
39
- text = text.replace(/\bsk-(?:proj-)?[A-Za-z0-9_-]{8,}\b/g, '[REDACTED_KEY]')
40
- text = text.replace(
42
+ // Query strings here keep the historical `key=`/`auth=` spelling; everything
43
+ // else (schemes, cookies, vendor keys, JWTs) comes from the shared pass.
44
+ const text = String(value ?? '').replace(
41
45
  /([?&](?:api[_-]?key|access[_-]?token|token|key|auth)=)[^&\s]+/gi,
42
46
  '$1[REDACTED]',
43
47
  )
44
- text = text.replace(
45
- /\b(authorization|api[_-]?key|access[_-]?token)\s*[:=]\s*["']?[^\s"',}]+/gi,
46
- '$1=[REDACTED]',
47
- )
48
- return text
48
+ return redactCredentialShapes(text)
49
49
  }
50
50
 
51
51
  function positiveInteger(value, fallback) {
@@ -15,8 +15,15 @@ export function isOffloadedImageBlock(block) {
15
15
  const value = objectRecord(block);
16
16
  return value?.type === 'image' && value.offloaded === true;
17
17
  }
18
- export function blocksHaveRetainedImage(content) {
19
- if (!Array.isArray(content))
18
+ /**
19
+ * Deepest content nesting this walker inspects, mirroring the bound
20
+ * `catalog-corrections.convertBlocks` already applies to the same block tree.
21
+ * The walker only needs message → content → nested content; an unbounded
22
+ * recursion would turn a pathological tree into a stack overflow.
23
+ */
24
+ const MAX_CONTENT_NESTING_DEPTH = 4;
25
+ export function blocksHaveRetainedImage(content, depth = 0) {
26
+ if (!Array.isArray(content) || depth > MAX_CONTENT_NESTING_DEPTH)
20
27
  return false;
21
28
  for (const block of content) {
22
29
  const value = objectRecord(block);
@@ -24,7 +31,7 @@ export function blocksHaveRetainedImage(content) {
24
31
  continue;
25
32
  if (value.type === 'image' && value.offloaded !== true)
26
33
  return true;
27
- if (Array.isArray(value.content) && blocksHaveRetainedImage(value.content))
34
+ if (Array.isArray(value.content) && blocksHaveRetainedImage(value.content, depth + 1))
28
35
  return true;
29
36
  }
30
37
  return false;
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'
4
4
  import path from 'node:path'
5
5
  import { promisify } from 'node:util'
6
6
  import { sameOriginRequest } from './adversarial-hardening.js'
7
+ import { objectRecord } from './core-primitives.js'
7
8
  import { normalizeRuntimeVisionConfig } from './runtime-config-normalizer.js'
8
9
  import { isOffloadedImageBlock, offloadedImagePlaceholder } from './image-offload-compat.js'
9
10
 
@@ -14,12 +15,13 @@ import { isOffloadedImageBlock, offloadedImagePlaceholder } from './image-offloa
14
15
  * specific to opt-in local vision, while non-local providers continue through
15
16
  * the original adapters byte-for-byte.
16
17
  */
17
- export async function triggerDesktopScreenshotPermission({
18
- platform = typeof process !== 'undefined' ? process.platform : '',
19
- run = promisify(execFile),
20
- remove = unlink,
21
- tempDir = tmpdir(),
22
- } = {}) {
18
+ export async function triggerDesktopScreenshotPermission(rawOptions) {
19
+ const {
20
+ platform = typeof process !== 'undefined' ? process.platform : '',
21
+ run = promisify(execFile),
22
+ remove = unlink,
23
+ tempDir = tmpdir(),
24
+ } = objectRecord(rawOptions) ?? {}
23
25
  // Windows has no Screen Recording privacy prompt equivalent and Linux
24
26
  // capture permission is compositor-specific. macOS is the platform where
25
27
  // enabling this feature must proactively cross an OS privacy boundary.
@@ -43,7 +45,7 @@ export async function triggerDesktopScreenshotPermission({
43
45
  }
44
46
  }
45
47
 
46
- export function installLocalVisionStabilizer(ctx, config = {}, core) {
48
+ export function installLocalVisionStabilizer(ctx, config = {}, core, installOptions = {}) {
47
49
  config = normalizeRuntimeVisionConfig(config)
48
50
  if (!ctx || typeof ctx !== 'object') return { ctx, bootConfig: config }
49
51
 
@@ -270,6 +272,7 @@ export function installLocalVisionStabilizer(ctx, config = {}, core) {
270
272
  const text = await core.callLocalBackend(local, await localMessages(options), {
271
273
  maxTokens: local.maxTokens ?? 4096,
272
274
  signal: options.signal,
275
+ providerTransport: installOptions?.providerTransport,
273
276
  resolveCredential,
274
277
  })
275
278
  if (text !== '') {
@@ -1,4 +1,10 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import {
3
+ VISION_ROUTER_ADAPTER_OWNER,
4
+ isVisionRouterOwnedAdapter,
5
+ } from './adapter-ownership.js'
6
+
7
+ export { VISION_ROUTER_ADAPTER_OWNER, isVisionRouterOwnedAdapter }
2
8
 
3
9
  /**
4
10
  * Turn-local image ownership used by the public entry composition.
@@ -21,7 +27,8 @@ export const IMAGE_OWNERSHIP = Object.freeze({
21
27
  * an enumerable symbol also survives the rare Object.assign/object-spread
22
28
  * adapter wrapper. Normal JSON/Object.keys surfaces still ignore symbols.
23
29
  */
24
- export const VISION_ROUTER_ADAPTER_OWNER = Symbol.for('dsh-vision-router.adapter-owner')
30
+ // The symbol and the ownership rule are owned by ./adapter-ownership.ts and
31
+ // re-exported here so existing importers keep working unchanged.
25
32
 
26
33
  const imageTurn = new AsyncLocalStorage()
27
34
  const markedAdapters = new WeakMap()
@@ -166,6 +173,9 @@ export function visionRouterAdapterOwner(adapter) {
166
173
  }
167
174
  }
168
175
 
176
+ // isVisionRouterOwnedAdapter is owned by ./adapter-ownership.ts (imported and
177
+ // re-exported at the top of this file) so every caller shares one rule.
178
+
169
179
  function routeList(routes) {
170
180
  const list = Array.isArray(routes) ? routes : [routes]
171
181
  return list
@@ -22,10 +22,25 @@ export function canonicalProxyHost(value) {
22
22
  host = host.slice(0, -1);
23
23
  if (host === '')
24
24
  return '';
25
+ // A `*.host` entry means "this host and its subdomains", which is exactly what the matcher
26
+ // already does for a bare hostname. `domainToASCII` rejects the star, so leaving it in place
27
+ // makes the entry equal no real hostname: the configured host silently proxies nothing.
28
+ if (host.startsWith('*.'))
29
+ host = host.slice(2);
30
+ if (host === '')
31
+ return '';
25
32
  // WHATWG URL.hostname keeps IPv6 literals bracketed. domainToASCII does not
26
- // accept a bare IPv6 literal, so preserve the bracketed spelling and only
27
- // case-fold it. Proxy hosts are otherwise DNS names and can be IDNA-folded.
28
- if (host.startsWith('[') && host.endsWith(']'))
33
+ // accept an IPv6 literal at all, so case-fold it directly. The brackets are
34
+ // pure spelling for the same address: stripping them is what lets a configured
35
+ // `::1` match a URL-derived `[::1]`. A bracketed non-literal (say
36
+ // `[example.com]`) keeps its spelling, so nothing about DNS is widened.
37
+ if (host.startsWith('[') && host.endsWith(']')) {
38
+ const literal = host.slice(1, -1).trim();
39
+ if (literal === '')
40
+ return '';
41
+ return literal.includes(':') ? literal.toLowerCase() : host.toLowerCase();
42
+ }
43
+ if (host.includes(':'))
29
44
  return host.toLowerCase();
30
45
  const ascii = domainToASCII(host);
31
46
  return (ascii || host).toLowerCase();
@@ -4,8 +4,13 @@
4
4
  *
5
5
  * PRODUCT CONTRACT (2.3+): acceptedRisk is reminder/friction only. It is not
6
6
  * authentication, authorization, local-presence proof, or a credential.
7
+ *
8
+ * This function is serialized with `toString()` into the browser boundary source
9
+ * below, so its body must stay self-contained: no imported helper and no free
10
+ * identifier may appear inside it.
7
11
  */
8
- export function createRemoteSettingsRiskClientBoundary(options = {}) {
12
+ export function createRemoteSettingsRiskClientBoundary(rawOptions) {
13
+ var options = rawOptions !== null && typeof rawOptions === 'object' ? rawOptions : {}
9
14
  var confirmImpl = options.confirmImpl
10
15
  var alertImpl = options.alertImpl
11
16
  var locale = options.locale
@@ -98,7 +98,7 @@ function legacyCoreVisionSettings(ctx, fallback) {
98
98
  return { ...current, autoActivateOnImage: false }
99
99
  }
100
100
 
101
- function installHostAndSecurityBoundaries(ctx, config, core) {
101
+ function installHostAndSecurityBoundaries(ctx, config, core, providerTransport) {
102
102
  // SessionVisionIndex calls planGuardStopShadows before Core.apply. Keep that
103
103
  // one helper behind the live-locale facade; the final Core itself still gets
104
104
  // the ordinary composition identity and is localized by the scoped context.
@@ -144,6 +144,7 @@ function installHostAndSecurityBoundaries(ctx, config, core) {
144
144
  ollamaColdStartCtx,
145
145
  hardenedConfig,
146
146
  core,
147
+ { providerTransport },
147
148
  )
148
149
 
149
150
  const routingProduct = resolveVisionRoutingProduct(bootConfig)
@@ -260,7 +261,7 @@ function createRuntimeOwners(host, core) {
260
261
  }
261
262
  }
262
263
 
263
- function installRoutingAndHostProducts(host, owners, core) {
264
+ function installRoutingAndHostProducts(host, owners, core, providerTransport) {
264
265
  const { capabilityStore, logging, runtimeConfig, runtimePerformanceStore } = host
265
266
  const { sessionVisionModeCompat } = owners
266
267
  // Final structured-flow guard sits closest to core.apply so it sees the
@@ -281,7 +282,7 @@ function installRoutingAndHostProducts(host, owners, core) {
281
282
  runtimeConfig,
282
283
  core,
283
284
  capabilityStore,
284
- { logger: logging.logger },
285
+ { logger: logging.logger, providerTransport },
285
286
  )
286
287
  const sessionTurnResolver = createSessionTurnResolver(backgroundProfiling.ctx)
287
288
  const sessionEventTailReader = createSessionEventTailReader(backgroundProfiling.ctx)
@@ -479,9 +480,10 @@ function applyMatureCoreBridge(host, owners, routing, execution, core, runtime)
479
480
  }
480
481
 
481
482
  export function applyVisionRuntimeComposition(ctx, config = {}, core, runtime = {}) {
482
- const host = installHostAndSecurityBoundaries(ctx, config, core)
483
+ const providerTransport = runtime?.providerTransport
484
+ const host = installHostAndSecurityBoundaries(ctx, config, core, providerTransport)
483
485
  const owners = createRuntimeOwners(host, core)
484
- const routing = installRoutingAndHostProducts(host, owners, core)
485
- const execution = installExecutionBoundaries(host, owners, routing, core, runtime?.providerTransport)
486
+ const routing = installRoutingAndHostProducts(host, owners, core, providerTransport)
487
+ const execution = installExecutionBoundaries(host, owners, routing, core, providerTransport)
486
488
  return applyMatureCoreBridge(host, owners, routing, execution, core, runtime)
487
489
  }
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { isVisionRouterOwnedAdapter } from './adapter-ownership.js';
2
3
  import { parseSessionVisionPolicy } from './session-vision-policy.js';
3
- const VISION_ROUTER_ADAPTER_OWNER = Symbol.for('dsh-vision-router.adapter-owner');
4
4
  const VISION_ROUTER_OWNERSHIP = 'vision-router-owned';
5
5
  const DEFAULT_WRAPPER_ROUTE = 'deepseek-vision';
6
6
  const DEFAULT_CHAIN_ROUTE = 'vision-chain';
@@ -139,12 +139,7 @@ function currentAdapter(ctx, provider) {
139
139
  function adapterOwnedByVisionRouter(adapter) {
140
140
  if (!isWeakKey(adapter))
141
141
  return false;
142
- try {
143
- return objectRecord(adapter)?.[VISION_ROUTER_ADAPTER_OWNER] !== undefined;
144
- }
145
- catch {
146
- return false;
147
- }
142
+ return isVisionRouterOwnedAdapter(adapter);
148
143
  }
149
144
  function sameRoute(left, right) {
150
145
  const rightRoute = routeFrom(right);
@@ -12,12 +12,15 @@ export function parseSessionVisionPolicy(value) {
12
12
  if (value === null || typeof value !== 'object' || Array.isArray(value))
13
13
  return undefined;
14
14
  const record = value;
15
- if (record.revision !== SESSION_VISION_POLICY_REVISION)
15
+ // Own keys only: a policy row is persisted JSON, so a value inherited from a
16
+ // prototype is evidence of a malformed row rather than of a policy.
17
+ if (!Object.hasOwn(record, 'revision') || record.revision !== SESSION_VISION_POLICY_REVISION)
16
18
  return undefined;
17
- if (typeof record.enabled !== 'boolean')
19
+ if (!Object.hasOwn(record, 'enabled') || typeof record.enabled !== 'boolean')
18
20
  return undefined;
19
- if (record.source !== 'user' && record.source !== 'delegation')
21
+ if (!Object.hasOwn(record, 'source') || (record.source !== 'user' && record.source !== 'delegation')) {
20
22
  return undefined;
23
+ }
21
24
  if (record.source === 'user') {
22
25
  if (record.inheritedFrom !== undefined)
23
26
  return undefined;
@@ -27,7 +30,9 @@ export function parseSessionVisionPolicy(value) {
27
30
  source: 'user',
28
31
  });
29
32
  }
30
- const inheritedFrom = nonEmptyString(record.inheritedFrom);
33
+ const inheritedFrom = Object.hasOwn(record, 'inheritedFrom')
34
+ ? nonEmptyString(record.inheritedFrom)
35
+ : undefined;
31
36
  if (inheritedFrom === undefined)
32
37
  return undefined;
33
38
  return Object.freeze({
@@ -197,13 +197,17 @@ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
197
197
  get: function(target, property) {
198
198
  if (property === 'settingsScope') {
199
199
  var original = safeGet(target, 'settingsScope');
200
- if (original && typeof original.bind === 'function') return original;
201
200
  // DSH 0.1.7 configForms only mirrors Config fields declared volatile.
202
201
  // DVR must keep its public Config ordinary for the older supported Host
203
202
  // window, so its namespace intentionally uses the local-only ConfigEditor
204
- // bridge while other namespaces may still delegate to native configForms.
203
+ // bridge. Even when a Host exposes a legacy-shaped settingsScope, its
204
+ // vision-router view may be empty; keep it only as the delegate for
205
+ // every other namespace instead of letting it bypass the local bridge.
205
206
  var official = configFormsBinder(target);
206
- return syntheticBinder(ctx, official);
207
+ return syntheticBinder(
208
+ ctx,
209
+ original && typeof original.bind === 'function' ? original : official
210
+ );
207
211
  }
208
212
  if (property === 'get') {
209
213
  var getter = Reflect.get(target, property, target);
@@ -10,7 +10,9 @@ export const SETTINGS_NUMBER_META = Object.freeze({
10
10
  })
11
11
 
12
12
  export function parseSettingsNumber(key, raw, { allowClear = false } = {}) {
13
- const meta = SETTINGS_NUMBER_META[key]
13
+ // Own keys only: a bare lookup would resolve inherited names such as
14
+ // `__proto__` or `constructor` to prototype machinery instead of "unknown key".
15
+ const meta = Object.hasOwn(SETTINGS_NUMBER_META, key) ? SETTINGS_NUMBER_META[key] : undefined
14
16
  if (!meta) return undefined
15
17
  if (raw === '' || raw === null || raw === undefined) return allowClear ? { clear: true } : undefined
16
18
  const value = Number(raw)
@@ -758,7 +758,7 @@ export function createBackgroundCapabilityProfiler({
758
758
  }, interval)
759
759
  try { policyTimer?.unref?.() } catch {}
760
760
 
761
- const runner = runAxisBenchmark ?? ((args) => runAxis({ ...args, options: invokerOptions }))
761
+ const runner = runAxisBenchmark ?? runAxis
762
762
  await runner({
763
763
  ctx,
764
764
  config: current,
@@ -768,6 +768,7 @@ export function createBackgroundCapabilityProfiler({
768
768
  axis: work.axis,
769
769
  signal: controller.signal,
770
770
  logger,
771
+ options: invokerOptions,
771
772
  onProgress(progress = {}) {
772
773
  if (!runningWork || runningWork.candidate.key !== work.candidate.key || runningWork.axis !== work.axis) return
773
774
  const completed = Math.max(0, Number(progress.completed) || 0)
@@ -999,6 +1000,16 @@ export function installBackgroundCapabilityProfiling(ctx, config, core, store, o
999
1000
  const logger = options.logger ?? ctx?.logger
1000
1001
  const imageVerdictStore = options.imageVerdictStore ?? createImageInputVerdictStore({ logger })
1001
1002
  const backgroundStopStore = options.backgroundStopStore ?? createBackgroundBenchmarkStopStore({ logger })
1003
+ // Provider transport is one explicit runtime capability shared by automatic
1004
+ // profiling and the user-triggered exact check. Top-level ownership wins so
1005
+ // a stale nested test/customization bag cannot silently bypass live proxy
1006
+ // policy after composition or HMR changes.
1007
+ const invokerOptions = {
1008
+ ...(options.invokerOptions ?? {}),
1009
+ ...(options.providerTransport === undefined
1010
+ ? {}
1011
+ : { providerTransport: options.providerTransport }),
1012
+ }
1002
1013
  const profiler = createBackgroundCapabilityProfiler({
1003
1014
  ctx,
1004
1015
  config,
@@ -1006,6 +1017,7 @@ export function installBackgroundCapabilityProfiling(ctx, config, core, store, o
1006
1017
  store,
1007
1018
  logger,
1008
1019
  ...options,
1020
+ invokerOptions,
1009
1021
  imageVerdictStore,
1010
1022
  backgroundStopStore,
1011
1023
  })
@@ -1120,7 +1132,7 @@ export function installBackgroundCapabilityProfiling(ctx, config, core, store, o
1120
1132
  provider: body.provider,
1121
1133
  model: body.model,
1122
1134
  signal: AbortSignal.timeout(VISION_CHECK_TIMEOUT_MS),
1123
- invokerOptions: options.invokerOptions ?? {},
1135
+ invokerOptions,
1124
1136
  })
1125
1137
  await profiler.recordImageSupported(body.provider, body.model)
1126
1138
  sendJson(res, 200, result)
@@ -105,8 +105,20 @@ async function load(file, fsOps, at) {
105
105
  return new Map();
106
106
  }
107
107
  const records = retainedStopMap(stops, at);
108
- if (version === 2)
109
- await save(file, records, fsOps, at);
108
+ // Rewrite whenever loading changed the persisted set: v2 stored raw entries
109
+ // that this version must drop, and any later retention rule (category,
110
+ // expiry, fingerprint shape) has to reach the file the same way instead of
111
+ // leaving a stale record that only the runtime filter happens to hide.
112
+ if (version === 2 || records.size !== stops.length) {
113
+ try {
114
+ await save(file, records, fsOps, at);
115
+ }
116
+ catch {
117
+ // This rewrite is opportunistic: a read-only or full disk must not discard the records
118
+ // the load could still read (the catch below returns an empty map). The store's own
119
+ // persist() retries the write and reports it through the logger.
120
+ }
121
+ }
110
122
  return records;
111
123
  }
112
124
  catch (error) {
@@ -106,7 +106,15 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
106
106
  // inside the adapter's proxy scope) do not have. Use the fetch the boundary
107
107
  // currently owns, with the module-load capture as the fallback.
108
108
  const injectedFetch = typeof options.fetchImpl === 'function' ? options.fetchImpl : undefined
109
- const resolveFetch = () => injectedFetch ?? currentLegacyGlobalProxyFetch() ?? moduleFetch
109
+ // Probes must describe the egress real vision turns use. That egress is the
110
+ // Router-owned provider transport, which also owns the proxy/proxyHosts decision;
111
+ // the legacy pair scope below only covers non-router-owned providers, so a
112
+ // vision-http direct backend would otherwise probe with ambient fetch.
113
+ const transport = options.transport
114
+ const transportFetch = typeof transport?.fetch === 'function'
115
+ ? (input, init) => transport.fetch(input, init, { allowProxy: true })
116
+ : undefined
117
+ const resolveFetch = () => injectedFetch ?? transportFetch ?? currentLegacyGlobalProxyFetch() ?? moduleFetch
110
118
  const connectionConfig = () => {
111
119
  try {
112
120
  const live = ctx?.get?.('settings')?.get?.('vision-router')
@@ -419,6 +419,7 @@ export const VISION_MODEL_VISIBILITY_PRELUDE = String.raw`(function(){
419
419
  function create() {
420
420
  var result = originalCreate.apply(this, arguments);
421
421
  patchLoader(loader);
422
+ if (result && result !== loader) patchLoader(result);
422
423
  return result;
423
424
  }
424
425
  try { Object.defineProperty(create, '__visionRouterModelVisibility', { value: true }); } catch (_) {}
@@ -427,31 +428,29 @@ export const VISION_MODEL_VISIBILITY_PRELUDE = String.raw`(function(){
427
428
  }
428
429
 
429
430
  function install() {
430
- if (window.__ModuleLoader__) {
431
- patchLoader(window.__ModuleLoader__);
432
- return;
433
- }
434
431
  var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
432
+ var current;
433
+ try { current = window.__ModuleLoader__; } catch (_) { current = undefined; }
434
+ if (current) patchLoader(current);
435
435
  if (descriptor && descriptor.configurable === false) return;
436
- var stored;
436
+ var previousGet = descriptor && descriptor.get;
437
+ var previousSet = descriptor && descriptor.set;
438
+ var stored = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
439
+ ? descriptor.value
440
+ : current;
437
441
  Object.defineProperty(window, '__ModuleLoader__', {
438
442
  configurable: true,
439
- enumerable: true,
440
- get: function(){ return stored; },
443
+ enumerable: !descriptor || descriptor.enumerable !== false,
444
+ get: function(){ return previousGet ? previousGet.call(window) : stored; },
441
445
  set: function(value) {
442
- stored = value;
443
- patchLoader(value);
444
- Object.defineProperty(window, '__ModuleLoader__', {
445
- configurable: true,
446
- enumerable: true,
447
- writable: true,
448
- value: stored
449
- });
446
+ if (previousSet) previousSet.call(window, value); else stored = value;
447
+ try { patchLoader(previousGet ? previousGet.call(window) : value); } catch (_) {}
450
448
  }
451
449
  });
450
+ if (stored) patchLoader(stored);
452
451
  }
453
452
 
454
- install();
453
+ try { install(); } catch (_) {}
455
454
  })();`
456
455
 
457
456
  export function injectVisionModelVisibilityBoundary(html) {
@@ -109,6 +109,35 @@ export function kindForHttpStatus(status) {
109
109
  return undefined
110
110
  }
111
111
 
112
+ /** Read one field without letting a hostile getter escape the classifier. */
113
+ function safeFailureField(error, name) {
114
+ try {
115
+ return error === null || error === undefined ? undefined : error[name]
116
+ } catch {
117
+ return undefined
118
+ }
119
+ }
120
+
121
+ /** Read the human-readable text; a throwing toString() must not escape either. */
122
+ function safeFailureText(error) {
123
+ try {
124
+ return String(safeFailureField(error, 'message') ?? error ?? '')
125
+ } catch {
126
+ return ''
127
+ }
128
+ }
129
+
130
+ /** Map a branded error code to a kind using own keys only (no prototype hits). */
131
+ function kindForCode(code) {
132
+ let key
133
+ try {
134
+ key = code === null || code === undefined ? '' : String(code)
135
+ } catch {
136
+ key = ''
137
+ }
138
+ return Object.hasOwn(CODE_KIND_MAP, key) ? CODE_KIND_MAP[key] : undefined
139
+ }
140
+
112
141
  /**
113
142
  * Classify a backend failure into the shared taxonomy plus a provider-retry
114
143
  * verdict. Deterministic failures (AUTH / INVALID_REQUEST / REGION / TOS)
@@ -117,8 +146,8 @@ export function kindForHttpStatus(status) {
117
146
  * the task deadline still has budget.
118
147
  */
119
148
  export function classifyVisionFailure(error) {
120
- const message = String((error && error.message) ?? error ?? '')
121
- let kind = CODE_KIND_MAP[(error && error.code) ?? ''] ?? kindForHttpStatus(error && error.status)
149
+ const message = safeFailureText(error)
150
+ let kind = kindForCode(safeFailureField(error, 'code')) ?? kindForHttpStatus(safeFailureField(error, 'status'))
122
151
  if (kind === undefined) {
123
152
  for (const [candidate, patterns] of KIND_BY_PATTERN) {
124
153
  if (patterns.some((pattern) => pattern.test(message))) {
@@ -11,6 +11,7 @@ import {
11
11
  LiveDescribeCache,
12
12
  } from './runtime-reliability.js'
13
13
  import { projectDelegatedCallConfig } from './delegated-call-config.js'
14
+ import { isVisionRouterOwnedAdapter } from './adapter-ownership.js'
14
15
  import { VISION_RESULT_CODES } from './vision-resilience.js'
15
16
 
16
17
  const toolRuntime = new AsyncLocalStorage()
@@ -26,7 +27,6 @@ const wrappedLlms = new WeakMap()
26
27
  const wrappedAdapters = new WeakMap()
27
28
  const wrappedDelegatingAdapters = new WeakMap()
28
29
  const wrappedTransparentAdapters = new WeakMap()
29
- const VISION_ROUTER_ADAPTER_OWNER = Symbol.for('dsh-vision-router.adapter-owner')
30
30
  const MAX_TRANSPARENT_REASONING_MEMORY = 512
31
31
  const VISION_FAILURE_RESULT_CODES = new Set(Object.values(VISION_RESULT_CODES))
32
32
 
@@ -434,12 +434,7 @@ function visionChainAdapter(adapter, routes) {
434
434
  }
435
435
 
436
436
  function visionRouterOwnedAdapter(adapter) {
437
- if (!adapter || (typeof adapter !== 'object' && typeof adapter !== 'function')) return false
438
- try {
439
- return adapter[VISION_ROUTER_ADAPTER_OWNER] !== undefined
440
- } catch {
441
- return false
442
- }
437
+ return isVisionRouterOwnedAdapter(adapter)
443
438
  }
444
439
 
445
440
  function explicitReasoningEffort(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
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": {
@@ -89,7 +89,7 @@
89
89
  "@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
90
90
  },
91
91
  "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/architecture-2.3-gate.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",
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
93
  "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
94
  "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
95
  "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",