dsh-vision-router 2.2.8 → 3.0.0

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 (132) hide show
  1. package/README.md +16 -7
  2. package/README.zh.md +16 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/3.0-architecture-plan.md +252 -0
  8. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  9. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  10. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  11. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  12. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  13. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  14. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  15. package/docs/architecture/3.0-r7-public-api.md +151 -0
  16. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  17. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  18. package/docs/architecture/compat-inventory.md +26 -34
  19. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  20. package/docs/architecture/dsh-support-window.md +30 -22
  21. package/docs/architecture/final-closure-audit.md +2 -0
  22. package/docs/releases/v2.3.0.md +53 -0
  23. package/docs/releases/v3.0.0.md +85 -0
  24. package/entry.js +9 -57
  25. package/index.js +268 -761
  26. package/lib/agent-request-route-authority.js +1 -20
  27. package/lib/catalog-corrections.js +1 -2
  28. package/lib/client-maintenance-actions.js +145 -0
  29. package/lib/client-presentation-boundary-main.js +75 -17
  30. package/lib/client-presentation-boundary.js +53 -28
  31. package/lib/client.js +246 -144
  32. package/lib/core-apply-lifecycle.js +41 -0
  33. package/lib/core-cache.js +87 -0
  34. package/lib/core-primitives.js +91 -278
  35. package/lib/core-vision-surface-domain.js +63 -0
  36. package/lib/core-vision-surface.js +11 -77
  37. package/lib/delegated-call-config.js +15 -24
  38. package/lib/desktop-screenshot-tool.js +262 -0
  39. package/lib/doctor-cli.js +0 -0
  40. package/lib/doctor-runtime.js +4 -1
  41. package/lib/dsh-contract-compat.js +37 -256
  42. package/lib/dsh-host-capabilities.js +3 -9
  43. package/lib/dsh-settings-017-compat.js +17 -10
  44. package/lib/dsh-support-window.js +3 -3
  45. package/lib/file-logger.js +8 -3
  46. package/lib/grounding-coordinate-runtime.js +1 -2
  47. package/lib/http-body-limit.js +97 -79
  48. package/lib/http-compat.js +1 -2
  49. package/lib/image-offload-compat.js +30 -21
  50. package/lib/image-resource-governor.js +210 -228
  51. package/lib/legacy-global-proxy-boundary.js +46 -3
  52. package/lib/live-model-client-prelude.js +12 -4
  53. package/lib/live-model-discovery.js +134 -43
  54. package/lib/local-remote-settings-permission.js +33 -7
  55. package/lib/local-vision-stabilizer.js +31 -17
  56. package/lib/maintenance-routes.js +134 -0
  57. package/lib/native-image-coexistence.js +15 -3
  58. package/lib/ollama-cold-start.js +87 -23
  59. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  60. package/lib/proxy-dispatcher-pool.js +151 -92
  61. package/lib/proxy-routing.js +75 -51
  62. package/lib/proxy-url-compat.js +3 -10
  63. package/lib/public-config.js +36 -0
  64. package/lib/public-entry.d.ts +148 -0
  65. package/lib/public-entry.js +19 -11
  66. package/lib/remote-settings-bridge.js +11 -0
  67. package/lib/remote-settings-risk-client-boundary.js +155 -0
  68. package/lib/remote-settings-risk-confirmation.js +17 -105
  69. package/lib/replay-delegation.js +12 -7
  70. package/lib/replay-envelope-v2-compat.js +12 -7
  71. package/lib/runtime-composition.js +127 -34
  72. package/lib/runtime-i18n-boundary.js +10 -11
  73. package/lib/screenshot-source-boundary.js +2 -2
  74. package/lib/session-affinity-runtime.js +113 -97
  75. package/lib/session-affinity.js +89 -76
  76. package/lib/session-event-history-compat.js +30 -0
  77. package/lib/session-message-source-compat.js +7 -3
  78. package/lib/session-query-readers.js +263 -0
  79. package/lib/session-surface-compat.js +30 -31
  80. package/lib/session-surface-policy-domain.js +85 -0
  81. package/lib/session-surface-policy.js +2 -111
  82. package/lib/session-turn-resolver.js +2 -6
  83. package/lib/session-vision-attachment-index.js +143 -0
  84. package/lib/session-vision-delegation-boundary.js +34 -0
  85. package/lib/session-vision-delegation.js +22 -0
  86. package/lib/session-vision-index.js +179 -116
  87. package/lib/session-vision-mode-authority.js +229 -192
  88. package/lib/session-vision-mode-boundary.js +65 -22
  89. package/lib/session-vision-policy-domain.js +304 -0
  90. package/lib/session-vision-policy.js +104 -0
  91. package/lib/session-vision-state.js +368 -378
  92. package/lib/settings-client-017-compat.js +9 -7
  93. package/lib/settings-client-loader-lifecycle.js +129 -0
  94. package/lib/sharp-runtime.js +0 -7
  95. package/lib/structured-flow-hardening.js +25 -6
  96. package/lib/tesseract-exec-compat.js +9 -1
  97. package/lib/turn-budget-context.js +47 -41
  98. package/lib/update-check.js +11 -2
  99. package/lib/vision-attachment-handle-runtime.js +176 -156
  100. package/lib/vision-backend-runtime-policy.js +19 -2
  101. package/lib/vision-background-benchmark.js +41 -4
  102. package/lib/vision-background-failure-policy.js +41 -41
  103. package/lib/vision-background-stop-store-core.js +217 -0
  104. package/lib/vision-background-stop-store.js +8 -179
  105. package/lib/vision-capability-benchmark-service.js +1 -0
  106. package/lib/vision-capability-benchmark.js +11 -61
  107. package/lib/vision-capability-evidence.js +35 -0
  108. package/lib/vision-capability-fingerprint.js +72 -0
  109. package/lib/vision-capability-identity.js +116 -61
  110. package/lib/vision-capability-router.js +13 -33
  111. package/lib/vision-chunk-assembler.js +95 -0
  112. package/lib/vision-diagnostics-routes.js +246 -0
  113. package/lib/vision-evidence-guidance.js +3 -1
  114. package/lib/vision-execution-order-apply.js +45 -55
  115. package/lib/vision-execution-order.js +19 -19
  116. package/lib/vision-execution-policy.js +12 -7
  117. package/lib/vision-model-registry.js +30 -17
  118. package/lib/vision-provider-transport.js +137 -176
  119. package/lib/vision-routing-authority.js +47 -48
  120. package/lib/vision-routing-product.js +26 -29
  121. package/lib/vision-routing-settings-prelude.js +8 -3
  122. package/lib/vision-runtime-performance-store.js +166 -0
  123. package/lib/vision-runtime-performance.js +13 -142
  124. package/lib/vision-toggle-root-hardening.js +22 -5
  125. package/lib/vision-tool-runtime-boundary.js +104 -31
  126. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  127. package/lib/web/remote-settings-client.js +6 -9
  128. package/lib/web-capability-boundary.js +17 -7
  129. package/lib/wrapper-directory.js +14 -23
  130. package/package.json +56 -20
  131. package/lib/android-attachment-compat.js +0 -244
  132. package/lib/settings-client-rc8-lifecycle.js +0 -397
