dsh-vision-router 2.2.7 → 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 (33) 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 +1 -1
  5. package/docs/architecture/dsh-support-window.md +4 -2
  6. package/docs/architecture/host-first-proxy-convergence.md +1 -1
  7. package/docs/releases/v2.2.8.md +25 -0
  8. package/lib/client-host-compat-prelude.js +7 -12
  9. package/lib/client-presentation-boundary-main.js +7 -15
  10. package/lib/client-presentation-boundary.js +7 -12
  11. package/lib/client-script-carrier.js +90 -0
  12. package/lib/client.js +2 -2
  13. package/lib/dsh-support-window.js +1 -1
  14. package/lib/guide-vision-toggle-highlight.js +7 -10
  15. package/lib/live-model-client-prelude.js +7 -14
  16. package/lib/local-remote-settings-permission.js +8 -9
  17. package/lib/session-vision-mode-boundary.js +43 -8
  18. package/lib/settings-client-rc8-lifecycle.js +7 -12
  19. package/lib/settings-factory-lifecycle.js +25 -14
  20. package/lib/settings-ia-client-prelude.js +11 -11
  21. package/lib/settings-limit-client-prelude.js +7 -9
  22. package/lib/strict-live-model-client-prelude.js +7 -10
  23. package/lib/v2-settings-ia-integration.js +21 -6
  24. package/lib/vision-background-benchmark.js +3 -5
  25. package/lib/vision-capability-benchmark-client.js +7 -10
  26. package/lib/vision-exact-check-client.js +11 -6
  27. package/lib/vision-model-visibility-boundary-main.js +7 -11
  28. package/lib/vision-model-visibility-boundary.js +7 -12
  29. package/lib/vision-routing-settings-prelude.js +7 -11
  30. package/lib/vision-toggle-root-hardening.js +28 -3
  31. package/lib/vision-turn-budget-client-prelude.js +7 -10
  32. package/lib/web/remote-settings-client.js +7 -12
  33. package/package.json +1 -1
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 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).
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` 从已验证的 `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)。
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.
@@ -12,7 +12,7 @@ The matrix is capability-based. Runtime code must feature-detect the seam it nee
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
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
+ | `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. |
16
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. |
17
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. |
18
18
 
@@ -19,7 +19,7 @@ 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 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.
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
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
 
@@ -30,7 +30,7 @@ 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 next/rc evidence | `0.2.0-rc.1` (`next`) | Required 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
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
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. |
@@ -76,3 +76,5 @@ DVR must not turn these tables into widespread version-string conditionals. Runt
76
76
  ## Compatibility-retirement rule
77
77
 
78
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 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`.
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,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.
@@ -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), ' +
@@ -6,7 +6,7 @@ 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.2.0-rc.1',
9
+ exactPreview: '0.2.0-rc.2',
10
10
  stableCanaryDistTag: 'latest',
11
11
  nextCanaryDistTag: 'next',
12
12
  previewCanaryDistTag: 'alpha',
@@ -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 { 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
  }
@@ -134,14 +134,26 @@ function disposeHeldRestriction(held) {
134
134
  }
135
135
  }
136
136
 
