dsh-vision-router 2.2.6 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -436,7 +436,7 @@ ollama pull qwen2.5vl
436
436
  ## Requirements
437
437
 
438
438
  - DeepSeek Harness Web profile. Normal installs can use `npx @deepseek-ai/dsh ...`; source checkouts use `pnpm dsh ...`. A bare `dsh ...` command only works when the CLI is already on your shell `PATH`.
439
- - **DSH Host support policy:** DVR 2.2.x keeps DSH `0.1.0-rc.8` as the public minimum and currently supports the released stable channel through `0.1.5-rc.3`. The DSH `0.2.x` train is forward-admitted (`^0.2.0`) after exact pre-release master, real Host, Windows Desktop, multi-plugin isolation, and packaged-ASAR validation; the final immutable `0.2.0` tag and signed Desktop artifact will be revalidated when upstream publishes them. Exact `0.1.7-rc.2` (`next`) coverage remains verification evidence. DVR 2.0.x was the final train with public support for rc.6/rc.7. See [DSH Host support window](docs/architecture/dsh-support-window.md).
439
+ - **DSH Host support policy:** DVR 2.2.x keeps DSH `0.1.0-rc.8` as the public minimum and currently supports the released stable channel through `0.1.5-rc.3`. The DSH `0.2.x` train is peer-admitted from the verified `0.2.0-rc.1` boundary (`>=0.2.0-rc.1 <0.3.0-0`); exact `0.2.0-rc.1` (`next`) source, real Host, Windows Desktop, multi-plugin, Settings, mixed-attachment, bundle-recompose, and Host-Sharp gates are preview verification evidence, not a support-floor increase. The final stable `0.2.0` tag and signed Desktop artifact will still be revalidated when published. DVR 2.0.x was the final train with public support for rc.6/rc.7. See [DSH Host support window](docs/architecture/dsh-support-window.md).
440
440
  - Node ≥ 22 (host side).
441
441
  - No API key for the default free chain; a credential reference (`apiKeyEnv`) only for paid `httpProviders`.
442
442
  - Chrome / Chromium / Edge is needed only for `vision_html_screenshot`; every other tool works without a browser.
package/README.zh.md CHANGED
@@ -434,7 +434,7 @@ ollama pull qwen2.5vl
434
434
  ## 环境要求
435
435
 
436
436
  - DeepSeek Harness 的 Web profile。普通安装可用 `npx @deepseek-ai/dsh ...`;从源码仓库运行时用 `pnpm dsh ...`。只有 CLI 已经进入系统 `PATH` 时才能直接写 `dsh ...`。
437
- - **DSH Host 支持策略:** DVR 2.2.x 的公开最低 Host 仍为 DSH `0.1.0-rc.8`,当前正式发布通道已验证并支持到 `0.1.5-rc.3`。DSH `0.2.x` 版本线已在完成精确预发布 master、真实 Host、Windows Desktop、多插件隔离及打包后 ASAR 验证后提前准入(`^0.2.0`);上游正式发布不可变 `0.2.0` tag 和签名 Desktop 安装包后仍会立即复验。对 `0.1.7-rc.2`(`next`)的精确覆盖继续作为验证证据。DVR 2.0.x 是最后公开支持 rc.6/rc.7 的版本线。详见 [DSH Host 支持窗口](docs/architecture/dsh-support-window.md)。
437
+ - **DSH Host 支持策略:** DVR 2.2.x 的公开最低 Host 仍为 DSH `0.1.0-rc.8`,当前稳定通道已验证并支持到 `0.1.5-rc.3`。DSH `0.2.x` 从已验证的 `0.2.0-rc.1` 边界开始准入(`>=0.2.0-rc.1 <0.3.0-0`);精确 `0.2.0-rc.1`(`next`)源码、真实 Host、Windows Desktop、多插件、Settings、混合附件、bundle 重组及 Host-Sharp 门禁仅作为预览验证证据,不抬高公开支持下限。上游正式发布稳定版 `0.2.0` tag 和签名 Desktop 安装包后仍会立即复验。DVR 2.0.x 是最后公开支持 rc.6/rc.7 的版本线。详见 [DSH Host 支持窗口](docs/architecture/dsh-support-window.md)。
438
438
  - Node ≥ 22(宿主侧)。