@@ -0,0 +1,36 @@
1
+ import z from '@deepseek-ai/schemastery';
2
+ // Increment whenever the browser-visible settings contract gains a field whose
3
+ // absence changes write semantics. The resolved schema also publishes this as
4
+ // a read-only-by-convention handshake for browser/Host compatibility checks.
5
+ export const SETTINGS_CONTRACT_REVISION = 7;
6
+ /**
7
+ * Compose DVR's public product settings onto the mature Core schema.
8
+ *
9
+ * Schemastery object schemas expose set() as the supported replacement seam.
10
+ * Mutating the supplied object is deliberate: Core later registers this exact
11
+ * Config identity with Settings, so a cloned/public-only schema would let the
12
+ * exported contract and runtime validation drift apart.
13
+ */
14
+ export function composePublicVisionConfig(coreConfig) {
15
+ if (!coreConfig || typeof coreConfig.set !== 'function') {
16
+ throw new TypeError('Vision Router public config requires a Schemastery object schema');
17
+ }
18
+ coreConfig.set('progressiveTools', z.boolean().default(false));
19
+ // Keep provider/task/turn timeout layers coherent. The whole-turn budget is
20
+ // an optional safety cap: 0 remains unlimited for long-running agent turns.
21
+ coreConfig.set('visionTaskTimeoutMs', z.number().step(1000).min(1000).max(180000).default(120000));
22
+ coreConfig.set('visionTurnBudgetMs', z.number().step(1000).min(0).max(600000).default(0));
23
+ // Both visible settings entry points edit the same Host-owned namespace.
24
+ coreConfig.set('visionDepth', z.union(['fast', 'standard', 'deep', 'custom']).default('standard'));
25
+ coreConfig.set('visionDepthMaxCalls', z.number().step(1).min(0).max(100).default(0));
26
+ // Product semantics: ordered remains the safe default. Auto execution and
27
+ // background measurement each require explicit live user authority.
28
+ coreConfig.set('routingMode', z.union(['ordered', 'auto']).default('ordered'));
29
+ coreConfig.set('routingPreference', z.union(['balanced', 'quality', 'speed', 'local']).default('balanced'));
30
+ coreConfig.set('backgroundBenchmarking', z.union(['local-free', 'all', 'off']).default('off'));
31
+ // Remote Settings remains an explicit opt-in product permission. The remote
32
+ // risk dialog is a reminder/acknowledgement, not a server-side auth proof.
33
+ coreConfig.set('allowRemoteSettings', z.boolean().default(false));
34
+ coreConfig.set('settingsContractRevision', z.number().step(1).min(1).max(SETTINGS_CONTRACT_REVISION).default(SETTINGS_CONTRACT_REVISION));
35
+ return coreConfig;
36
+ }
@@ -0,0 +1,148 @@
1
+ import '@deepseek-ai/schemastery'
2
+
3
+ export const name: 'vision-router'
4
+ export const inject: readonly ['tools', 'llm']
5
+ export const SETTINGS_CONTRACT_REVISION: 7
6
+
7
+ export const Config: Schemastery<
8
+ Record<string, unknown>,
9
+ Record<string, unknown>
10
+ >
11
+
12
+ /**
13
+ * Minimum Cordis shape the plugin requires at the package root.
14
+ *
15
+ * Every supported DSH Host line mounts a Cordis lifecycle context, so this is a
16
+ * precondition rather than an optional capability: cords are installed through
17
+ * `ctx.effect()`, and a generation that cannot own its cleanup is refused
18
+ * instead of publishing routes, wrappers or global mutations without an owner.
19
+ */
20
+ export interface CordisLifecycleContext {
21
+ readonly effect: (...args: any[]) => unknown
22
+ }
23
+
24
+ /**
25
+ * Install the plugin on a Cordis plugin context.
26
+ *
27
+ * @param ctx Host plugin context. Must expose `effect()`, otherwise a
28
+ * `TypeError` is thrown before anything is published.
29
+ * @throws {TypeError} when `ctx.effect` is not callable.
30
+ */
31
+ export function apply(
32
+ ctx: CordisLifecycleContext,
33
+ config?: Record<string, unknown>,
34
+ ): unknown
35
+
36
+ export interface LegacySessionSurfaceReplacementIntent {
37
+ readonly surfaceOp: Readonly<{
38
+ op: 'replace'
39
+ start: number
40
+ end: number
41
+ }>
42
+ readonly sourceEventSeqs: readonly [number]
43
+ }
44
+
45
+ export interface CurrentSessionSurfaceReplacementIntent {
46
+ readonly surfaceOp: Readonly<{
47
+ op: 'replace'
48
+ startSeq: number
49
+ endSeq: number
50
+ }>
51
+ readonly sourceEventSeqs: readonly [number]
52
+ }
53
+
54
+ export type SessionSurfaceReplacementIntent =
55
+ | LegacySessionSurfaceReplacementIntent
56
+ | CurrentSessionSurfaceReplacementIntent
57
+
58
+ /**
59
+ * Build the Host-owned Session surface replacement intent for one exact node.
60
+ *
61
+ * @param seq Non-negative safe integer sequence number. `-0`, fractional,
62
+ * `NaN` and `Infinity` are rejected.
63
+ * @throws {TypeError} when `seq` is not a non-negative safe integer.
64
+ */
65
+ export function sessionSurfaceReplacementIntent(
66
+ session: unknown,
67
+ seq: number,
68
+ ): SessionSurfaceReplacementIntent | undefined
69
+
70
+ export interface VisionAttachmentAdmissionLogger {
71
+ info?: (message: string, ...args: unknown[]) => unknown
72
+ warn?: (message: string, ...args: unknown[]) => unknown
73
+ }
74
+
75
+ export interface VisionAttachmentAdmissionOptions {
76
+ readonly maxImageDimension?: number
77
+ readonly normalizedImageMaxPixels?: number
78
+ readonly normalizedImageMaxDimension?: number
79
+ readonly normalizedImageMaxBytes?: number
80
+ }
81
+
82
+ export interface VisionAttachmentAdmissionResult {
83
+ readonly changed: boolean
84
+ readonly reason: string
85
+ readonly limits?: Readonly<Record<string, unknown>>
86
+ readonly normalizationPolicy?: Readonly<Record<string, unknown>>
87
+ }
88
+
89
+ export function hasBatchAttachmentContract(ctx: unknown): boolean
90
+
91
+ export function hostOwnsOfficialDeepSeekProvider(ctx: unknown): boolean
92
+
93
+ export function ensureVisionAttachmentAdmissionPolicy(
94
+ ctx: unknown,
95
+ logger?: VisionAttachmentAdmissionLogger,
96
+ options?: VisionAttachmentAdmissionOptions,
97
+ ): VisionAttachmentAdmissionResult
98
+
99
+ export function installVisionAttachmentAdmissionPolicy(
100
+ ctx: unknown,
101
+ logger?: VisionAttachmentAdmissionLogger,
102
+ options?: VisionAttachmentAdmissionOptions,
103
+ ): VisionAttachmentAdmissionResult
104
+
105
+ /**
106
+ * Service reads this helper performs on the given context.
107
+ *
108
+ * `llm` is read through the non-owning `get()` probe when the context provides
109
+ * one; a context without it must not throw on an unresolved `llm` property.
110
+ */
111
+ export interface HostOwnershipContext {
112
+ readonly get?: (name: string) => unknown
113
+ readonly llm?: unknown
114
+ }
115
+
116
+ export function protectHostProviderOwnership<Context extends HostOwnershipContext>(
117
+ ctx: Context,
118
+ ): Context
119
+
120
+ export interface HostSettingsCompatibilityOptions {
121
+ readonly namespace: string
122
+ readonly Config: unknown
123
+ readonly installSettingsSection?: (
124
+ ctx: unknown,
125
+ namespace: unknown,
126
+ Config: unknown,
127
+ entryConfig: Record<string, unknown>,
128
+ hooks: unknown,
129
+ ) => unknown
130
+ }
131
+
132
+ /** Context shape required to register and own the compatibility settings section. */
133
+ export interface HostSettingsCompatibilityContext {
134
+ readonly inject?: (...args: any[]) => unknown
135
+ }
136
+
137
+ /**
138
+ * Present DVR's settings face on Hosts whose Settings service predates the
139
+ * current contract.
140
+ *
141
+ * @throws {TypeError} when `ctx.inject` is not callable, before any settings
142
+ * section or watcher is registered.
143
+ */
144
+ export function installHostSettingsCompatibility<Context extends HostSettingsCompatibilityContext>(
145
+ ctx: Context,
146
+ entryConfig: Record<string, unknown>,
147
+ options: HostSettingsCompatibilityOptions,
148
+ ): Context
@@ -2,10 +2,7 @@ import './abort-signal-compat.js'
2
2
  import * as base from '../entry.js'
