dsh-vision-router 2.2.8 → 2.3.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.
- package/README.md +7 -7
- package/README.zh.md +7 -7
- package/docs/architecture/2.3.0-compat-retirement.md +147 -0
- package/docs/architecture/2.3.0-execution-plan.md +410 -0
- package/docs/architecture/2.3.0-final-audit.md +279 -0
- package/docs/architecture/2.3.0-reopened-plan.md +720 -0
- package/docs/architecture/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +27 -21
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/entry.js +8 -56
- package/index.js +161 -691
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +75 -17
- package/lib/client-presentation-boundary.js +53 -28
- package/lib/client.js +246 -144
- package/lib/core-apply-lifecycle.js +47 -0
- package/lib/core-primitives.js +88 -135
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/dsh-contract-compat.js +11 -26
- package/lib/dsh-support-window.js +3 -3
- package/lib/http-compat.js +1 -2
- package/lib/local-remote-settings-permission.js +33 -7
- package/lib/maintenance-routes.js +136 -0
- package/lib/public-config.js +57 -0
- package/lib/public-entry.js +2 -7
- package/lib/remote-settings-bridge.js +11 -0
- package/lib/remote-settings-risk-client-boundary.js +155 -0
- package/lib/remote-settings-risk-confirmation.js +17 -105
- package/lib/runtime-composition.js +105 -30
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-vision-index.js +154 -9
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +3 -2
- package/lib/vision-backend-runtime-policy.js +3 -2
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-diagnostics-routes.js +216 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-provider-transport.js +0 -29
- package/lib/vision-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +5 -1
- package/lib/web/remote-settings-client.js +2 -2
- package/package.json +23 -13
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
|
@@ -2,12 +2,17 @@ import { inspectDshHostCapabilities } from './dsh-host-capabilities.js'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Compatibility inventory
|
|
5
|
-
* Reason: preserve
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Reason: preserve the remaining Settings/session/provider ownership and durable
|
|
6
|
+
* profile migrations across DVR 2.3's supported DSH 0.1.5+ Host window.
|
|
7
|
+
* Host gap: current supported Hosts still span multiple Settings/session
|
|
8
|
+
* lifecycle shapes, while historical persisted attachment policy overlays may
|
|
9
|
+
* survive an in-place Host upgrade.
|
|
10
|
+
* Feature detection: concrete attachment/settings/session/LLM methods only;
|
|
11
|
+
* never a DSH version string.
|
|
12
|
+
* Removal condition: each retained capability bridge has a current-Host or
|
|
13
|
+
* durable-data consumer; pre-floor Host-only branches are removed in R2.3.
|
|
14
|
+
* Tests: compatibility contracts, minimum Host Settings persistence,
|
|
15
|
+
* attachment-admission policy, and DSH contract CI.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
const HOST_OWNED_ROUTES = new Set(['deepseek-official', 'deepseek-official-native'])
|
|
@@ -654,23 +659,3 @@ export function installHostSettingsCompatibility(ctx, entryConfig, options = {})
|
|
|
654
659
|
},
|
|
655
660
|
})
|
|
656
661
|
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* The minimum-supported host keeps the narrow Termux fallback. A
|
|
660
|
-
* batch-capable attachment service keeps AttachmentId store-owned, so that
|
|
661
|
-
* host context passes through untouched.
|
|
662
|
-
*/
|
|
663
|
-
export function attachmentContextForContract(ctx, logger, options = {}) {
|
|
664
|
-
installDshHostCapabilityDiagnostics(ctx)
|
|
665
|
-
if (hasBatchAttachmentContract(ctx)) return ctx
|
|
666
|
-
const install = options.installAndroidAttachmentCompat
|
|
667
|
-
if (typeof install !== 'function') return ctx
|
|
668
|
-
return install(ctx, logger, options.android)
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Transitional aliases for external/tests written against the rc.7-era names.
|
|
672
|
-
// New code must consume the capability-named exports above so future DSH
|
|
673
|
-
// releases do not accrete `if (rc8) / if (rc9)` branches.
|
|
674
|
-
export const isRc7ContractRuntime = hasBatchAttachmentContract
|
|
675
|
-
export const protectRc7ProviderOwnership = protectHostProviderOwnership
|
|
676
|
-
export const installRc7SettingsCompatibility = installHostSettingsCompatibility
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const DSH_SUPPORT_WINDOW = Object.freeze({
|
|
2
|
-
dvrTrain: '2.
|
|
3
|
-
minimum: '0.1.
|
|
2
|
+
dvrTrain: '2.3.x',
|
|
3
|
+
minimum: '0.1.5',
|
|
4
4
|
currentStable: '0.1.5-rc.3',
|
|
5
5
|
})
|
|
6
6
|
|
|
@@ -49,7 +49,7 @@ export function formatDshSupportWindowLines(capabilities = {}) {
|
|
|
49
49
|
` support floor: DVR ${DSH_SUPPORT_WINDOW.dvrTrain} -> DSH ${DSH_SUPPORT_WINDOW.minimum}`,
|
|
50
50
|
'DSH compatibility verification evidence:',
|
|
51
51
|
` exact stable: ${DSH_VERIFICATION_EVIDENCE.exactStable}`,
|
|
52
|
-
` exact
|
|
52
|
+
` exact supported 0.2.x boundary: ${DSH_VERIFICATION_EVIDENCE.exactPreview}`,
|
|
53
53
|
` scheduled stable canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.stableCanaryDistTag}`,
|
|
54
54
|
` scheduled next/rc canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.nextCanaryDistTag}`,
|
|
55
55
|
` scheduled alpha/pre-release canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.previewCanaryDistTag}`,
|
package/lib/http-compat.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { currentVisionProviderTransport } from './vision-provider-transport.js'
|
|
2
1
|
|
|
3
2
|
const GLM_4V_FLASH = /^glm-4v-flash(?:$|[-_.])/i
|
|
4
3
|
export const DEFAULT_HTTP_ERROR_DETAIL_MAX_BYTES = 64 * 1024
|
|
@@ -278,7 +277,7 @@ function nextCompatibilityBody(body, detail) {
|
|
|
278
277
|
*/
|
|
279
278
|
export async function fetchWithOpenAICompatibility(fetchImpl, input, init, context = {}) {
|
|
280
279
|
if (typeof fetchImpl !== 'function') throw new TypeError('fetch implementation is required')
|
|
281
|
-
const transport = context.active === true ?
|
|
280
|
+
const transport = context.active === true ? context.providerTransport : undefined
|
|
282
281
|
const requestFetch = transport
|
|
283
282
|
? (nextInput, nextInit) => transport.fetch(nextInput, nextInit, {
|
|
284
283
|
providerName: context.providerName,
|
|
@@ -173,10 +173,16 @@ export function createVisionRouterLocalPermissionHttpHandler(settings, logger) {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Current-product browser boundary for the local allowRemoteSettings control.
|
|
178
|
+
* Host loader lifecycle compatibility stays outside this owner.
|
|
179
|
+
*/
|
|
180
|
+
export function createLocalRemoteSettingsPermissionClientBoundary(options = {}) {
|
|
181
|
+
var fetchImpl = options.fetchImpl
|
|
182
|
+
var ENDPOINT = options.endpoint
|
|
183
|
+
var FIELD = options.field
|
|
184
|
+
if (typeof ENDPOINT !== 'string' || ENDPOINT === '') throw new TypeError('local permission boundary requires endpoint')
|
|
185
|
+
if (typeof FIELD !== 'string' || FIELD === '') throw new TypeError('local permission boundary requires field')
|
|
180
186
|
var scopeCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
181
187
|
var binderCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
182
188
|
var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
@@ -189,11 +195,11 @@ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
async function writePermission(operation, value, revision) {
|
|
192
|
-
if (typeof
|
|
198
|
+
if (typeof fetchImpl !== 'function') throw new Error('Vision Router local settings transport is unavailable');
|
|
193
199
|
var payload = { operation: operation };
|
|
194
200
|
if (operation === 'set') payload.value = value;
|
|
195
201
|
if (Number.isInteger(revision) && revision >= 0) payload.expectedRevision = revision;
|
|
196
|
-
var response = await
|
|
202
|
+
var response = await fetchImpl(ENDPOINT, {
|
|
197
203
|
method: 'POST',
|
|
198
204
|
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
199
205
|
body: JSON.stringify(payload)
|
|
@@ -366,6 +372,26 @@ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
366
372
|
return wrapped;
|
|
367
373
|
}
|
|
368
374
|
|
|
375
|
+
|
|
376
|
+
return { wrapContext: wrapContext }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE =
|
|
380
|
+
'var createLocalRemoteSettingsPermissionClientBoundary = (' +
|
|
381
|
+
createLocalRemoteSettingsPermissionClientBoundary.toString() +
|
|
382
|
+
');'
|
|
383
|
+
|
|
384
|
+
export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
|
|
385
|
+
'use strict';
|
|
386
|
+
${LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE}
|
|
387
|
+
var localPermissionBoundary = createLocalRemoteSettingsPermissionClientBoundary({
|
|
388
|
+
endpoint: '${LOCAL_REMOTE_SETTINGS_PERMISSION_PATH}',
|
|
389
|
+
field: '${LOCAL_REMOTE_SETTINGS_PERMISSION_FIELD}',
|
|
390
|
+
fetchImpl: typeof fetch === 'function'
|
|
391
|
+
? function(url, options) { return fetch(url, options); }
|
|
392
|
+
: undefined
|
|
393
|
+
});
|
|
394
|
+
|
|
369
395
|
function patchLoader(loader) {
|
|
370
396
|
if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterLocalPermission) return;
|
|
371
397
|
var original = loader.load;
|
|
@@ -379,7 +405,7 @@ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
379
405
|
var apply = exports.apply;
|
|
380
406
|
var wrappedApply = function(ctx) {
|
|
381
407
|
var rest = Array.prototype.slice.call(arguments, 1);
|
|
382
|
-
return apply.apply(exports, [wrapContext(ctx)].concat(rest));
|
|
408
|
+
return apply.apply(exports, [localPermissionBoundary.wrapContext(ctx)].concat(rest));
|
|
383
409
|
};
|
|
384
410
|
Object.defineProperty(wrappedApply, '__visionRouterLocalPermission', { value: true });
|
|
385
411
|
exports.apply = wrappedApply;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { createCachedUpdateChecker } from './update-check.js'
|
|
3
|
+
import { detectDshSelfUpdatePlan, runDshPluginUpdate } from './self-update.js'
|
|
4
|
+
|
|
5
|
+
function json(res, status, body, headers = {}) {
|
|
6
|
+
res.writeHead(status, {
|
|
7
|
+
'content-type': 'application/json',
|
|
8
|
+
...headers,
|
|
9
|
+
})
|
|
10
|
+
res.end(JSON.stringify(body))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Product-maintenance Web routes kept outside the visual Core.
|
|
15
|
+
*
|
|
16
|
+
* The supplied context must already carry DVR's Web capability/auth boundary;
|
|
17
|
+
* this installer deliberately owns only update-product state and the narrow
|
|
18
|
+
* per-route CSRF/replay checks that existed in Core before extraction.
|
|
19
|
+
*/
|
|
20
|
+
export function installVisionMaintenanceRoutes(ctx, options = {}) {
|
|
21
|
+
if (!ctx || typeof ctx.inject !== 'function') return
|
|
22
|
+
const logger = options.logger ?? ctx.logger
|
|
23
|
+
const updateChecker = options.updateChecker ?? createCachedUpdateChecker({
|
|
24
|
+
fetchImpl: (...args) => globalThis.fetch(...args),
|
|
25
|
+
})
|
|
26
|
+
const selfUpdatePlan = options.selfUpdatePlan ?? detectDshSelfUpdatePlan()
|
|
27
|
+
const updateRunner = options.runUpdate ?? runDshPluginUpdate
|
|
28
|
+
const tokenFactory = options.tokenFactory ?? (() => randomBytes(24).toString('base64url'))
|
|
29
|
+
let selfUpdateToken = tokenFactory()
|
|
30
|
+
let selfUpdateInFlight
|
|
31
|
+
|
|
32
|
+
const updateResultForClient = (result) => ({
|
|
33
|
+
...result,
|
|
34
|
+
autoUpdate: {
|
|
35
|
+
supported: selfUpdatePlan.available === true,
|
|
36
|
+
method: selfUpdatePlan.available === true ? selfUpdatePlan.method : undefined,
|
|
37
|
+
profile: selfUpdatePlan.profile,
|
|
38
|
+
reason: selfUpdatePlan.available === true ? undefined : selfUpdatePlan.reason,
|
|
39
|
+
token:
|
|
40
|
+
selfUpdatePlan.available === true &&
|
|
41
|
+
result &&
|
|
42
|
+
result.ok === true &&
|
|
43
|
+
result.updateAvailable === true
|
|
44
|
+
? selfUpdateToken
|
|
45
|
+
: undefined,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
void updateChecker.check(false).then((result) => {
|
|
50
|
+
if (result && result.ok === true && result.updateAvailable === true) {
|
|
51
|
+
logger?.info?.(
|
|
52
|
+
'vision-router: update available %s -> %s',
|
|
53
|
+
result.currentVersion,
|
|
54
|
+
result.latestVersion,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
60
|
+
webCtx.effect(
|
|
61
|
+
() => webCtx.webServer.register({
|
|
62
|
+
kind: 'exact',
|
|
63
|
+
path: '/_dsh/vision-router/update-check',
|
|
64
|
+
handler: async (req, res) => {
|
|
65
|
+
if (req.method !== 'GET') {
|
|
66
|
+
res.setHeader('Allow', 'GET')
|
|
67
|
+
res.writeHead(405)
|
|
68
|
+
res.end()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const force = /(?:[?&])force=1(?:&|$)/.test(String(req.url ?? ''))
|
|
72
|
+
const result = await updateChecker.check(force)
|
|
73
|
+
json(res, 200, updateResultForClient(result), { 'cache-control': 'no-store' })
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
'vision-router: update-check route',
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
webCtx.effect(
|
|
80
|
+
() => webCtx.webServer.register({
|
|
81
|
+
kind: 'exact',
|
|
82
|
+
path: '/_dsh/vision-router/self-update',
|
|
83
|
+
handler: async (req, res) => {
|
|
84
|
+
if (req.method !== 'POST') {
|
|
85
|
+
res.setHeader('Allow', 'POST')
|
|
86
|
+
res.writeHead(405)
|
|
87
|
+
res.end()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const fetchSite = String(req.headers?.['sec-fetch-site'] ?? '')
|
|
91
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
|
|
92
|
+
json(res, 403, { ok: false, error: 'cross-origin update request rejected' })
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const token = String(req.headers?.['x-dsh-vision-router-update-token'] ?? '')
|
|
96
|
+
if (!token || token !== selfUpdateToken) {
|
|
97
|
+
json(res, 403, { ok: false, error: 'invalid update token' })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (selfUpdatePlan.available !== true) {
|
|
101
|
+
json(res, 409, { ok: false, error: 'automatic update is not safe for this DSH launch' })
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const fresh = await updateChecker.check(true)
|
|
106
|
+
if (!fresh || fresh.ok !== true) {
|
|
107
|
+
json(res, 502, { ok: false, error: fresh?.error || 'could not refresh update metadata' })
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (fresh.updateAvailable !== true) {
|
|
111
|
+
json(res, 409, { ok: false, error: 'no newer version is currently available' })
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (!selfUpdateInFlight) {
|
|
115
|
+
const pending = updateRunner(selfUpdatePlan, { targetVersion: fresh.latestVersion })
|
|
116
|
+
selfUpdateInFlight = pending
|
|
117
|
+
void pending.then(
|
|
118
|
+
() => { if (selfUpdateInFlight === pending) selfUpdateInFlight = undefined },
|
|
119
|
+
() => { if (selfUpdateInFlight === pending) selfUpdateInFlight = undefined },
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
const result = await selfUpdateInFlight
|
|
123
|
+
selfUpdateToken = tokenFactory()
|
|
124
|
+
json(res, 200, result, { 'cache-control': 'no-store' })
|
|
125
|
+
} catch (error) {
|
|
126
|
+
json(res, 500, {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: error && error.message ? error.message : String(error),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
'vision-router: self-update route',
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
|
|
3
|
+
// Increment whenever the browser-visible settings contract gains a field whose
|
|
4
|
+
// absence changes write semantics. The resolved schema also publishes this as
|
|
5
|
+
// a read-only-by-convention handshake for browser/Host compatibility checks.
|
|
6
|
+
export const SETTINGS_CONTRACT_REVISION = 7
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compose DVR's public product settings onto the mature Core schema.
|
|
10
|
+
*
|
|
11
|
+
* Schemastery object schemas expose set() as the supported replacement seam.
|
|
12
|
+
* Mutating the supplied object is deliberate: Core later registers this exact
|
|
13
|
+
* Config identity with Settings, so a cloned/public-only schema would let the
|
|
14
|
+
* exported contract and runtime validation drift apart.
|
|
15
|
+
*/
|
|
16
|
+
export function composePublicVisionConfig(coreConfig) {
|
|
17
|
+
if (!coreConfig || typeof coreConfig.set !== 'function') {
|
|
18
|
+
throw new TypeError('Vision Router public config requires a Schemastery object schema')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
coreConfig.set('progressiveTools', z.boolean().default(false))
|
|
22
|
+
|
|
23
|
+
// Keep provider/task/turn timeout layers coherent. The whole-turn budget is
|
|
24
|
+
// an optional safety cap: 0 remains unlimited for long-running agent turns.
|
|
25
|
+
coreConfig.set(
|
|
26
|
+
'visionTaskTimeoutMs',
|
|
27
|
+
z.number().step(1000).min(1000).max(180000).default(120000),
|
|
28
|
+
)
|
|
29
|
+
coreConfig.set('visionTurnBudgetMs', z.number().step(1000).min(0).max(600000).default(0))
|
|
30
|
+
|
|
31
|
+
// Both visible settings entry points edit the same Host-owned namespace.
|
|
32
|
+
coreConfig.set('visionDepth', z.union(['fast', 'standard', 'deep', 'custom']).default('standard'))
|
|
33
|
+
coreConfig.set('visionDepthMaxCalls', z.number().step(1).min(0).max(100).default(0))
|
|
34
|
+
|
|
35
|
+
// Product semantics: ordered remains the safe default. Auto execution and
|
|
36
|
+
// background measurement each require explicit live user authority.
|
|
37
|
+
coreConfig.set('routingMode', z.union(['ordered', 'auto']).default('ordered'))
|
|
38
|
+
coreConfig.set(
|
|
39
|
+
'routingPreference',
|
|
40
|
+
z.union(['balanced', 'quality', 'speed', 'local']).default('balanced'),
|
|
41
|
+
)
|
|
42
|
+
coreConfig.set(
|
|
43
|
+
'backgroundBenchmarking',
|
|
44
|
+
z.union(['local-free', 'all', 'off']).default('off'),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// Remote Settings remains an explicit opt-in product permission. The remote
|
|
48
|
+
// risk dialog is a reminder/acknowledgement, not a server-side auth proof.
|
|
49
|
+
coreConfig.set('allowRemoteSettings', z.boolean().default(false))
|
|
50
|
+
|
|
51
|
+
coreConfig.set(
|
|
52
|
+
'settingsContractRevision',
|
|
53
|
+
z.number().step(1).min(1).max(SETTINGS_CONTRACT_REVISION).default(SETTINGS_CONTRACT_REVISION),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return coreConfig
|
|
57
|
+
}
|
package/lib/public-entry.js
CHANGED
|
@@ -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'
|
|
@@ -59,12 +56,10 @@ export function apply(ctx, config = {}) {
|
|
|
59
56
|
ctx: hardening.ctx,
|
|
60
57
|
config: () => liveVisionConfig(hardening.ctx, config),
|
|
61
58
|
})
|
|
62
|
-
const releaseTransportRegistry = installVisionProviderTransport(transport)
|
|
63
59
|
let transportReleased = false
|
|
64
60
|
const releaseTransport = () => {
|
|
65
61
|
if (transportReleased) return
|
|
66
62
|
transportReleased = true
|
|
67
|
-
releaseTransportRegistry()
|
|
68
63
|
void transport.dispose()
|
|
69
64
|
}
|
|
70
65
|
try {
|
|
@@ -72,7 +67,7 @@ export function apply(ctx, config = {}) {
|
|
|
72
67
|
() => releaseTransport,
|
|
73
68
|
'vision-router: provider transport',
|
|
74
69
|
)
|
|
75
|
-
const result = base.apply(runtimeCtx, hardening.config)
|
|
70
|
+
const result = base.apply(runtimeCtx, hardening.config, { providerTransport: transport })
|
|
76
71
|
installLegacyGlobalProxyBoundary(runtimeCtx, hardening.config)
|
|
77
72
|
hardening.installClientBoundary()
|
|
78
73
|
return result
|
|
@@ -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
|
+
');'
|