439
439
  - 默认免费链路无需 API Key;付费 `httpProviders` 只需一个凭据引用(`apiKeyEnv`)。
440
440
  - 只有 `vision_html_screenshot` 需要 Chrome / Chromium / Edge;其余工具无浏览器也能用。
@@ -11,9 +11,10 @@ The matrix is capability-based. Runtime code must feature-detect the seam it nee
11
11
  | `minimum-contract` | `0.1.0-rc.6` | Historical compatibility-retention fixture. The name is legacy; DVR 2.1.x and later 2.x public support starts at rc.8. |
12
12
  | `legacy-contract` | `0.1.0-rc.8` | Public support-floor fixture carrying batch attachments and dimension policy. |
13
13
  | `current-contract` | `0.1.5-rc.3` | Current stable Host contract baseline. Must remain green. |
14
- | `preview-contract` / exact preview gates | `0.1.7-rc.2` (`next`) | Required verification evidence only; not a public preview-support claim. |
14
+ | `preview-contract` | `0.1.7-rc.2` | Retained predecessor-preview regression fixture. |
15
+ | `DSH 0.2.0-rc.1 validation` | `0.2.0-rc.1` (`next`) | Current exact next/rc evidence; not a public preview-support claim. |
15
16
  | retained preview regression | `0.1.5-alpha.2` | Older preview regression fixture retained as an older preview regression while the 0.1.7 next line is qualified. |
16
- | `latest-dsh` / alpha canaries | resolved dynamically from npm dist-tags | Scheduled drift surveillance only. Never changes support policy by itself. |
17
+ | release-channel canary | npm `latest` / `next` / `alpha` | Separately labelled stable, next/rc, and alpha/pre-release drift surveillance. Never changes support policy by itself. |
17
18
 
18
19
  Node 22 and Node 24 remain the general required runtime matrix. The Host contract jobs are additive; they do not replace the normal test matrix.
19
20
 
@@ -38,7 +39,7 @@ Node 22 and Node 24 remain the general required runtime matrix. The Host contrac
38
39
  | Public entry boot | yes | yes | yes | packed plugin public entry import in each Host contract fixture. |
39
40
  | Packaged tarball install | yes | yes | yes | each Host contract fixture packs the plugin then installs the tarball into an isolated Host package. |
40
41
 
41
- Preview-only evidence: the `0.1.7-rc.2` (`next`) contract fixture additionally mounts the Host-owned `@deepseek-ai/dsh-compaction-image-offload` projection and proves that one offloaded image occurrence stays offloaded across live requests, `SessionStore.fork()`, JSONL process restart, and cold resume, while a new occurrence of the same durable attachment remains a retained image. This is verification evidence for the preview contract only; it does not expand the public support window.
42
+ Preview-only evidence: the retained `0.1.7-rc.2` contract fixture additionally mounts the Host-owned `@deepseek-ai/dsh-compaction-image-offload` projection and proves that one offloaded image occurrence stays offloaded across live requests, `SessionStore.fork()`, JSONL process restart, and cold resume. The exact `0.2.0-rc.1` gate separately proves immutable source contracts, official peer evaluation, Host proxy authority, WebServer overlay ownership, real Host + Chromium Vision/mixed-attachment/Settings/bundle lifecycles, and Windows Node 22/24 Desktop plus the Node 24 multi-plugin adversary. Both are verification evidence only; neither expands the public support window.
42
43
 
43
44
  ## Compatibility inventory and exit criteria
44
45
 
@@ -19,9 +19,9 @@ No later support-floor increase is currently announced.
19
19
 
20
20
  ### DSH 0.2.x forward admission
21
21
 