3
3
  import { createVisionToggleRootHardening } from './vision-toggle-root-hardening.js'
4
4
  import { contextWithVisionRoutingTopologyRefresh } from './vision-routing-topology-refresh.js'
5
- import {
6
- createVisionProviderTransport,
7
- installVisionProviderTransport,
8
- } from './vision-provider-transport.js'
5
+ import { createVisionProviderTransport } from './vision-provider-transport.js'
9
6
  import { installLegacyGlobalProxyBoundary } from './legacy-global-proxy-boundary.js'
10
7
  import { installDsh017SettingsCompatibility } from './dsh-settings-017-compat.js'
11
8
  import { installDsh017RootLocalSettingsTransport } from './web/dsh-settings-017-root-transport.js'
@@ -37,6 +34,16 @@ function liveVisionConfig(ctx, fallback) {
37
34
  * settings therefore leave DSH/Host as the sole network authority.
38
35
  */
39
36
  export function apply(ctx, config = {}) {
37
+ // Cordis effect() is a required plugin-fiber primitive across the entire
38
+ // supported DSH window, not an optional capability. Refuse malformed/partial
39
+ // Hosts before any DVR route, wrapper, transport, watcher or global mutation
40
+ // can be published without a lifecycle owner.
41
+ let effect
42
+ try { effect = ctx?.effect } catch (error) { throw error }
43
+ if (typeof effect !== 'function') {
44
+ throw new TypeError('vision-router requires a Cordis lifecycle context with effect()')
45
+ }
46
+
40
47
  // DSH 0.1.7 replaces SettingsProvider.register() with profile-owned
41
48
  // ConfigEditor writes for ordinary fields. Feature-detect that exact contract
42
49
  // and present DVR's mature settings face without changing older Hosts.
@@ -59,25 +66,26 @@ export function apply(ctx, config = {}) {
59
66
  ctx: hardening.ctx,
60
67
  config: () => liveVisionConfig(hardening.ctx, config),
61
68
  })
62
- const releaseTransportRegistry = installVisionProviderTransport(transport)
63
69
  let transportReleased = false
70
+ let transportReleasePromise
64
71
  const releaseTransport = () => {
65
- if (transportReleased) return
72
+ if (transportReleasePromise) return transportReleasePromise
73
+ if (transportReleased) return Promise.resolve()
66
74
  transportReleased = true
67
- releaseTransportRegistry()
68
- void transport.dispose()
75
+ transportReleasePromise = Promise.resolve().then(() => transport.dispose())
76
+ return transportReleasePromise
69
77
  }
70
78
  try {
71
- runtimeCtx?.effect?.(
79
+ runtimeCtx.effect(
72
80
  () => releaseTransport,
73
81
  'vision-router: provider transport',
74
82
  )
75
- const result = base.apply(runtimeCtx, hardening.config)
83
+ const result = base.apply(runtimeCtx, hardening.config, { providerTransport: transport })
76
84
  installLegacyGlobalProxyBoundary(runtimeCtx, hardening.config)
77
85
  hardening.installClientBoundary()
78
86
  return result
79
87
  } catch (error) {
80
- releaseTransport()
88
+ void releaseTransport().catch(() => {})
81
89
  throw error
82
90
  }
83
91
  }
@@ -138,6 +138,17 @@ function conflictResult(error) {
138
138
  }
139
139
  }