137
- function releaseAgentToolRestriction(agent, restrictions) {
137
+ function releaseAgentToolRestriction(agent, restrictions, activeRestrictions) {
138
138
  if (!isObject(agent)) return
139
139
  const held = restrictions.get(agent)
140
140
  if (!held) return
141
141
  disposeHeldRestriction(held)
142
+ activeRestrictions?.delete(held)
142
143
  restrictions.delete(agent)
143
144
  }
144
145
 
146
+ function rememberAgentToolRestriction(agent, restrictions, activeRestrictions, held) {
147
+ restrictions.set(agent, held)
148
+ activeRestrictions?.add(held)
149
+ }
150
+
151
+ function releaseAllAgentToolRestrictions(activeRestrictions) {
152
+ if (!(activeRestrictions instanceof Set)) return
153
+ for (const held of activeRestrictions) disposeHeldRestriction(held)
154
+ activeRestrictions.clear()
155
+ }
156
+
145
157
  function restrictionFailureLabel(error) {
146
158
  const name =
147
159
  typeof error?.name === 'string' && error.name !== ''
@@ -165,10 +177,10 @@ function warnRestrictionFailure(agent, error, deny, failed = deny) {
165
177
  } catch {}
166
178
  }
167
179
 
168
- function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryTools) {
180
+ function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryTools, activeRestrictions) {
169
181
  if (!isObject(agent)) return
170
182
  if (enabled) {
171
- releaseAgentToolRestriction(agent, restrictions)
183
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
172
184
  return
173
185
  }
174
186
 
@@ -181,19 +193,19 @@ function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryT
181
193
  const deny = restrictableOwnedVisionToolNames(registryTools, owned)
182
194
  if (typeof restrict !== 'function') return
183
195
  if (deny.length === 0) {
184
- releaseAgentToolRestriction(agent, restrictions)
196
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
185
197
  return
186
198
  }
187
199
 
188
200
  const held = restrictions.get(agent)
189
201
  const signature = deny.join('\u0000')
190
202
  if (held?.signature === signature) return
191
- releaseAgentToolRestriction(agent, restrictions)
203
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
192
204
 
193
205
  try {
194
206
  // Current DSH can mask all inherited plugin tools in one scoped operation.
195
207
  const dispose = restrict.call(tools, { deny })
196
- restrictions.set(agent, {
208
+ rememberAgentToolRestriction(agent, restrictions, activeRestrictions, {
197
209
  signature,
198
210
  disposers: typeof dispose === 'function' ? [dispose] : [],
199
211
  })
@@ -212,7 +224,9 @@ function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryT
212
224
  failed.push(name)
213
225
  }
214
226
  }
215
- if (disposers.length > 0) restrictions.set(agent, { signature, disposers })
227
+ if (disposers.length > 0) {
228
+ rememberAgentToolRestriction(agent, restrictions, activeRestrictions, { signature, disposers })
229
+ }
216
230
  if (failed.length > 0 || disposers.length === 0) {
217
231
  warnRestrictionFailure(agent, combinedError, deny, failed.length > 0 ? failed : deny)
218
232
  }
@@ -311,6 +325,7 @@ function wrapTools(tools, ctx, config, owned) {
311
325
  export function installSessionVisionModeBoundary(ctx, config = {}) {
312
326
  if (!isObject(ctx)) return { ctx, config }
313
327
  const restrictions = new WeakMap()
328
+ const activeRestrictions = new Set()
314
329
  const assemblySnapshots = new WeakMap()
315
330
  const ownedVisionTools = new Map()
316
331
  let toolsView
@@ -335,6 +350,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
335
350
  restrictions,
336
351
  ownedVisionTools,
337
352
  ctx?.tools,
353
+ activeRestrictions,
338
354
  )
339
355
  }
340
356
 
@@ -354,6 +370,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
354
370
  restrictions,
355
371
  ownedVisionTools,
356
372
  ctx?.tools,
373
+ activeRestrictions,
357
374
  )
358
375
  }
359
376
  return authority
@@ -419,13 +436,30 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
419
436
  })
420
437
  ctx.on?.('agent/disposed', ({ agent }) => {
421
438
  clearAgent(agent)
422
- releaseAgentToolRestriction(agent, restrictions)
439
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
423
440
  })
424
441
  } catch {
425
442
  // Minimum Hosts may not expose every lifecycle event. Prompt assembly and
426
443
  // execution guards preserve the safety boundary without making apply fail.
427
444
  }
428
445
 
446
+ // tools.restrict() is invoked through the long-lived Agent scope so its
447
+ // disposer belongs to that scope, not to this plugin fiber. A Settings/HMR
448
+ // recompose can therefore retire this boundary while the Agent survives. If
449
+ // we do not release those masks here, the next boundary generation cannot
450
+ // discover or dispose the old WeakMap entries and Vision tools stay hidden
451
+ // even after the Session selects a Router-owned route again.
452
+ try {
453
+ if (typeof ctx.effect === 'function') {
454
+ ctx.effect(
455
+ () => () => releaseAllAgentToolRestrictions(activeRestrictions),
456
+ 'vision-router: Session Vision restriction lifecycle',
457
+ )
458
+ }
459
+ } catch {
460
+ // Agent disposal / live enable transitions still release their own masks.
461
+ }
462
+
429
463
  const wrapped = new Proxy(ctx, {
430
464
  get(target, property) {
431
465
  if (property === 'tools') {
@@ -467,6 +501,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
467
501
  restrictions,
468
502
  ownedVisionTools,
469
503
  target?.tools,
504
+ activeRestrictions,
470
505
  )
471
506
  return runWithSessionVisionModeAuthority(
472
507
  authority,