22
- DVR 2.2.x peer-admits the DSH `0.2.x` train (`^0.2.0`) without raising the `0.1.0-rc.8` minimum Host floor. This forward admission is backed by the public pre-0.2.0 master at `21638c56315ae6a2b552d6091945d3144c9af32e`: three-OS source contracts, real Host + Chromium, Windows Node 22/24 Desktop authentication, the Node 24 multi-plugin isolation adversary, and an unsigned Windows Desktop built through the upstream release packager all passed. The packaged audit also exercised DVR from `resources/app.asar/dsh`, including authenticated index/API requests, DVR RPC, structured bootstrap injection, and foreign WebServer registrar isolation.
22
+ DVR 2.2.x peer-admits the DSH `0.2.x` train from the verified rc.1 boundary (`>=0.2.0-rc.1 <0.3.0-0`) without raising the `0.1.0-rc.8` minimum Host floor. Initial forward admission was backed by public pre-0.2.0 master `21638c56315ae6a2b552d6091945d3144c9af32e`. Exact release evidence has now advanced to immutable `dsh-v0.2.0-rc.1` commit `4878cdabd87d4041bdaff61d04c966883b9fd07a`, published on 2026-09-28. Required gates cover three-OS source contracts, real Host + Chromium, Settings mount/save/reload/readback, mixed attachments, bundle recomposition, Windows Node 22/24 Desktop authentication, the Node 24 multi-plugin isolation adversary, and Host-owned Sharp resolution.
23
23
 
24
- The immutable upstream `0.2.0` tag and signed official Desktop installer did not yet exist when this admission was added. They remain release-time revalidation targets; this forward admission prevents DSH's Host peer gate from rejecting otherwise-compatible DVR installs during that release transition.
24
+ The final immutable upstream `0.2.0` stable tag and signed official Desktop installer remain release-time revalidation targets. The rc.1 evidence is next-channel verification, not a preview support promise and not a support-floor increase.
25
25
 
26
26
  ## Verification evidence — not support policy
27
27
 
@@ -30,9 +30,10 @@ Compatibility evidence answers a different question: what exact upstream release
30
30
  | Evidence role | DSH source | Meaning |
31
31
  |---|---|---|
32
32
  | Exact stable evidence | `0.1.5-rc.3` | Required Host/wire and real Host + Chromium coverage for the current stable release. |
33
- | Exact preview evidence | `0.1.7-rc.2` (`next`) | Required preview Host/wire/lifecycle/browser evidence. This is not a preview support promise. |
33
+ | Exact next/rc evidence | `0.2.0-rc.1` (`next`) | Required Host/wire/lifecycle/browser evidence. This is not a preview support promise. |
34
34
  | Stable drift canary | npm dist-tag `latest` | Scheduled, dynamically resolved surveillance. A failure starts compatibility investigation; it does not rewrite support policy. |
35
- | Preview drift canary | npm dist-tag `alpha` | Scheduled, dynamically resolved surveillance with preview-specific lifecycle coverage. A failure does not rewrite support policy. |
35
+ | Next/rc drift canary | npm dist-tag `next` | Scheduled, dynamically resolved surveillance for the rc channel. A failure does not rewrite support policy. |
36
+ | Alpha/pre-release drift canary | npm dist-tag `alpha` | Scheduled, dynamically resolved surveillance for the alpha channel. A failure does not rewrite support policy. |
36
37
 
37
38
  The exact evidence values may move in a patch-level maintenance PR when CI proof advances. The public minimum may move only under the support-floor protocol below.
38
39
 
@@ -50,7 +50,7 @@ H1 is complete as an egress contract, not as a new production dependency. Exact
50
50
  4. The same contract runs against the exact current stable Host and exact preview evidence on Linux, macOS, and Windows through the existing source-contract matrix.
51
51
  5. Production code contains no `@deepseek-ai/dsh-http-proxy` dependency or import. `proxyRouteFor()` is used only by the exact-source test as an oracle for the Host decision.
52
52
 
53
- The current stable evidence advances to DSH `0.1.5-rc.3`; `0.1.5-rc.1` / `0.1.5-rc.2` remain explicitly peer-admitted, exact next-channel evidence is `0.1.7-rc.2`, and the public minimum remains `0.1.0-rc.8`.
53
+ The current stable evidence remains DSH `0.1.5-rc.3`; `0.1.5-rc.1` / `0.1.5-rc.2` remain explicitly peer-admitted, exact next/rc evidence is `0.2.0-rc.1`, and the public minimum remains `0.1.0-rc.8`.
54
54
 
55
55
  ## H2 — Scope Host-owned override authority
56
56
 
