dsh-vision-router 2.2.6 → 2.2.8

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 (41) hide show
  1. package/README.md +2 -2
  2. package/README.zh.md +2 -2
  3. package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
  4. package/docs/architecture/dsh-compatibility-matrix.md +4 -3
  5. package/docs/architecture/dsh-support-window.md +7 -4
  6. package/docs/architecture/host-first-proxy-convergence.md +1 -1
  7. package/docs/releases/v2.2.7.md +22 -0
  8. package/docs/releases/v2.2.8.md +25 -0
  9. package/lib/artifact-io.js +11 -3
  10. package/lib/client-host-compat-prelude.js +7 -12
  11. package/lib/client-presentation-boundary-main.js +7 -15
  12. package/lib/client-presentation-boundary.js +7 -12
  13. package/lib/client-script-carrier.js +90 -0
  14. package/lib/client.js +7 -2
  15. package/lib/dsh-support-window.js +5 -3
  16. package/lib/guide-vision-toggle-highlight.js +7 -10
  17. package/lib/live-model-client-prelude.js +7 -14
  18. package/lib/live-model-discovery.js +2 -2
  19. package/lib/local-remote-settings-permission.js +8 -9
  20. package/lib/session-vision-mode-boundary.js +43 -8
  21. package/lib/settings-client-017-compat.js +4 -0
  22. package/lib/settings-client-rc8-lifecycle.js +11 -12
  23. package/lib/settings-factory-lifecycle.js +25 -14
  24. package/lib/settings-ia-client-prelude.js +12 -12
  25. package/lib/settings-limit-client-prelude.js +7 -9
  26. package/lib/strict-live-model-client-prelude.js +7 -10
  27. package/lib/tesseract-exec-compat.js +28 -6
  28. package/lib/v2-settings-ia-integration.js +21 -6
  29. package/lib/vision-background-benchmark.js +3 -5
  30. package/lib/vision-background-stop-store.js +2 -1
  31. package/lib/vision-capability-benchmark-client.js +7 -10
  32. package/lib/vision-capability-probe.js +2 -1
  33. package/lib/vision-exact-check-client.js +13 -8
  34. package/lib/vision-image-input-verdict.js +2 -1
  35. package/lib/vision-model-visibility-boundary-main.js +7 -11
  36. package/lib/vision-model-visibility-boundary.js +7 -12
  37. package/lib/vision-routing-settings-prelude.js +7 -11
  38. package/lib/vision-toggle-root-hardening.js +28 -3
  39. package/lib/vision-turn-budget-client-prelude.js +7 -10
  40. package/lib/web/remote-settings-client.js +11 -12
  41. package/package.json +2 -5
package/README.md CHANGED
@@ -375,7 +375,7 @@ Everything is optional; defaults work out of the box. Prefer **Settings → Visi
375
375
  | `tool` / `progressiveTools` / `autoActivateOnImage` | `true` / `false` / `true` | vision tools on / progressive mounting (off by default for a stable tool schema) / image-turn auto-mount when progressive mode is enabled; `progressiveTools` is boot-time config |
376
376
  | `rewriteImages` | `true` | rewrite image blocks in the model input (cached description or tool-hint marker); the UI log keeps images |
377
377
  | `desktopScreenshot` | `false` | privacy opt-in for the model-callable `vision_screenshot` desktop-capture tool; checked live before every capture |
378
- | `localOnlyVision` | `false` | runtime privacy policy: when enabled, only loopback vision endpoints may execute; cloud/DSH providers and the built-in OVH fallback stay saved but are fail-closed |
378
+ | `localOnlyVision` | `false` | visual-endpoint privacy policy: only loopback vision endpoints may execute, so Vision Router keeps image bytes/crops off cloud vision services; derived result text (descriptions, OCR, coordinates) still goes to the current chat model, so strict end-to-end local use also requires a local chat model |
379
379
  | `freeFallback` | `true` | append the anonymous OVH models after explicit local/custom HTTP backends; ignored while `localOnlyVision` is enabled |
