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
@@ -1,244 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
-
3
- const wrappedContexts = new WeakMap()
4
- const MIB = 1024 * 1024
5
-
6
- export const DEFAULT_ANDROID_TRANSIENT_LIMITS = Object.freeze({
7
- maxEntries: 64,
8
- maxBytes: 128 * MIB,
9
- maxSingleBytes: 64 * MIB,
10
- })
11
-
12
- function nonEmptyString(value) {
13
- return typeof value === 'string' && value.length > 0
14
- }
15
-
16
- function positiveInteger(value, fallback) {
17
- const n = Number(value)
18
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback
19
- }
20
-
21
- export function isAndroidTermuxRuntime(options = {}) {
22
- const platform = options.platform ?? process.platform
23
- const env = options.env ?? process.env
24
- if (platform === 'android') return true
25
- if (nonEmptyString(env?.TERMUX_VERSION)) return true
26
- const prefix = nonEmptyString(env?.PREFIX) ? env.PREFIX : ''
27
- return prefix.includes('/com.termux/') || prefix.includes('\\com.termux\\')
28
- }
29
-
30
- export function isPermissionBoundaryError(error) {
31
- let current = error
32
- const seen = new Set()
33
- for (let depth = 0; current && depth < 8; depth += 1) {
34
- if ((typeof current === 'object' || typeof current === 'function') && seen.has(current)) break
35
- if (typeof current === 'object' || typeof current === 'function') seen.add(current)
36
- const code = current && typeof current === 'object' ? current.code : undefined
37
- if (code === 'EACCES' || code === 'EPERM') return true
38
- current = current && typeof current === 'object' ? current.cause : undefined
39
- }
40
- return false
41
- }
42
-
43
- function displayName(value) {
44
- if (!nonEmptyString(value)) return undefined
45
- const leaf = value.slice(Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\')) + 1)
46
- const clean = leaf.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 255)
47
- return clean === '' ? undefined : clean
48
- }
49
-
50
- async function probeImage(data, injectedProbe) {
51
- if (typeof injectedProbe === 'function') return injectedProbe(data)
52
- const mod = await import('sharp')
53
- const sharp = mod.default ?? mod
54
- const metadata = await sharp(data, { failOn: 'none' }).metadata()
55
- const width = Number(metadata.width ?? 0)
56
- const height = Number(metadata.height ?? 0)
57
- if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
58
- throw new Error('vision-router: Android attachment fallback could not read image dimensions')
59
- }
60
- return { width, height }
61
- }
62
-
63
- function transientTooLargeError(bytes, limit) {
64
- const error = new Error(
65
- `vision-router: Android attachment fallback refuses to keep a ${bytes}-byte image in process memory (limit ${limit} bytes)`,
66
- )
67
- error.code = 'ANDROID_TRANSIENT_ATTACHMENT_TOO_LARGE'
68
- return error
69
- }
70
-
71
- async function transientStoredImage(input, injectedProbe, maxSingleBytes) {
72
- const data = input?.data
73
- if (!(data instanceof Uint8Array) || data.byteLength === 0) {
74
- throw new Error('vision-router: Android attachment fallback received empty image bytes')
75
- }
76
- // Enforce the byte boundary BEFORE probing with sharp. Metadata decode can
77
- // itself allocate native memory, so rejecting after probeImage() would still
78
- // allow a single pathological input to cross the mobile memory boundary.
79
- if (data.byteLength > maxSingleBytes) {
80
- throw transientTooLargeError(data.byteLength, maxSingleBytes)
81
- }
82
- const mediaType = input?.mediaType
83
- if (!nonEmptyString(mediaType)) {
84
- throw new Error('vision-router: Android attachment fallback received no media type')
85
- }
86
- const { width, height } = await probeImage(data, injectedProbe)
87
- const sha256 = createHash('sha256').update(data).digest('hex')
88
- const name = displayName(input?.name)
89
- const ref = {
90
- attachmentId: `sha256:${sha256}`,
91
- mediaType,
92
- bytes: data.byteLength,
93
- width,
94
- height,
95
- ...(name !== undefined ? { name } : {}),
96
- }
97
- return { ref, data }
98
- }
99
-
100
- /** Byte-weighted LRU for the Android process-local durability fallback. */
101
- export function createTransientAttachmentCache(options = {}) {
102
- const maxEntries = positiveInteger(options.maxEntries, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxEntries)
103
- const maxBytes = positiveInteger(options.maxBytes, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxBytes)
104
- const map = new Map()
105
- let bytes = 0
106
-
107
- const remove = (id) => {
108
- const old = map.get(id)
109
- if (old === undefined) return false
110
- bytes = Math.max(0, bytes - Number(old.data?.byteLength ?? 0))
111
- return map.delete(id)
112
- }
113
-
114
- const trim = () => {
115
- while (map.size > maxEntries || bytes > maxBytes) {
116
- const oldest = map.keys().next().value
117
- if (oldest === undefined) break
118
- remove(oldest)
119
- }
120
- }
121
-
122
- return {
123
- get(id) {
124
- const stored = map.get(id)
125
- if (stored === undefined) return undefined
126
- // Refresh recency so an actively used image survives pressure from newer
127
- // one-shot attachments.
128
- map.delete(id)
129
- map.set(id, stored)
130
- return stored
131
- },
132
- set(stored) {
133
- const id = String(stored?.ref?.attachmentId ?? '')
134
- const weight = Number(stored?.data?.byteLength ?? 0)
135
- if (id === '' || !Number.isFinite(weight) || weight <= 0 || weight > maxBytes) return false
136
- remove(id)
137
- map.set(id, stored)
138
- bytes += weight
139
- trim()
140
- return map.has(id)
141
- },
142
- stats() {
143
- return { entries: map.size, bytes, maxEntries, maxBytes }
144
- },
145
- }
146
- }
147
-
148
- /**
149
- * Android/Termux compatibility shim for deepseek-harness attachment-local.
150
- *
151
- * Current DSH attachment-local proves directory durability by fsyncing every
152
- * ancestor from DSH_HOME to the filesystem root. Android SELinux lets Termux
153
- * access its own /data/data/com.termux subtree but intentionally denies opening
154
- * /data/data itself, so saveImage() is wrapped as ATTACHMENT_WRITE_FAILED even
155
- * though the image bytes are otherwise readable.
156
- *
157
- * Keep the host service authoritative. Only when an Android runtime reaches an
158
- * EACCES/EPERM boundary do Vision Router's own calls fall back to a byte- and
159
- * count-bounded, process-local content-addressed attachment. The paired
160
- * readImage() wrapper makes the direct vision-http path and plugin-side bridge
161
- * code consume those bytes normally. Once upstream accepts the save, this path
162
- * is never used.
163
- */
164
- export function installAndroidAttachmentCompat(ctx, logger, options = {}) {
165
- if (!ctx || typeof ctx !== 'object' || !isAndroidTermuxRuntime(options.runtime)) return ctx
166
- const cached = wrappedContexts.get(ctx)
167
- if (cached) return cached
168
-
169
- let attachments
170
- try {
171
- attachments = typeof ctx.get === 'function' ? ctx.get('attachments') : undefined
172
- } catch {
173
- return ctx
174
- }
175
- if (!attachments || typeof attachments.saveImage !== 'function' || typeof attachments.readImage !== 'function') {
176
- return ctx
177
- }
178
-
179
- const maxTransientBytes = positiveInteger(
180
- options.maxTransientBytes,
181
- DEFAULT_ANDROID_TRANSIENT_LIMITS.maxBytes,
182
- )
183
- const maxTransientAttachments = positiveInteger(
184
- options.maxTransientAttachments,
185
- DEFAULT_ANDROID_TRANSIENT_LIMITS.maxEntries,
186
- )
187
- const maxTransientSingleBytes = Math.min(
188
- maxTransientBytes,
189
- positiveInteger(options.maxTransientSingleBytes, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxSingleBytes),
190
- )
191
- const transient = createTransientAttachmentCache({
192
- maxEntries: maxTransientAttachments,
193
- maxBytes: maxTransientBytes,
194
- })
195
-
196
- const wrappedAttachments = new Proxy(attachments, {
197
- get(target, property) {
198
- if (property === 'saveImage') {
199
- return async (input) => {
200
- try {
201
- return await target.saveImage(input)
202
- } catch (error) {
203
- if (!isPermissionBoundaryError(error)) throw error
204
- const stored = await transientStoredImage(input, options.probeImage, maxTransientSingleBytes)
205
- if (!transient.set(stored)) {
206
- throw transientTooLargeError(stored.data.byteLength, maxTransientBytes)
207
- }
208
- try {
209
- logger?.warn?.(
210
- 'vision-router: Android/Termux attachment save hit an inaccessible ancestor; using a bounded process-local image ref until DSH attachment-local is fixed upstream',
211
- )
212
- } catch {
213
- /* diagnostics must never block the fallback */
214
- }
215
- return stored.ref
216
- }
217
- }
218
- }
219
- if (property === 'readImage') {
220
- return async (ref, signal) => {
221
- signal?.throwIfAborted?.()
222
- const id = String(ref?.attachmentId ?? '')
223
- const stored = transient.get(id)
224
- if (stored !== undefined) return stored
225
- return target.readImage(ref, signal)
226
- }
227
- }
228
- const value = Reflect.get(target, property, target)
229
- return typeof value === 'function' ? value.bind(target) : value
230
- },
231
- })
232
-
233
- const wrapped = new Proxy(ctx, {
234
- get(target, property) {
235
- if (property === 'get') {
236
- return (name) => (name === 'attachments' ? wrappedAttachments : target.get(name))
237
- }
238
- const value = Reflect.get(target, property, target)
239
- return typeof value === 'function' ? value.bind(target) : value
240
- },
241
- })
242
- wrappedContexts.set(ctx, wrapped)
243
- return wrapped
244
- }
@@ -1,397 +0,0 @@
1
- import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
-
3
- const SETTINGS_RC8_MARK = 'data-vision-router-settings-rc8-lifecycle'
4
-
5
- /**
6
- * rc.8 replaces ModuleLoader.load() inside loader.create(). The original local
7
- * permission and remote-risk shims predate that lifecycle and are intentionally
8
- * left untouched for rc.6/rc.7. This narrow safety net activates only when a
9
- * create() transition exists, then re-installs both settings context wrappers
10
- * on the new live loader.
11
- */
12
- export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
13
- 'use strict';
14
- var TARGET = 'dsh-vision-router';
15
- var FIELD = 'allowRemoteSettings';
16
- var ENDPOINT = '/_dsh/vision-router/remote-settings-permission';
17
- var CHANNEL = '/vision-router-settings';
18
- var AUTHORIZE_ENDPOINT = 'authorize';
19
- var scopeCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
20
- var binderCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
21
- var localContextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
22
- var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
23
- var rpcCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
24
- var riskContextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
25
- var authorizationPromise;
26
-
27
- function normalizePermissionValue(value) {
28
- if (typeof value === 'boolean') return value;
29
- if (value === 'true') return true;
30
- if (value === 'false') return false;
31
- return undefined;
32
- }
33
-
34
- function isLoopbackLocation(locationLike) {
35
- var hostname = locationLike && typeof locationLike.hostname === 'string'
36
- ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
37
- : '';
38
- var protocol = locationLike && typeof locationLike.protocol === 'string'
39
- ? locationLike.protocol.toLowerCase()
40
- : '';
41
- if (protocol === 'dsh-app:' && hostname === 'app') return true;
42
- if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
43
- return /^127(?:\.\d{1,3}){3}$/.test(hostname);
44
- }
45
-
46
- async function writePermission(operation, value, revision) {
47
- if (typeof fetch !== 'function') throw new Error('Vision Router local settings transport is unavailable');
48
- var payload = { operation: operation };
49
- if (operation === 'set') payload.value = value;
50
- if (Number.isInteger(revision) && revision >= 0) payload.expectedRevision = revision;
51
- var response = await fetch(ENDPOINT, {
52
- method: 'POST',
53
- headers: { 'content-type': 'application/json', accept: 'application/json' },
54
- body: JSON.stringify(payload)
55
- });
56
- var body;
57
- try { body = await response.json(); } catch (_) { body = undefined; }
58
- if (!response.ok || !body || body.ok !== true) {
59
- var message = body && body.error && body.error.message ? body.error.message : ('HTTP ' + response.status);
60
- var error = new Error(message);
61
- if (body && body.error && body.error.code) error.code = body.error.code;
62
- throw error;
63
- }
64
- return body.value;
65
- }
66
-
67
- function wrapScope(scope) {
68
- if (!scope || (typeof scope !== 'object' && typeof scope !== 'function')) return scope;
69
- if (scopeCache && scopeCache.has(scope)) return scopeCache.get(scope);
70
- var overlay;
71
- var cachedRawSnapshot;
72
- var cachedOverlayKey;
73
- var cachedClientSnapshot;
74
-
75
- function overlayKey() {
76
- if (!overlay) return 'none';
77
- return (overlay.present ? '1' : '0') + ':' + String(overlay.value) + ':' + String(overlay.revision);
78
- }
79
-
80
- function rawSnapshot() {
81
- try { return typeof scope.getSnapshot === 'function' ? scope.getSnapshot() : undefined; } catch (_) { return undefined; }
82
- }
83
-
84
- function snapshotWithOverlay(snapshot) {
85
- if (!overlay || !snapshot || typeof snapshot !== 'object') return snapshot;
86
- var user = snapshot.user && typeof snapshot.user === 'object' && !Array.isArray(snapshot.user) ? snapshot.user : {};
87
- var present = Object.prototype.hasOwnProperty.call(user, FIELD);
88
- var matches = overlay.present ? present && user[FIELD] === overlay.value : !present;
89
- if (matches && (!Number.isInteger(overlay.revision) || !Number.isInteger(snapshot.revision) || snapshot.revision >= overlay.revision)) {
90
- overlay = undefined;
91
- return snapshot;
92
- }
93
- var nextUser = Object.assign({}, user);
94
- if (overlay.present) nextUser[FIELD] = overlay.value;
95
- else delete nextUser[FIELD];
96
- var nextValue = snapshot.value && typeof snapshot.value === 'object' && !Array.isArray(snapshot.value)
97
- ? Object.assign({}, snapshot.value)
98
- : snapshot.value;
99
- if (nextValue && typeof nextValue === 'object') nextValue[FIELD] = overlay.present ? overlay.value : false;
100
- return Object.assign({}, snapshot, {
101
- user: nextUser,
102
- value: nextValue,
103
- revision: Number.isInteger(overlay.revision) ? overlay.revision : snapshot.revision
104
- });
105
- }
106
-
107
- function snapshotForClient(snapshot) {
108
- var keyBefore = overlayKey();
109
- if (snapshot === cachedRawSnapshot && keyBefore === cachedOverlayKey) return cachedClientSnapshot;
110
- var effective = snapshotWithOverlay(snapshot);
111
- var keyAfter = overlayKey();
112
- if (snapshot === cachedRawSnapshot && keyAfter === cachedOverlayKey) return cachedClientSnapshot;
113
- if (!effective || typeof effective !== 'object' || effective.mode !== 'host') {
114
- cachedRawSnapshot = snapshot;
115
- cachedOverlayKey = keyAfter;
116
- cachedClientSnapshot = effective;
117
- return effective;
118
- }
119
- var next = effective;
120
- var value = effective.value;
121
- if (value && typeof value === 'object' && !Array.isArray(value)
122
- && Object.prototype.hasOwnProperty.call(value, FIELD)
123
- && typeof value[FIELD] === 'boolean') {
124
- value = Object.assign({}, value, { [FIELD]: value[FIELD] ? 'true' : '' });
125
- next = Object.assign({}, next, { value: value });
126
- }
127
- var user = effective.user;
128
- if (user && typeof user === 'object' && !Array.isArray(user)
129
- && Object.prototype.hasOwnProperty.call(user, FIELD)
130
- && typeof user[FIELD] === 'boolean') {
131
- user = Object.assign({}, user, { [FIELD]: user[FIELD] ? 'true' : 'false' });
132
- next = Object.assign({}, next, { user: user });
133
- }
134
- cachedRawSnapshot = snapshot;
135
- cachedOverlayKey = keyAfter;
136
- cachedClientSnapshot = next;
137
- return next;
138
- }
139
-
140
- var wrapped = new Proxy(scope, {
141
- get: function(target, property) {
142
- if (property === 'getSnapshot') return function(){ return snapshotForClient(rawSnapshot()); };
143
- if (property === 'set') {
144
- var set = Reflect.get(target, property, target);
145
- return async function(field, value) {
146
- var snapshot = rawSnapshot();
147
- if (field === FIELD && snapshot && snapshot.mode === 'host') {
148
- var normalized = normalizePermissionValue(value);
149
- if (normalized === undefined) throw new TypeError('allowRemoteSettings must be boolean');
150
- var result = await writePermission('set', normalized, snapshot.revision);
151
- overlay = { present: true, value: normalized, revision: result && result.revision };
152
- cachedRawSnapshot = undefined;
153
- if (typeof target.load === 'function') await target.load();
154
- return;
155
- }
156
- return typeof set === 'function' ? set.apply(target, arguments) : undefined;
157
- };
158
- }
159
- if (property === 'unset') {
160
- var unset = Reflect.get(target, property, target);
161
- return async function(field) {
162
- var snapshot = rawSnapshot();
163
- if (field === FIELD && snapshot && snapshot.mode === 'host') {
164
- var result = await writePermission('unset', undefined, snapshot.revision);
165
- overlay = { present: false, revision: result && result.revision };
166
- cachedRawSnapshot = undefined;
167
- if (typeof target.load === 'function') await target.load();
168
- return;
169
- }
170
- return typeof unset === 'function' ? unset.apply(target, arguments) : undefined;
171
- };
172
- }
173
- var value = Reflect.get(target, property, target);
174
- return typeof value === 'function' ? value.bind(target) : value;
175
- }
176
- });
177
- if (scopeCache) scopeCache.set(scope, wrapped);
178
- return wrapped;
179
- }
180
-
181
- function wrapBinder(binder) {
182
- if (!binder || (typeof binder !== 'object' && typeof binder !== 'function')) return binder;
183
- if (binderCache && binderCache.has(binder)) return binderCache.get(binder);
184
- var wrapped = new Proxy(binder, {
185
- get: function(target, property) {
186
- if (property === 'bind') {
187
- var bind = Reflect.get(target, property, target);
188
- if (typeof bind !== 'function') return bind;
189
- return function(spec) {
190
- var scope = bind.apply(target, arguments);
191
- return spec && spec.namespace === 'vision-router' ? wrapScope(scope) : scope;
192
- };
193
- }
194
- var value = Reflect.get(target, property, target);
195
- return typeof value === 'function' ? value.bind(target) : value;
196
- }
197
- });
198
- if (binderCache) binderCache.set(binder, wrapped);
199
- return wrapped;
200
- }
201
-
202
- function wrapLocalContext(ctx) {
203
- if (!ctx || typeof ctx !== 'object') return ctx;
204
- if (localContextCache && localContextCache.has(ctx)) return localContextCache.get(ctx);
205
- var wrapped = new Proxy(ctx, {
206
- get: function(target, property) {
207
- if (property === 'settingsScope') return wrapBinder(Reflect.get(target, property, target));
208
- var value = Reflect.get(target, property, target);
209
- return typeof value === 'function' ? value.bind(target) : value;
210
- }
211
- });
212
- if (localContextCache) localContextCache.set(ctx, wrapped);
213
- return wrapped;
214
- }
215
-
216
- function chineseLocale() {
217
- var lang = '';
218
- try {
219
- lang = String((document && document.documentElement && document.documentElement.lang) || (navigator && navigator.language) || '').toLowerCase();
220
- } catch (_) {}
221
- return lang === '' || lang.indexOf('zh') === 0;
222
- }
223
-
224
- function riskMessage() {
225
- return chineseLocale()
226
- ? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
227
- : '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?';
228
- }
229
-
230
- function isPermissionDisabledDescribe(channel, endpoint, result) {
231
- return channel === CHANNEL
232
- && endpoint === 'describe'
233
- && result && result.ok === true
234
- && result.value && result.value.enabled !== true
235
- && result.value.reason === 'permission-disabled';
236
- }
237
-
238
- async function authorizeAndRefresh(target, call) {
239
- if (authorizationPromise) return authorizationPromise;
240
- authorizationPromise = (async function(){
241
- if (typeof window.confirm !== 'function' || window.confirm(riskMessage()) !== true) return undefined;
242
- var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true });
243
- if (!authorized || authorized.ok !== true) {
244
- var message = authorized && authorized.error && authorized.error.message
245
- ? authorized.error.message
246
- : 'Vision Router could not enable remote settings.';
247
- try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
248
- return undefined;
249
- }
250
- return call.call(target, CHANNEL, 'describe', {});
251
- })();
252
- try { return await authorizationPromise; }
253
- finally { authorizationPromise = undefined; }
254
- }
255
-
256
- function wrapRpc(rpc) {
257
- if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc;
258
- if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc);
259
- var wrapped = new Proxy(rpc, {
260
- get: function(target, property) {
261
- if (property === 'call') {
262
- var call = Reflect.get(target, property, target);
263
- if (typeof call !== 'function') return call;
264
- return async function(channel, endpoint) {
265
- var result = await call.apply(target, arguments);
266
- if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result;
267
- var refreshed = await authorizeAndRefresh(target, call);
268
- return refreshed || result;
269
- };
270
- }
271
- var value = Reflect.get(target, property, target);
272
- return typeof value === 'function' ? value.bind(target) : value;
273
- }
274
- });
275
- if (rpcCache) rpcCache.set(rpc, wrapped);
276
- return wrapped;
277
- }
278
-
279
- function wrapConnection(connection) {
280
- if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
281
- if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
282
- var wrapped = new Proxy(connection, {
283
- get: function(target, property) {
284
- if (property === 'isLoopback') {
285
- var locationLike;
286
- try { locationLike = window && window.location; } catch (_) { locationLike = undefined; }
287
- if (isLoopbackLocation(locationLike)) return true;
288
- }
289
- if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
290
- var value = Reflect.get(target, property, target);
291
- return typeof value === 'function' ? value.bind(target) : value;
292
- }
293
- });
294
- if (connectionCache) connectionCache.set(connection, wrapped);
295
- return wrapped;
296
- }
297
-
298
- function wrapRiskContext(ctx) {
299
- if (!ctx || typeof ctx !== 'object') return ctx;
300
- if (riskContextCache && riskContextCache.has(ctx)) return riskContextCache.get(ctx);
301
- var wrapped = new Proxy(ctx, {
302
- get: function(target, property) {
303
- if (property === 'get') {
304
- var get = Reflect.get(target, property, target);
305
- if (typeof get !== 'function') return get;
306
- return function(name) {
307
- var value = get.apply(target, arguments);
308
- return name === 'connection' ? wrapConnection(value) : value;
309
- };
310
- }
311
- var value = Reflect.get(target, property, target);
312
- return typeof value === 'function' ? value.bind(target) : value;
313
- }
314
- });
315
- if (riskContextCache) riskContextCache.set(ctx, wrapped);
316
- return wrapped;
317
- }
318
-
319
- function patchLiveLoader(loader) {
320
- if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterSettingsRc8Lifecycle) return;
321
- var original = loader.load;
322
- function load(spec) {
323
- if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
324
- var factory = spec.factory;
325
- spec = Object.assign({}, spec, {
326
- factory: function(require) {
327
- var exports = factory(require);
328
- if (exports && typeof exports.apply === 'function' && !exports.apply.__visionRouterSettingsRc8Lifecycle) {
329
- var apply = exports.apply;
330
- var wrappedApply = function(ctx) {
331
- var rest = Array.prototype.slice.call(arguments, 1);
332
- var nextCtx = wrapRiskContext(wrapLocalContext(ctx));
333
- return apply.apply(exports, [nextCtx].concat(rest));
334
- };
335
- Object.defineProperty(wrappedApply, '__visionRouterSettingsRc8Lifecycle', { value: true });
336
- exports.apply = wrappedApply;
337
- }
338
- return exports;
339
- }
340
- });
341
- }
342
- return original.call(this, spec);
343
- }
344
- Object.defineProperty(load, '__visionRouterSettingsRc8Lifecycle', { value: true });
345
- loader.load = load;
346
- }
347
-
348
- function patchCreate(loader) {
349
- if (!loader || typeof loader.create !== 'function' || loader.create.__visionRouterSettingsRc8Lifecycle) return;
350
- var originalCreate = loader.create;
351
- function create() {
352
- var result = originalCreate.apply(this, arguments);
353
- patchLiveLoader(loader);
354
- return result;
355
- }
356
- Object.defineProperty(create, '__visionRouterSettingsRc8Lifecycle', { value: true });
357
- loader.create = create;
358
- if (loader.mode === 'live') patchLiveLoader(loader);
359
- }
360
-
361
- function install() {
362
- if (window.__ModuleLoader__) {
363
- patchCreate(window.__ModuleLoader__);
364
- return;
365
- }
366
- var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
367
- if (descriptor && descriptor.configurable === false) return;
368
- var previousGet = descriptor && descriptor.get;
369
- var previousSet = descriptor && descriptor.set;
370
- var stored = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined;
371
- Object.defineProperty(window, '__ModuleLoader__', {
372
- configurable: true,
373
- enumerable: !descriptor || descriptor.enumerable !== false,
374
- get: function(){ return previousGet ? previousGet.call(window) : stored; },
375
- set: function(value) {
376
- if (previousSet) previousSet.call(window, value); else stored = value;
377
- try { patchCreate(previousGet ? previousGet.call(window) : value); } catch (_) {}
378
- }
379
- });
380
- if (stored) patchCreate(stored);
381
- }
382
-
383
- try { install(); } catch (_) {}
384
- })();`
385
-
386
- export function injectSettingsRc8ClientPrelude(html) {
387
- return injectClientCarrierHtml(html, SETTINGS_RC8_MARK, SETTINGS_RC8_CLIENT_PRELUDE)
388
- }
389
-
390
- export function installSettingsRc8ClientLifecycle(ctx) {
391
- installClientScriptCarriers(ctx, {
392
- marker: SETTINGS_RC8_MARK,
393
- prelude: SETTINGS_RC8_CLIENT_PRELUDE,
394
- label: 'vision-router: rc8 settings client lifecycle',
395
- injectHtml: injectSettingsRc8ClientPrelude,
396
- })
397
- }