@@ -0,0 +1,22 @@
1
+ # v2.2.7
2
+
3
+ DSH Vision Router 2.2.7 is a reliability, Desktop compatibility, and security hotfix. It restores the official Desktop Settings/local-capability path, repairs Electron-local Tesseract execution, hardens artifact/cache publication under symlinked workspaces and overlapping writers, and picks up the upstream Undici 7.29.1 security release without changing the public DSH support floor.
4
+
5
+ ## Highlights
6
+
7
+ - **Official Desktop origin fix (#589):** `dsh-app://app` is now recognized consistently as the exact DSH-owned local Desktop origin across Settings, Settings IA, local diagnostics, exact-check, screenshot, and 0.1.7 compatibility surfaces. Lookalike origins remain remote, and server-side local-only boundaries are unchanged.
8
+ - **Electron local OCR fix (#590):** the Tesseract `execFile` compatibility layer now synchronizes Node CJS/ESM builtin exports symmetrically during install, rollback, and teardown. This fixes Windows Electron cases where forced Tesseract received the literal `stdin` path or Auto OCR silently fell back to a vision model.
9
+ - **Artifact path identity fix (#594):** artifact containment and atomic writes still use canonical real paths, while successful publication returns the caller-visible workspace path. Symlinked workspaces such as macOS `/tmp -> /private/tmp` no longer make a valid artifact appear to escape its session workspace.
10
+ - **Collision-resistant cache publication (#594):** live-model, capability-profile, background-stop, and image-input-verdict caches now use unique atomic temporary paths. Independent writers in the same process and millisecond can no longer collide on one temp file and fail `rename()` with `ENOENT`.
11
+ - **Undici 7.29.1 security update (#593):** the runtime picks up the upstream 7.29.1 security release, including fixes around connection-option preservation, WebSocket failure handling, shared-cache safety, decompression bounds, retry/body settlement, and response framing. This dependency hardening does not imply every upstream advisory was reachable through Vision Router itself.
12
+ - **DSH 0.2 validation/cache hardening (#588):** canonical compatibility evidence now reflects the validated `0.2.0-rc.1` train, and DSH build-cache manifests include required `lib` outputs so incomplete archives fail closed instead of reaching runtime smoke with missing build artifacts.
13
+
14
+ ## Validation
15
+
16
+ - PR #594 completed 75 repository checks with 0 failures, including Node 22/24, Windows/macOS/Linux, artifact-boundary 22/24, DSH minimum/current/preview contracts, exact-source, real Host + Chromium, Windows Desktop Host, routing parity, Host Sharp, hostile-input fuzz, dependency review, and CodeQL.
17
+ - Local final hotfix validation on the Undici 7.29.1 base: main suite `1520` passed, `1` intentional skip, `0` failed; backend runtime-policy suite passed `9/9`; targeted artifact/cache/live-model/capability/background lifecycle coverage passed `43/43`.
18
+ - The public minimum supported DSH Host remains `0.1.0-rc.8`; existing 2.2.x settings require no migration.
19
+
20
+ ## Upgrade
21
+
22
+ 2.2.6 users should upgrade to 2.2.7, especially when using the official Desktop app, local Tesseract OCR, symlinked/custom workspaces, or long-running installations that may overlap cache writes. No configuration changes are required.
@@ -106,7 +106,8 @@ async function resolveArtifactTargetForRun(workspace, artifactsDir, relativePath
106
106
  const lstatImpl = deps.lstat ?? lstat
107
107
 
108
108
  throwIfVisionAborted()
109
- const workspaceReal = await realpathImpl(path.resolve(String(workspace ?? '')))
109
+ const workspaceLexical = path.resolve(String(workspace ?? ''))
110
+ const workspaceReal = await realpathImpl(workspaceLexical)
110
111
  throwIfVisionAborted()
111
112
  const relativeBase = normalizeArtifactsDir(artifactsDir)
112
113
  const requestedTarget = normalizeRelativeArtifactPath(relativePath)
@@ -114,6 +115,7 @@ async function resolveArtifactTargetForRun(workspace, artifactsDir, relativePath
114
115
  ? normalizeRelativeArtifactPath(path.join(ARTIFACT_RUNS_DIR, runId, requestedTarget))
115
116
  : requestedTarget
116
117
  const lexicalBase = path.resolve(workspaceReal, relativeBase)
118
+ const callerVisibleBase = path.resolve(workspaceLexical, relativeBase)
117
119
  if (!isPathInside(workspaceReal, lexicalBase)) {
118
120
  throw new Error('vision-router: artifactsDir must stay inside the session workspace')
119
121
  }
@@ -135,6 +137,7 @@ async function resolveArtifactTargetForRun(workspace, artifactsDir, relativePath
135
137
  }
136
138
 
137
139
  const lexicalTarget = path.resolve(lexicalBase, relativeTarget)
140
+ const callerVisibleTarget = path.resolve(callerVisibleBase, relativeTarget)
138
141
  if (!isPathInside(lexicalBase, lexicalTarget)) {
139
142
  throw new Error('vision-router: artifact target must stay inside the artifacts directory')
140
143
  }
@@ -186,7 +189,7 @@ async function resolveArtifactTargetForRun(workspace, artifactsDir, relativePath
186
189
  if (existing?.isDirectory?.()) {
187
190
  throw new Error('vision-router: artifact target is a directory')
188
191
  }
189
- return { target, parentReal, artifactsBaseReal, runsBaseReal, runBaseReal, existing, runId }
192
+ return { target, callerVisibleTarget, parentReal, artifactsBaseReal, runsBaseReal, runBaseReal, existing, runId }
190
193
  }
191
194
 
192
195
  export function resolveArtifactTarget(workspace, artifactsDir, relativePath, deps = {}) {
@@ -246,7 +249,12 @@ async function writeArtifactFileForRun(workspace, artifactsDir, relativePath, da
246
249
  }
247
250
  scheduleArtifactRetention(resolved.artifactsBaseReal, { protectRunId: resolved.runId })
248
251
  }
249
- return resolved.target
252
+ // Keep canonical paths for containment and atomic I/O, but return the
253
+ // caller-visible lexical workspace path. On macOS /tmp resolves to
254
+ // /private/tmp (and real deployments may use symlinked workspaces);
255
+ // leaking the realpath makes a valid artifact appear outside the session
256
+ // workspace to downstream path/address code.
257
+ return resolved.callerVisibleTarget
250
258
  } finally {
251
259
  if (!published) {
252
260
  try { await unlinkImpl(temp) } catch { /* best effort */ }
package/lib/client.js CHANGED
@@ -1210,6 +1210,11 @@ window.__ModuleLoader__.load({
1210
1210
  // become local merely because a stale Connection says true.
1211
1211
  const location = locationLike ?? (typeof window !== 'undefined' ? window.location : undefined)
1212
1212
  const hostname = typeof location?.hostname === 'string' ? location.hostname.toLowerCase().replace(/^\[|\]$/g, '') : ''
1213
+ const protocol = typeof location?.protocol === 'string' ? location.protocol.toLowerCase() : ''
1214
+ // The official Desktop shell uses dsh-app://app instead of an HTTP
1215
+ // loopback URL. Trust only that exact scheme/host pair: broad custom-
1216
+ // protocol trust would let an unrelated embedded page gain local UI.
1217
+ if (protocol === 'dsh-app:' && hostname === 'app') return false
1213
1218
  if (hostname !== '') {
1214
1219
  if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return false
1215
1220
  if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return false
@@ -6,8 +6,9 @@ export const DSH_SUPPORT_WINDOW = Object.freeze({
6
6
 
7
7
  export const DSH_VERIFICATION_EVIDENCE = Object.freeze({
8
8
  exactStable: '0.1.5-rc.3',
9
- exactPreview: '0.1.7-rc.2',
9
+ exactPreview: '0.2.0-rc.1',
10
10
  stableCanaryDistTag: 'latest',
11
+ nextCanaryDistTag: 'next',
11
12
  previewCanaryDistTag: 'alpha',
12
13
  })
13
14
 
@@ -48,9 +49,10 @@ export function formatDshSupportWindowLines(capabilities = {}) {
48
49
  ` support floor: DVR ${DSH_SUPPORT_WINDOW.dvrTrain} -> DSH ${DSH_SUPPORT_WINDOW.minimum}`,
49
50
  'DSH compatibility verification evidence:',
50
51
  ` exact stable: ${DSH_VERIFICATION_EVIDENCE.exactStable}`,
51
- ` exact preview (not a support claim): ${DSH_VERIFICATION_EVIDENCE.exactPreview}`,
52
+ ` exact next/rc (not a support claim): ${DSH_VERIFICATION_EVIDENCE.exactPreview}`,
52
53
  ` scheduled stable canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.stableCanaryDistTag}`,
53
- ` scheduled preview canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.previewCanaryDistTag}`,
54
+ ` scheduled next/rc canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.nextCanaryDistTag}`,
55
+ ` scheduled alpha/pre-release canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.previewCanaryDistTag}`,
54
56
  ` upgrade advice: ${advice.level} (${advice.code}) — ${advice.message}`,
55
57
  ])
56
58
  }
@@ -1,4 +1,4 @@
1
- import { createHash } from 'node:crypto'
1
+ import { createHash, randomUUID } from 'node:crypto'
2
2
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
  import process from 'node:process'
@@ -151,7 +151,7 @@ async function loadCache(file, fsOps) {
151
151
 
152
152
  async function saveCache(file, providers, fsOps) {
153
153
  const directory = path.dirname(file)
154
- const temporary = `${file}.${process.pid}.${Date.now()}.tmp`
154
+ const temporary = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
155
155
  await fsOps.mkdir(directory, { recursive: true })
156
156
  const entries = [...providers.values()]
157
157
  .filter((entry) => entry && entry.routeMismatch !== true)
@@ -28,6 +28,10 @@ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
28
28
  var hostname = locationLike && typeof locationLike.hostname === 'string'
29
29
  ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
30
30
  : '';
31
+ var protocol = locationLike && typeof locationLike.protocol === 'string'
32
+ ? locationLike.protocol.toLowerCase()
33
+ : '';
34
+ if (protocol === 'dsh-app:' && hostname === 'app') return true;
31
35
  if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
32
36
  return /^127(?:\.\d{1,3}){3}$/.test(hostname);
33
37
  }
@@ -35,6 +35,10 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
35
35
  var hostname = locationLike && typeof locationLike.hostname === 'string'
36
36
  ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
37
37
  : '';
38
+ var protocol = locationLike && typeof locationLike.protocol === 'string'
39
+ ? locationLike.protocol.toLowerCase()
40
+ : '';
41
+ if (protocol === 'dsh-app:' && hostname === 'app') return true;
38
42
  if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
39
43
  return /^127(?:\.\d{1,3}){3}$/.test(hostname);
40
44
  }
@@ -13,7 +13,7 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
13
13
  function same(a,b){try{return JSON.stringify(a)===JSON.stringify(b);}catch(_){return false;}}
14
14
  function bool(value){return value===true||value==='true';}
15
15
  function rpcValue(body){if(body&&typeof body==='object'&&body.result&&typeof body.result==='object')return body.result.ok===true?body.result.value:undefined;return body;}
16
- function localPage(){try{var host=String(window.location.hostname||'').toLowerCase();return host==='localhost'||host.endsWith('.localhost')||host==='::1'||host==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(host);}catch(_){return false;}}
16
+ function localPage(){try{var location=window.location||{},host=String(location.hostname||'').toLowerCase(),protocol=String(location.protocol||'').toLowerCase();return(protocol==='dsh-app:'&&host==='app')||host==='localhost'||host.endsWith('.localhost')||host==='::1'||host==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(host);}catch(_){return false;}}
17
17
  function builtinRow(row){return !!row&&row.provider==='vision-http'&&typeof row.model==='string'&&row.model.indexOf('ovh/')===0;}
18
18
  function cloneRow(row,model){return {provider:String(row&&row.provider||''),model:String(model===undefined?(row&&row.model||''):model),fallbacks:[]};}
19
19
  function splitChain(value){var builtins=[],users=[];function push(row,model){var next=cloneRow(row,model);(builtinRow(next)?builtins:users).push(next);}arr(value).forEach(function(row){if(!row||typeof row!=='object')return;push(row,row.model);arr(row.fallbacks).forEach(function(fallback){if(typeof fallback==='string'&&fallback.trim())push(row,fallback.trim());});});return {builtins:builtins,users:users};}
@@ -187,8 +187,12 @@ function callbackForwarder(execFileImpl, patchedCustom) {
187
187
  return wrapped
188
188
  }
189
189
 
190
- function syncIfBuiltin(moduleObject) {
191
- if (moduleObject === childProcess) syncBuiltinESMExports()
190
+ function builtinExportSynchronizer(moduleObject, options) {
191
+ const builtinModule = options.builtinChildProcessModule ?? childProcess
192
+ const synchronize = options.syncBuiltinESMExports ?? syncBuiltinESMExports
193
+ return () => {
194
+ if (moduleObject === builtinModule) synchronize()
195
+ }
192
196
  }
193
197
 
194
198
  function warnInstallDegraded(ctx, reason) {
@@ -217,13 +221,26 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
217
221
  const execFileImpl = moduleObject.execFile
218
222
  let state = installStates.get(execFileImpl)
219
223
  if (state === undefined) {
224
+ const synchronizeBuiltinExports = builtinExportSynchronizer(moduleObject, options)
220
225
  const originalCustom = execFileImpl[promisify.custom]
221
226
  const patchedCustom = createVisionRouterExecFilePromisifyCompat(execFileImpl, originalCustom, options)
222
227
 
223
228
  if (canReplaceCustomPromisify(execFileImpl)) {
224
229
  try {
225
230
  setCustomPromisify(execFileImpl, patchedCustom)
231
+ // Electron can expose distinct CJS and ESM execFile function objects.
232
+ // Synchronize after mutating the CJS custom hook so existing ESM named
233
+ // imports observe the same promisify.custom compatibility function.
234
+ synchronizeBuiltinExports()
226
235
  } catch (error) {
236
+ try {
237
+ if (execFileImpl[promisify.custom] === patchedCustom) {
238
+ restoreCustomPromisify(execFileImpl, originalCustom)
239
+ synchronizeBuiltinExports()
240
+ }
241
+ } catch {
242
+ /* best effort rollback; the CJS hook is restored before re-sync */
243
+ }
227
244
  patchedCustom.deactivate?.()
228
245
  warnInstallDegraded(ctx, error && error.message ? error.message : String(error))
229
246
  return () => {}
@@ -236,6 +253,7 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
236
253
  exposedExecFile: execFileImpl,
237
254
  originalCustom,
238
255
  patchedCustom,
256
+ synchronizeBuiltinExports,
239
257
  }
240
258
  installStates.set(execFileImpl, state)
241
259
  } else {
@@ -243,12 +261,12 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
243
261
  try {
244
262
  moduleObject.execFile = exposedExecFile
245
263
  if (moduleObject.execFile !== exposedExecFile) throw new Error('child_process.execFile export is not writable')
246
- syncIfBuiltin(moduleObject)
264
+ synchronizeBuiltinExports()
247
265
  } catch (error) {
248
266
  try {
249
267
  if (moduleObject.execFile === exposedExecFile) {
250
268
  moduleObject.execFile = execFileImpl
251
- syncIfBuiltin(moduleObject)
269
+ synchronizeBuiltinExports()
252
270
  }
253
271
  } catch {
254
272
  /* best effort rollback */
@@ -265,6 +283,7 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
265
283
  exposedExecFile,
266
284
  originalCustom,
267
285
  patchedCustom,
286
+ synchronizeBuiltinExports,
268
287
  }
269
288
  installStates.set(execFileImpl, state)
270
289
  installStates.set(exposedExecFile, state)
@@ -284,7 +303,10 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
284
303
 
285
304
  if (state.mode === 'custom-property') {
286
305
  if (stillOwnsCustom) {
287
- try { restoreCustomPromisify(state.execFileImpl, state.originalCustom) } catch { /* best effort */ }
306
+ try {
307
+ restoreCustomPromisify(state.execFileImpl, state.originalCustom)
308
+ state.synchronizeBuiltinExports()
309
+ } catch { /* best effort */ }
288
310
  }
289
311
  } else if (
290
312
  state.mode === 'module-wrapper' &&
@@ -293,7 +315,7 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
293
315
  ) {
294
316
  try {
295
317
  state.moduleObject.execFile = state.execFileImpl
296
- syncIfBuiltin(state.moduleObject)
318
+ state.synchronizeBuiltinExports()
297
319
  } catch {
298
320
  /* a later host/plugin mutation stays authoritative */
299
321
  }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
2
3
  import path from 'node:path'
3
4
  import process from 'node:process'
@@ -88,7 +89,7 @@ async function load(file, fsOps, at) {
88
89
  }
89
90
 
90
91
  async function save(file, records, fsOps, at) {
91
- const temporary = `${file}.${process.pid}.${Date.now()}.tmp`
92
+ const temporary = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
92
93
  const stops = selectRetainedStops(records.values(), at)
93
94
  await fsOps.mkdir(path.dirname(file), { recursive: true })
94
95
  await fsOps.writeFile(temporary, JSON.stringify({ version: CACHE_VERSION, stops }), { encoding: 'utf8', mode: 0o600 })
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
2
3
  import path from 'node:path'
3
4
  import process from 'node:process'
@@ -374,7 +375,7 @@ async function loadCache(file, fsOps, maxEntries) {
374
375
 
375
376
  async function saveCache(file, records, fsOps, maxEntries) {
376
377
  const directory = path.dirname(file)
377
- const temporary = `${file}.${process.pid}.${Date.now()}.tmp`
378
+ const temporary = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
378
379
  const profiles = selectRetainedCapabilityProfiles(records.values(), maxEntries)
379
380
  await fsOps.mkdir(directory, { recursive: true })
380
381
  await fsOps.writeFile(temporary, JSON.stringify(cacheEnvelope(profiles)), { encoding: 'utf8', mode: 0o600 })
@@ -19,8 +19,8 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
19
19
  function text(cn,en){ return zh() ? cn : en; }
20
20
  function localPage(){
21
21
  try {
22
- var host=String(window.location.hostname||'').toLowerCase();
23
- return host==='localhost'||host.endsWith('.localhost')||host==='::1'||host==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(host);
22
+ var location=window.location||{},host=String(location.hostname||'').toLowerCase(),protocol=String(location.protocol||'').toLowerCase();
23
+ return (protocol==='dsh-app:'&&host==='app')||host==='localhost'||host.endsWith('.localhost')||host==='::1'||host==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(host);
24
24
  } catch (_) { return false; }
25
25
  }
26
26
  function rowSelection(row){
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
2
3
  import path from 'node:path'
3
4
  import process from 'node:process'
@@ -69,7 +70,7 @@ async function load(file, fsOps) {
69
70
  }
70
71
 
71
72
  async function save(file, records, fsOps) {
72
- const temporary = `${file}.${process.pid}.${Date.now()}.tmp`
73
+ const temporary = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
73
74
  const verdicts = selectRetainedVerdicts(records.values())
74
75
  await fsOps.mkdir(path.dirname(file), { recursive: true })
75
76
  await fsOps.writeFile(temporary, JSON.stringify({ version: CACHE_VERSION, verdicts }), { encoding: 'utf8', mode: 0o600 })
@@ -34,6 +34,10 @@ export const SETTINGS_CONFIG_FORMS_CLIENT_PRELUDE = String.raw`(function(){
34
34
  var hostname = locationLike && typeof locationLike.hostname === 'string'
35
35
  ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
36
36
  : '';
37
+ var protocol = locationLike && typeof locationLike.protocol === 'string'
38
+ ? locationLike.protocol.toLowerCase()
39
+ : '';
40
+ if (protocol === 'dsh-app:' && hostname === 'app') return true;
37
41
  if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
38
42
  return /^127(?:\.\d{1,3}){3}$/.test(hostname);
39
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
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": {
@@ -70,7 +70,7 @@
70
70
  "sharp": "^0.35.4"
71
71
  },
72
72
  "scripts": {
73
- "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/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/rc6-real-settings-persistence.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/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/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/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/issue-512-tool-restriction.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.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/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 && node --test tests/vision-backend-runtime-policy.test.js",
73
+ "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/rc6-real-settings-persistence.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/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/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/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/issue-512-tool-restriction.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.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/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 && node --test tests/vision-backend-runtime-policy.test.js",
74
74
  "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",
75
75
  "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",
76
76
  "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",
@@ -88,9 +88,6 @@
88
88
  "quality:round2:run": "node quality/vision-round2/run-host-api.mjs",
89
89
  "quality:round2:report": "node quality/vision-round2/report-results.mjs"
90
90
  },
91
- "pnpm": {
92
- "onlyBuiltDependencies": ["sharp"]
93
- },
94
91
  "dsh": {
95
92
  "client": {
96
93
  "platform": "web",