140
140
 
141
+ /**
142
+ * PRODUCT CONTRACT (2.3+): remote-settings enablement is reminder-only.
143
+ *
144
+ * `acceptedRisk === true` is an acknowledgement bit for the UX flow, not an
145
+ * authentication/authorization credential, proof of local presence, challenge
146
+ * response, one-time secret, or administrator approval. Do not strengthen this
147
+ * endpoint into a second security gate unless the product policy is explicitly
148
+ * changed. The real capability boundary is the narrow remote mutable-field
149
+ * allow-list plus the fields that remain local-only. The historical RPC/function
150
+ * name uses "authorize"; do not infer security-auth semantics from that name.
151
+ */
141
152
  export async function authorizeRemoteSettingsAfterRiskConfirmation(settings, payload, logger) {
142
153
  if (!payload || typeof payload !== 'object' || Array.isArray(payload) || payload.acceptedRisk !== true) {
143
154
  return badRequest('acceptedRisk must be true')
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Current-product remote Settings risk behavior shared by every browser
3
+ * attachment lifecycle. Host loader/version compatibility stays outside this owner.
4
+ *
5
+ * PRODUCT CONTRACT (2.3+): acceptedRisk is reminder/friction only. It is not
6
+ * authentication, authorization, local-presence proof, or a credential.
7
+ */
8
+ export function createRemoteSettingsRiskClientBoundary(options = {}) {
9
+ var confirmImpl = options.confirmImpl
10
+ var alertImpl = options.alertImpl
11
+ var locale = options.locale
12
+ var location = options.location
13
+ var normalizeLoopback = options.normalizeLoopback === true
14
+ var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined
15
+ var rpcCache = typeof WeakMap === 'function' ? new WeakMap() : undefined
16
+ var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined
17
+ var authorizationByRpc = typeof WeakMap === 'function' ? new WeakMap() : undefined
18
+ var CHANNEL = '/vision-router-settings'
19
+ var AUTHORIZE_ENDPOINT = 'authorize'
20
+
21
+ function localeValue() {
22
+ try {
23
+ var value = typeof locale === 'function' ? locale() : locale
24
+ return String(value || '').toLowerCase()
25
+ } catch (_) {
26
+ return ''
27
+ }
28
+ }
29
+
30
+ function chineseLocale() {
31
+ var lang = localeValue()
32
+ return lang === '' || lang.indexOf('zh') === 0
33
+ }
34
+
35
+ function riskMessage() {
36
+ return chineseLocale()
37
+ ? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
38
+ : 'Enable remote settings?\n\nThis page is accessing DSH through a remote address. After enabling this, clients that can reach this trusted host can change the Vision Router settings exposed by the remote allow-list.\n\nDSH trustedHosts is not authentication. Continue only if you trust the current network and its users. Sensitive settings outside the remote allow-list, including API keys, HTTP-provider credentials, local Ollama / LM Studio, and artifact paths, remain unavailable remotely.\n\nAllow remote settings?'
39
+ }
40
+
41
+ function isLoopbackLocation(locationLike) {
42
+ var hostname = locationLike && typeof locationLike.hostname === 'string'
43
+ ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
44
+ : ''
45
+ var protocol = locationLike && typeof locationLike.protocol === 'string'
46
+ ? locationLike.protocol.toLowerCase()
47
+ : ''
48
+ if (protocol === 'dsh-app:' && hostname === 'app') return true
49
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true
50
+ return /^127(?:\.\d{1,3}){3}$/.test(hostname)
51
+ }
52
+
53
+ function currentLocation() {
54
+ try { return typeof location === 'function' ? location() : location }
55
+ catch (_) { return undefined }
56
+ }
57
+
58
+ function isPermissionDisabledDescribe(channel, endpoint, result) {
59
+ return channel === CHANNEL
60
+ && endpoint === 'describe'
61
+ && result && result.ok === true
62
+ && result.value && result.value.enabled !== true
63
+ && result.value.reason === 'permission-disabled'
64
+ }
65
+
66
+ async function authorizeAndRefresh(target, call) {
67
+ var existing = authorizationByRpc && authorizationByRpc.get(target)
68
+ if (existing) return existing
69
+ var pending = (async function() {
70
+ if (typeof confirmImpl !== 'function' || confirmImpl(riskMessage()) !== true) return undefined
71
+ var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true })
72
+ if (!authorized || authorized.ok !== true) {
73
+ var message = authorized && authorized.error && authorized.error.message
74
+ ? authorized.error.message
75
+ : 'Vision Router could not enable remote settings.'
76
+ try { if (typeof alertImpl === 'function') alertImpl(message) } catch (_) {}
77
+ return undefined
78
+ }
79
+ return call.call(target, CHANNEL, 'describe', {})
80
+ })()
81
+ if (authorizationByRpc) authorizationByRpc.set(target, pending)
82
+ try { return await pending }
83
+ finally {
84
+ if (authorizationByRpc && authorizationByRpc.get(target) === pending) {
85
+ authorizationByRpc.delete(target)
86
+ }
87
+ }
88
+ }
89
+
90
+ function wrapRpc(rpc) {
91
+ if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc
92
+ if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc)
93
+ var wrapped = new Proxy(rpc, {
94
+ get: function(target, property) {
95
+ if (property === 'call') {
96
+ var call = Reflect.get(target, property, target)
97
+ if (typeof call !== 'function') return call
98
+ return async function(channel, endpoint) {
99
+ var result = await call.apply(target, arguments)
100
+ if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result
101
+ var refreshed = await authorizeAndRefresh(target, call)
102
+ return refreshed || result
103
+ }
104
+ }
105
+ var value = Reflect.get(target, property, target)
106
+ return typeof value === 'function' ? value.bind(target) : value
107
+ }
108
+ })
109
+ if (rpcCache) rpcCache.set(rpc, wrapped)
110
+ return wrapped
111
+ }
112
+
113
+ function wrapConnection(connection) {
114
+ if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection
115
+ if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection)
116
+ var wrapped = new Proxy(connection, {
117
+ get: function(target, property) {
118
+ if (property === 'isLoopback' && normalizeLoopback && isLoopbackLocation(currentLocation())) return true
119
+ if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target))
120
+ var value = Reflect.get(target, property, target)
121
+ return typeof value === 'function' ? value.bind(target) : value
122
+ }
123
+ })
124
+ if (connectionCache) connectionCache.set(connection, wrapped)
125
+ return wrapped
126
+ }
127
+
128
+ function wrapContext(ctx) {
129
+ if (!ctx || typeof ctx !== 'object') return ctx
130
+ if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx)
131
+ var wrapped = new Proxy(ctx, {
132
+ get: function(target, property) {
133
+ if (property === 'get') {
134
+ var get = Reflect.get(target, property, target)
135
+ if (typeof get !== 'function') return get
136
+ return function(name) {
137
+ var value = get.apply(target, arguments)
138
+ return name === 'connection' ? wrapConnection(value) : value
139
+ }
140
+ }
141
+ var value = Reflect.get(target, property, target)
142
+ return typeof value === 'function' ? value.bind(target) : value
143
+ }
144
+ })
145
+ if (contextCache) contextCache.set(ctx, wrapped)
146
+ return wrapped
147
+ }
148
+
149
+ return { wrapContext: wrapContext }
150
+ }
151
+
152
+ export const REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE =
153
+ 'var createRemoteSettingsRiskClientBoundary = (' +
154
+ createRemoteSettingsRiskClientBoundary.toString() +
155
+ ');'
@@ -1,113 +1,25 @@
1
1
  import { htmlHasScriptMarker } from './html-script-marker.js'