380
380
  | `localOllama` | `{ enabled: false, baseURL: 'http://127.0.0.1:11434/v1', model: 'qwen2.5vl', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | local vision backend; OpenAI mode disables supported model reasoning by default so the output budget is spent on answer text |
381
381
  | `localLmStudio` | `{ enabled: false, baseURL: 'http://localhost:1234/v1', model: '', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | local LM Studio backend after Ollama; LM Studio 0.4+ can use `format: 'lmstudio'` for documented native reasoning control |
@@ -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.2` (`next/rc`) 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
@@ -373,7 +373,7 @@ Web profile 现在提供一级 **设置 → Vision Router** 页面。常规页
373
373
  | `tool` / `progressiveTools` / `autoActivateOnImage` | `true` / `false` / `true` | 视觉工具总开关 / 渐进式挂载(默认关闭以稳定工具 schema)/ 渐进模式下图片轮自动挂载;`progressiveTools` 为启动期配置 |
374
374
  | `rewriteImages` | `true` | 模型输入层改写图片块(缓存描述或工具提示标记);界面日志保留图片 |
375
375
  | `desktopScreenshot` | `false` | 模型可调用的 `vision_screenshot` 桌面截屏隐私开关;每次截屏前实时检查 |
376
- | `localOnlyVision` | `false` | 运行时隐私策略:开启后只有回环地址上的视觉端点可执行;云端/DSH Provider 与内置 OVH 兜底保留配置但 fail closed |
376
+ | `localOnlyVision` | `false` | 视觉端点隐私策略:只有回环地址上的视觉端点可执行,Vision Router 不会把图片/裁剪发给云端视觉服务;描述、OCR、坐标等识图结果文本仍会交给当前聊天模型,因此严格端到端纯本地还需同时使用本地聊天模型 |
377
377
  | `freeFallback` | `true` | 在显式本地/自定义 HTTP 后端之后追加匿名 OVH 模型;`localOnlyVision` 开启期间不会执行 |
378
378
  | `localOllama` | `{ enabled: false, baseURL: 'http://127.0.0.1:11434/v1', model: 'qwen2.5vl', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | 本地视觉后端;OpenAI 模式默认关闭受支持模型的推理,把输出预算留给正文 |
379
379
  | `localLmStudio` | `{ enabled: false, baseURL: 'http://localhost:1234/v1', model: '', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | Ollama 之后的本地 LM Studio 后端;LM Studio 0.4+ 可选 `format: 'lmstudio'` 使用官方原生推理控制 |
@@ -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.2`(`next/rc`)源码、真实 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;其余工具无浏览器也能用。
@@ -0,0 +1,23 @@
1
+ # DSH 0.2.0-rc.2 compatibility audit
2
+
3
+ - Channel: next/rc (kept distinct from stable and alpha/pre-release)
4
+ - Upstream tag: `dsh-v0.2.0-rc.2`
5
+ - Upstream commit: `639ed015397290b3745d163aafe02ffee4aa3f84`
6
+ - Published: 2026-09-29 09:21:31 UTC
7
+ - Comparison baseline: `dsh-v0.2.0-rc.1` at `4878cdabd87d4041bdaff61d04c966883b9fd07a`
8
+
9
+ ## Seam review
10
+
11
+ | Area | rc.1 → rc.2 finding | DVR action |
12
+ | --- | --- | --- |
13
+ | Host / Desktop | Desktop now ships a bundled CLI entry, command installation management, login-shell environment import, and Windows console signal bridging. Existing Web profile plugin injection remains available. | Re-run exact-source on Windows/macOS/Linux plus Windows Desktop on Node 22 and 24. |
14
+ | Session | Session controller source is unchanged; its package version advances only. New timed user-question persistence is additive outside DVR's session image ownership seam. | Retain session/adversarial regressions and real Host lifecycle checks. |
15
+ | Settings | Settings controller source is unchanged. Web preset/default and Creator controls are no longer gated by Coding Tools. | Re-run Settings mount/save/reload/readback in real Chromium; no compatibility shim added. |
16
+ | bundle | Published bundle package sources are unchanged apart from versions. Desktop/Host bundles become more self-contained for CLI launch. | Re-run bundle recomposition and cache-manifest validation. |
17
+ | web/client | Model picker search, shortcut modal caching, preset Settings behavior, and long-session rendering changed. | Re-run Vision toggle, mixed attachments, Settings, and client/browser contract suites. |
18
+ | model / attachment | Attachment package sources are unchanged; model-facing work is UI/search and pi-ai/Mistral compatibility. | Re-run mixed-attachment and provider wire contracts. |
19
+ | service injection | API Remotes adds the user-question remote; Gateway adds additive `hasLiveClient()`. DVR's declared injected services and Host-owned proxy authority are unchanged. | Re-run injection, proxy-egress, multi-plugin ordering, and Host-Sharp contracts. |
20
+
21
+ ## Policy
22
+
23
+ The public minimum Host remains DSH `0.1.0-rc.8`. The current stable evidence remains `0.1.5-rc.3`. The peer range remains `>=0.2.0-rc.1 <0.3.0-0`; rc.2 advances exact next/rc verification evidence and does not widen the range or turn preview evidence into a stable support claim.
@@ -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.2 validation` | `0.2.0-rc.2` (`next/rc`) | 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.2` commit `639ed015397290b3745d163aafe02ffee4aa3f84`, published on 2026-09-29. 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.2` (`next/rc`) | 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
 
@@ -75,3 +76,5 @@ DVR must not turn these tables into widespread version-string conditionals. Runt
75
76
  ## Compatibility-retirement rule
76
77
 
77
78
  The 2.1.x floor makes rc.6-only compatibility candidates eligible for a fresh deletion audit, but does not automatically authorize deletion. Durable session formats, replay envelopes, adapter wire shapes, and other historical inputs may outlive the Host version that originally produced them.
79
+
80
+ See [DSH 0.2.0-rc.2 compatibility audit](dsh-020-rc2-adaptation.md) for the upstream seam review and validation plan.
@@ -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.2`, 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.
@@ -0,0 +1,25 @@
1
+ # v2.2.8
2
+
3
+ DSH Vision Router 2.2.8 is a Desktop compatibility and lifecycle reliability patch. It restores conversation-side Vision UI injection in the official static-shell Desktop, prevents stale Vision tool restrictions from surviving plugin reloads, adds real Electron end-to-end coverage on Windows and macOS, hardens DSH build-cache completeness and test diagnostics, clarifies the `localOnlyVision` privacy boundary, and advances exact preview verification to DSH `0.2.0-rc.2` without changing the public Host support floor.
4
+
5
+ ## Highlights
6
+
7
+ - **Official Desktop conversation injection (#597):** browser-side client preludes now use one capability-detected carrier that publishes structured `webserver/index-inject` rows for the official Desktop shell while retaining `tapIndex()` as the older/Web fallback. The Vision toggle and related client boundaries no longer depend on HTTP-served HTML to appear.
8
+ - **Vision tool lifecycle fix (#597):** Session tool restrictions installed while Vision mode is off are explicitly released when the plugin boundary is disposed. A surviving Agent can no longer inherit an orphaned restriction from an older plugin/HMR instance and keep `vision_describe` and the rest of the DVR tool surface hidden after Vision is re-enabled.
9
+ - **Real Desktop functional gates (#597):** Windows Node 24 and macOS now launch the actual Electron renderer and exercise Settings open/save/reload, workspace/session startup, image attachment, Vision toggle state, real `vision_describe` tool calling, image delivery to a controlled local vision backend, and the final rendered assistant response. Separate adversarial tests still cover duplicate/ordered carriers, failure recovery, OFF→ON→OFF authority, and plugin reload boundaries.
10
+ - **Complete DSH build caches (#597):** trusted exact-Host caches retain both native `packages/*/lib` JavaScript outputs and `packages/*/bin` artifacts under a new cache schema. A cache hit can no longer restore a half-built DSH runtime missing files such as `@deepseek-ai/node-addon-system/lib/flock.js`.
11
+ - **Safer Desktop diagnostics (#597):** failure artifacts persist fixed summaries of Host/network evidence rather than writing arbitrary response bodies to disk. CodeQL verified the previous network-data-to-file path as fixed.
12
+ - **`localOnlyVision` privacy wording (#598):** Settings and documentation now state the actual boundary precisely: raw image bytes stay on local vision backends, while derived OCR/description/tool results can still enter the active chat-model context. This is a disclosure clarification, not a routing-policy change.
13
+ - **DSH `0.2.0-rc.2` verification (#599):** the permanent next/rc compatibility gate now pins exact rc.2 source and covers Linux/macOS/Windows source contracts, Node 22/24 Windows Desktop Host, real Host + Chromium, multi-plugin ordering, Settings, mixed attachments, bundle recomposition, and real Windows/macOS Electron renderer flows. The peer-admission boundary and public minimum remain unchanged.
14
+
15
+ ## Validation
16
+
17
+ - Final local regression on the rc.2-compatible tree: main suite `1534` passed, `1` intentional skip, `0` failed; backend runtime-policy suite passed `9/9`.
18
+ - DSH `0.2.0-rc.2` passed exact-source checks on Ubuntu, Windows, and macOS; real Host + Chromium; Windows Desktop Host on Node 22 and Node 24; Node 24 multi-plugin ordering; and real Windows/macOS Electron renderer lifecycle tests.
19
+ - The real Desktop Vision flow reached the controlled text model, issued `vision_describe`, delivered actual image content to the controlled vision backend, consumed the tool result, and rendered the expected final response.
20
+ - The post-merge `main` push gates for `f92a6e0` completed with 0 failures, including CI, HOL security, DSH exact-source, DSH 0.1.7 real Host, DSH `0.2.0-rc.2` validation, preview browser smoke, routing parity, data boundary, architecture closure, and compatibility convergence.
21
+ - The public minimum supported DSH Host remains `0.1.0-rc.8`; existing 2.2.x settings require no migration.
22
+
23
+ ## Upgrade
24
+
25
+ 2.2.7 users should upgrade to 2.2.8, especially when using the official Desktop app or frequently reloading/updating plugins in long-lived sessions. 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 */ }
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
 
3
3
  const CLIENT_HOST_COMPAT_MARK = 'data-vision-router-host-compat'
4
4
 
@@ -243,19 +243,14 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
243
243
  })();`
244
244
 
245
245
  export function injectClientHostCompatibility(html) {
246
- if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_HOST_COMPAT_MARK)) return html
247
- const safe = CLIENT_HOST_COMPAT_PRELUDE.replace(/<\/script/gi, '<\\/script')
248
- const script = `<script ${CLIENT_HOST_COMPAT_MARK}>${safe}</script>`
249
- const closeHead = html.indexOf('</head>')
250
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
251
- return `${html}${script}`
246
+ return injectClientCarrierHtml(html, CLIENT_HOST_COMPAT_MARK, CLIENT_HOST_COMPAT_PRELUDE)
252
247
  }
253
248
 
254
249
  export function installClientHostCompatibility(ctx) {
255
- ctx?.inject?.(['webServer'], (webCtx) => {
256
- webCtx.effect(
257
- () => webCtx.webServer.tapIndex(injectClientHostCompatibility),
258
- 'vision-router: client Host compatibility boundary',
259
- )
250
+ installClientScriptCarriers(ctx, {
251
+ marker: CLIENT_HOST_COMPAT_MARK,
252
+ prelude: CLIENT_HOST_COMPAT_PRELUDE,
253
+ label: 'vision-router: client Host compatibility',
254
+ injectHtml: injectClientHostCompatibility,
260
255
  })
261
256
  }
@@ -1,3 +1,4 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  const CLIENT_PRESENTATION_MARK = 'data-vision-router-presentation-boundary'
2
3
  const VISION_MODE_WRAPPER_SOURCE = 'deepseek-official'
3
4
 
@@ -1304,23 +1305,14 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
1304
1305
  })();`
1305
1306
 
1306
1307
  export function injectClientPresentationBoundary(html) {
1307
- if (typeof html !== 'string' || html.includes(CLIENT_PRESENTATION_MARK)) return html
1308
- const safe = CLIENT_PRESENTATION_PRELUDE.replace(/<\/script/gi, '<\\/script')
1309
- const script = `<script ${CLIENT_PRESENTATION_MARK}>${safe}</script>`
1310
- // DSH's client-modules index tap prepends the queue-mode loader bootstrap at
1311
- // <head>. Run after that parser bootstrap so we can wrap both load() and the
1312
- // later create() transition. The create wrapper re-applies the boundary when
1313
- // rc.8 replaces loader.load while entering live mode.
1314
- const closeHead = html.indexOf('</head>')
1315
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
1316
- return `${html}${script}`
1308
+ return injectClientCarrierHtml(html, CLIENT_PRESENTATION_MARK, CLIENT_PRESENTATION_PRELUDE)
1317
1309
  }
1318
1310
 
1319
1311
  export function installClientPresentationBoundary(ctx) {
1320
- ctx?.inject?.(['webServer'], (webCtx) => {
1321
- webCtx.effect(
1322
- () => webCtx.webServer.tapIndex(injectClientPresentationBoundary),
1323
- 'vision-router: client presentation boundary',
1324
- )
1312
+ installClientScriptCarriers(ctx, {
1313
+ marker: CLIENT_PRESENTATION_MARK,
1314
+ prelude: CLIENT_PRESENTATION_PRELUDE,
1315
+ label: 'vision-router: client presentation boundary',
1316
+ injectHtml: injectClientPresentationBoundary,
1325
1317
  })
1326
1318
  }
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
  import {
3
3
  CLIENT_PRESENTATION_PRELUDE as BASE_CLIENT_PRESENTATION_PRELUDE,
4
4
  resolveVisionModePair,
@@ -187,20 +187,15 @@ export {
187
187
  }
188
188
 
189
189
  export function injectClientPresentationBoundary(html) {
190
- if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_PRESENTATION_MARK)) return html
191
- const safe = CLIENT_PRESENTATION_PRELUDE.replace(/<\/script/gi, '<\\/script')
192
- const script = `<script ${CLIENT_PRESENTATION_MARK}>${safe}</script>`
193
- const closeHead = html.indexOf('</head>')
194
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
195
- return `${html}${script}`
190
+ return injectClientCarrierHtml(html, CLIENT_PRESENTATION_MARK, CLIENT_PRESENTATION_PRELUDE)
196
191
  }
197
192
 
198
193
  export function installClientPresentationBoundary(ctx) {
199
- ctx?.inject?.(['webServer'], (webCtx) => {
200
- webCtx.effect(
201
- () => webCtx.webServer.tapIndex(injectClientPresentationBoundary),
202
- 'vision-router: client presentation boundary',
203
- )
194
+ installClientScriptCarriers(ctx, {
195
+ marker: CLIENT_PRESENTATION_MARK,
196
+ prelude: CLIENT_PRESENTATION_PRELUDE,
197
+ label: 'vision-router: client presentation boundary',
198
+ injectHtml: injectClientPresentationBoundary,
204
199
  })
205
200
  // Keep Host-version compatibility in its own capability-detected prelude.
206
201
  // This lets the large legacy client factory stay byte-stable while alpha/new
@@ -0,0 +1,90 @@
1
+ import { htmlHasScriptMarker } from './html-script-marker.js'
2
+
3
+ const CARRIER_REGISTRY = '__dshVisionRouterClientCarriers'
4
+
5
+ function carrierKey(marker) {
6
+ const value = String(marker || 'vision-router-client-script')
7
+ return `vr:${value.replace(/^data-vision-router-/, '')}`
8
+ }
9
+
10
+ /**
11
+ * Wrap one browser prelude with a shared one-shot fence. Modern served-Web
12
+ * pages can receive both structured index rows and tapIndex HTML transforms,
13
+ * while Desktop/static carriers receive only the structured row. The fence
14
+ * keeps both delivery paths safe without relying on each prelude to implement
15
+ * its own duplicate-execution semantics.
16
+ */
17
+ export function clientCarrierSource(marker, prelude) {
18
+ const key = JSON.stringify(carrierKey(marker))
19
+ return `(function(){\n var root = window;\n var registry = root.${CARRIER_REGISTRY};\n if (!registry || typeof registry !== 'object') {\n registry = Object.create(null);\n try { Object.defineProperty(root, '${CARRIER_REGISTRY}', { value: registry, configurable: true }); }\n catch (_) { root.${CARRIER_REGISTRY} = registry; }\n }\n var key = ${key};\n if (registry[key]) return;\n registry[key] = true;\n try {\n${prelude}\n } catch (error) {\n try { delete registry[key]; } catch (_) { registry[key] = false; }\n throw error;\n }\n})();`
20
+ }
21
+
22
+ export function injectClientCarrierHtml(html, marker, prelude) {
23
+ const structuredMark = `/* ${marker} */`
24
+ if (typeof html !== 'string' || htmlHasScriptMarker(html, marker) || html.includes(structuredMark)) return html
25
+ const safe = clientCarrierSource(marker, prelude).replace(/<\/script/gi, '<\\/script')
26
+ const script = `<script ${marker}>${safe}</script>`
27
+ const closeHead = html.indexOf('</head>')
28
+ return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
29
+ }
30
+
31
+
32
+ export function injectClientStyleHtml(html, marker, css) {
33
+ if (typeof html !== 'string' || typeof marker !== 'string' || marker === '') return html
34
+ if (html.includes(`<style ${marker}>`)) return html
35
+ const style = `<style ${marker}>${String(css || '')}</style>`
36
+ const closeHead = html.indexOf('</head>')
37
+ return closeHead === -1 ? `${html}${style}` : `${html.slice(0, closeHead)}${style}${html.slice(closeHead)}`
38
+ }
39
+
40
+ export function appendStructuredClientStyle(table, marker, css) {
41
+ if (!Array.isArray(table) || typeof marker !== 'string' || marker === '') return false
42
+ const signature = `/* ${marker}:style */`
43
+ if (table.some((row) => row?.kind === 'style' && typeof row.text === 'string' && row.text.includes(signature))) return false
44
+ table.push({ kind: 'style', placement: 'head', text: `${signature}
45
+ ${String(css || '')}` })
46
+ return true
47
+ }
48
+
49
+ export function appendStructuredClientCarrier(table, marker, prelude) {
50
+ if (!Array.isArray(table)) return false
51
+ const signature = `/* ${marker} */`
52
+ if (table.some((row) => row?.kind === 'script' && typeof row.text === 'string' && row.text.includes(signature))) {
53
+ return false
54
+ }
55
+ table.push({
56
+ kind: 'script',
57
+ placement: 'head',
58
+ text: `${signature}\n${clientCarrierSource(marker, prelude)}`,
59
+ })
60
+ return true
61
+ }
62
+
63
+ /**
64
+ * Register the same client prelude on both supported browser carriers:
65
+ * - structured `webserver/index-inject` rows for Desktop/static shells;
66
+ * - `tapIndex()` HTML transforms for served-Web and older Hosts.
67
+ */
68
+ export function installClientScriptCarriers(ctx, { marker, prelude, label, injectHtml, style }) {
69
+ if (!ctx || typeof ctx.inject !== 'function') return
70
+ ctx.inject(['webServer'], (webCtx) => {
71
+ if (typeof webCtx.on === 'function') {
72
+ webCtx.on('webserver/index-inject', (table) => {
73
+ if (style) appendStructuredClientStyle(table, style.marker || marker, style.text)
74
+ appendStructuredClientCarrier(table, marker, prelude)
75
+ })
76
+ }
77
+ if (typeof webCtx.effect === 'function' && webCtx.webServer?.tapIndex) {
78
+ const baseTransform = typeof injectHtml === 'function'
79
+ ? injectHtml
80
+ : (html) => injectClientCarrierHtml(html, marker, prelude)
81
+ const transform = style
82
+ ? (html) => baseTransform(injectClientStyleHtml(html, style.marker || marker, style.text))
83
+ : baseTransform
84
+ webCtx.effect(
85
+ () => webCtx.webServer.tapIndex(transform),
86
+ label,
87
+ )
88
+ }
89
+ })
90
+ }
package/lib/client.js CHANGED
@@ -230,7 +230,7 @@ window.__ModuleLoader__.load({
230
230
  hintRewriteImages: '避免把无法读取的原始图片直接发送给纯文字模型。推荐保持开启。',
231
231
  hintDownscale: '超过像素预算的图片先缩放再送视觉模型,降低延迟与成本;默认开启。',
232
232
  hintCache: '缓存识图答案(按图片内容 + 问题);默认开启。',
233
- hintLocalOnlyVision: '开启后仅允许回环地址上的本地视觉后端;云端/DSH Provider 与 OVH 免费兜底会被运行时硬阻止。主聊天模型是否联网不受此开关控制。',
233
+ hintLocalOnlyVision: '开启后仅允许回环地址上的本地视觉后端;云端/DSH Provider 与 OVH 免费兜底会被运行时硬阻止,因此 Vision Router 不会把图片和裁剪发送到云端视觉服务。视觉后端返回的描述、OCR、坐标等识图结果文本仍会交给当前聊天模型;若聊天模型在云端,图片语义仍可能离开本机。严格纯本地还需同时选择本地聊天模型。',
234
234
  hintFreeFallback: '当你选择的识图模型都不可用时,自动尝试内置 OVH 免费模型。免注册、免 API Key。',
235
235
  hintFreeCloudFirst:
236
236
  '默认关闭。开启后,云端后端先尝试内置 OVH 免费模型(免注册、免 API Key),' +
@@ -519,7 +519,7 @@ window.__ModuleLoader__.load({
519
519
  hintRewriteImages: 'Prevents raw image content from being sent to a text-only model that cannot read it. Recommended on.',
520
520
  hintDownscale: 'Images beyond the pixel budget are resized before the vision call, cutting latency and cost; on by default.',
521
521
  hintCache: 'Caches vision answers (keyed by image content + question); on by default.',
522
- hintLocalOnlyVision: 'When enabled, only visual backends on loopback addresses may run. Cloud/DSH providers and the OVH free fallback are blocked at runtime. This does not control whether the main chat model is remote.',
522
+ hintLocalOnlyVision: 'When enabled, only visual backends on loopback addresses may run. Cloud/DSH providers and the OVH free fallback are blocked at runtime, so Vision Router does not send image bytes or crops to cloud vision services. Derived result text—descriptions, OCR, and coordinates—is still passed to the current chat model; if that model is remote, image semantics may still leave this machine. For a strict end-to-end local workflow, select a local chat model too.',
523
523
  hintFreeFallback: 'If your selected vision models fail, automatically try the built-in OVH free models. No signup or API key required.',
524
524
  hintFreeCloudFirst:
525
525
  'Off by default. When enabled, cloud backends try the built-in OVH free models first (no signup, no API key), ' +
@@ -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.2',
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,3 +1,4 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  const CLIENT_PRELUDE_MARK = 'data-vision-router-guide-toggle-highlight'
2
3
 
3
4
  export function unionGuideRects(first, second) {
@@ -149,18 +150,14 @@ export const GUIDE_VISION_TOGGLE_HIGHLIGHT_PRELUDE = String.raw`(function(){
149
150
  })();`
150
151
 
151
152
  export function injectGuideVisionToggleHighlight(html) {
152
- if (typeof html !== 'string' || html.includes(CLIENT_PRELUDE_MARK)) return html
153
- const script = `<script ${CLIENT_PRELUDE_MARK}>${GUIDE_VISION_TOGGLE_HIGHLIGHT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
154
- const closeHead = html.indexOf('</head>')
155
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
156
- return `${html}${script}`
153
+ return injectClientCarrierHtml(html, CLIENT_PRELUDE_MARK, GUIDE_VISION_TOGGLE_HIGHLIGHT_PRELUDE)
157
154
  }
158
155
 
159
156
  export function installGuideVisionToggleHighlight(ctx) {
160
- ctx?.inject?.(['webServer'], (webCtx) => {
161
- webCtx.effect(
162
- () => webCtx.webServer.tapIndex(injectGuideVisionToggleHighlight),
163
- 'vision-router: onboarding vision-toggle spotlight',
164
- )
157
+ installClientScriptCarriers(ctx, {
158
+ marker: CLIENT_PRELUDE_MARK,
159
+ prelude: GUIDE_VISION_TOGGLE_HIGHLIGHT_PRELUDE,
160
+ label: 'vision-router: guide Vision toggle highlight',
161
+ injectHtml: injectGuideVisionToggleHighlight,
165
162
  })
166
163
  }
@@ -1,3 +1,4 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  import { htmlHasScriptMarker } from './html-script-marker.js'
2
3
 
3
4
  const CLIENT_PRELUDE_MARK = 'data-vision-router-live-models'
@@ -588,22 +589,14 @@ export const LIVE_MODEL_CLIENT_PRELUDE = String.raw`(function(){
588
589
  })();`
589
590
 
590
591
  export function injectLiveModelClientPrelude(html) {
591
- if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_PRELUDE_MARK)) return html
592
- const script = `<script ${CLIENT_PRELUDE_MARK}>${LIVE_MODEL_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
593
- // DSH's client-modules index tap prepends its queue-mode facade immediately
594
- // after <head>. Run after that parser bootstrap so we can wrap both load()
595
- // and create(); the create wrapper then re-attaches us after rc.8 swaps in
596
- // its live registration sink, before lazy third-party bundles arrive.
597
- const closeHead = html.indexOf('</head>')
598
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
599
- return `${html}${script}`
592
+ return injectClientCarrierHtml(html, CLIENT_PRELUDE_MARK, LIVE_MODEL_CLIENT_PRELUDE)
600
593
  }
601
594
 
602
595
  export function installLiveModelClientPrelude(ctx) {
603
- ctx?.inject?.(['webServer'], (webCtx) => {
604
- webCtx.effect(
605
- () => webCtx.webServer.tapIndex(injectLiveModelClientPrelude),
606
- 'vision-router: live model client prelude',
607
- )
596
+ installClientScriptCarriers(ctx, {
597
+ marker: CLIENT_PRELUDE_MARK,
598
+ prelude: LIVE_MODEL_CLIENT_PRELUDE,
599
+ label: 'vision-router: live model client prelude',
600
+ injectHtml: injectLiveModelClientPrelude,
608
601
  })
609
602
  }
@@ -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)
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
  import { isLocalUiRequest } from './web-capability-boundary.js'
3
3
 
4
4
  const SETTINGS_NS = 'vision-router'
@@ -437,10 +437,7 @@ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
437
437
  })();`
438
438
 
439
439
  export function injectLocalPermissionClientPrelude(html) {
440
- if (typeof html !== 'string' || htmlHasScriptMarker(html, LOCAL_PERMISSION_PRELUDE_MARK)) return html
441
- const script = `<script ${LOCAL_PERMISSION_PRELUDE_MARK}>${LOCAL_PERMISSION_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
442
- const endHead = html.indexOf('</head>')
443
- return endHead === -1 ? `${html}${script}` : `${html.slice(0, endHead)}${script}${html.slice(endHead)}`
440
+ return injectClientCarrierHtml(html, LOCAL_PERMISSION_PRELUDE_MARK, LOCAL_PERMISSION_CLIENT_PRELUDE)
444
441
  }
445
442
 
446
443
  export function installLocalRemoteSettingsPermissionBridge(ctx, logger) {
@@ -456,9 +453,11 @@ export function installLocalRemoteSettingsPermissionBridge(ctx, logger) {
456
453
  }),
457
454
  'vision-router: loopback remote-settings permission mutation',
458
455
  )
459
- localCtx.effect(
460
- () => localCtx.webServer.tapIndex(injectLocalPermissionClientPrelude),
461
- 'vision-router: local settings permission client shim',
462
- )
456
+ })
457
+ installClientScriptCarriers(ctx, {
458
+ marker: LOCAL_PERMISSION_PRELUDE_MARK,
459
+ prelude: LOCAL_PERMISSION_CLIENT_PRELUDE,
460
+ label: 'vision-router: local settings permission client shim',
461
+ injectHtml: injectLocalPermissionClientPrelude,
463
462
  })
464
463
  }