2
+ import { REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE } from './remote-settings-risk-client-boundary.js'
2
3
 
3
4
  const RISK_PRELUDE_MARK = 'data-vision-router-remote-settings-risk-confirmation'
4
5
 
5
6
  export const REMOTE_SETTINGS_RISK_CLIENT_PRELUDE = String.raw`(function(){
6
7
  'use strict';
7
- var CHANNEL = '/vision-router-settings';
8
- var AUTHORIZE_ENDPOINT = 'authorize';
9
- var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
10
- var rpcCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
11
- var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
12
- var authorizationPromise;
13
-
14
- function chineseLocale() {
15
- var lang = '';
16
- try {
17
- lang = String((document && document.documentElement && document.documentElement.lang) || (navigator && navigator.language) || '').toLowerCase();
18
- } catch (_) {}
19
- return lang === '' || lang.indexOf('zh') === 0;
20
- }
21
-
22
- function riskMessage() {
23
- return chineseLocale()
24
- ? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
25
- : 'Enable remote settings?\n\nThis page is accessing DSH through a remote address. After enabling this, clients that can reach this trusted host can change the Vision Router settings exposed by the remote allow-list.\n\nDSH trustedHosts is not authentication. Continue only if you trust the current network and its users. Sensitive settings outside the remote allow-list, including API keys, HTTP-provider credentials, local Ollama / LM Studio, and artifact paths, remain unavailable remotely.\n\nAllow remote settings?';
26
- }
27
-
28
- function isPermissionDisabledDescribe(channel, endpoint, result) {
29
- return channel === CHANNEL
30
- && endpoint === 'describe'
31
- && result && result.ok === true
32
- && result.value && result.value.enabled !== true
33
- && result.value.reason === 'permission-disabled';
34
- }
35
-
36
- async function authorizeAndRefresh(target, call) {
37
- if (authorizationPromise) return authorizationPromise;
38
- authorizationPromise = (async function(){
39
- if (typeof window.confirm !== 'function' || window.confirm(riskMessage()) !== true) return undefined;
40
- var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true });
41
- if (!authorized || authorized.ok !== true) {
42
- var message = authorized && authorized.error && authorized.error.message
43
- ? authorized.error.message
44
- : 'Vision Router could not enable remote settings.';
45
- try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
46
- return undefined;
47
- }
48
- return call.call(target, CHANNEL, 'describe', {});
49
- })();
50
- try { return await authorizationPromise; }
51
- finally { authorizationPromise = undefined; }
52
- }
53
-
54
- function wrapRpc(rpc) {
55
- if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc;
56
- if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc);
57
- var wrapped = new Proxy(rpc, {
58
- get: function(target, property) {
59
- if (property === 'call') {
60
- var call = Reflect.get(target, property, target);
61
- if (typeof call !== 'function') return call;
62
- return async function(channel, endpoint) {
63
- var result = await call.apply(target, arguments);
64
- if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result;
65
- var refreshed = await authorizeAndRefresh(target, call);
66
- return refreshed || result;
67
- };
68
- }
69
- var value = Reflect.get(target, property, target);
70
- return typeof value === 'function' ? value.bind(target) : value;
71
- }
72
- });
73
- if (rpcCache) rpcCache.set(rpc, wrapped);
74
- return wrapped;
75
- }
76
-
77
- function wrapConnection(connection) {
78
- if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
79
- if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
80
- var wrapped = new Proxy(connection, {
81
- get: function(target, property) {
82
- if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
83
- var value = Reflect.get(target, property, target);
84
- return typeof value === 'function' ? value.bind(target) : value;
85
- }
86
- });
87
- if (connectionCache) connectionCache.set(connection, wrapped);
88
- return wrapped;
89
- }
90
-
91
- function wrapContext(ctx) {
92
- if (!ctx || typeof ctx !== 'object') return ctx;
93
- if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx);
94
- var wrapped = new Proxy(ctx, {
95
- get: function(target, property) {
96
- if (property === 'get') {
97
- var get = Reflect.get(target, property, target);
98
- if (typeof get !== 'function') return get;
99
- return function(name) {
100
- var value = get.apply(target, arguments);
101
- return name === 'connection' ? wrapConnection(value) : value;
102
- };
103
- }
104
- var value = Reflect.get(target, property, target);
105
- return typeof value === 'function' ? value.bind(target) : value;
106
- }
107
- });
108
- if (contextCache) contextCache.set(ctx, wrapped);
109
- return wrapped;
110
- }
8
+ ${REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE}
9
+ var riskBoundary = createRemoteSettingsRiskClientBoundary({
10
+ confirmImpl: function(message) {
11
+ return typeof window.confirm === 'function' && window.confirm(message) === true;
12
+ },
13
+ alertImpl: function(message) {
14
+ try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
15
+ },
16
+ locale: function() {
17
+ try {
18
+ return String((document && document.documentElement && document.documentElement.lang)
19
+ || (navigator && navigator.language) || '');
20
+ } catch (_) { return ''; }
21
+ }
22
+ });
111
23
 
112
24
  function patchLoader(loader) {
113
25
  if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterRemoteRiskConfirmation) return;
@@ -122,7 +34,7 @@ export const REMOTE_SETTINGS_RISK_CLIENT_PRELUDE = String.raw`(function(){
122
34
  var apply = exports.apply;
123
35
  var wrappedApply = function(ctx) {
124
36
  var rest = Array.prototype.slice.call(arguments, 1);
125
- return apply.apply(exports, [wrapContext(ctx)].concat(rest));
37
+ return apply.apply(exports, [riskBoundary.wrapContext(ctx)].concat(rest));
126
38
  };
127
39
  Object.defineProperty(wrappedApply, '__visionRouterRemoteRiskConfirmation', { value: true });
128
40
  exports.apply = wrappedApply;
@@ -548,15 +548,20 @@ export function contextWithDelegatedReplay(ctx, options = {}) {
548
548
  },
549
549
  })
550
550
  wrappedContexts.set(ctx, wrapped)
551
+ let lifecycleOwned = false
551
552
  try {
552
- ctx.effect?.(
553
- () => () => {
554
- if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
555
- },
556
- 'vision-router: delegated replay context lifecycle',
557
- )
553
+ if (typeof ctx.effect === 'function') {
554
+ ctx.effect(
555
+ () => () => {
556
+ if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
557
+ },
558
+ 'vision-router: delegated replay context lifecycle',
559
+ )
560
+ lifecycleOwned = true
561
+ }
558
562
  } catch {
559
- /* lifecycle hardening must not block plugin apply */
563
+ // The current caller may still use this private view.
560
564
  }
565
+ if (!lifecycleOwned && wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
561
566
  return wrapped
562
567
  }
@@ -88,15 +88,20 @@ export function contextWithReplayEnvelopeV2Compat(ctx) {
88
88
  },
89
89
  })
90
90
  wrappedContexts.set(ctx, wrapped)
91
+ let lifecycleOwned = false
91
92
  try {
92
- ctx.effect?.(
93
- () => () => {
94
- if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
95
- },
96
- 'vision-router: rc7 replay envelope compatibility lifecycle',
97
- )
93
+ if (typeof ctx.effect === 'function') {
94
+ ctx.effect(
95
+ () => () => {
96
+ if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
97
+ },
98
+ 'vision-router: rc7 replay envelope compatibility lifecycle',
99
+ )
100
+ lifecycleOwned = true
101
+ }
98
102
  } catch {
99
- /* lifecycle hardening must not block plugin apply */
103
+ // The current caller can still use this one-shot compatibility view.
100
104
  }
105
+ if (!lifecycleOwned && wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
101
106
  return wrapped
102
107
  }