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.
- package/README.md +16 -7
- package/README.zh.md +16 -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/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
- package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
- package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
- package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
- package/docs/architecture/3.0-r7-public-api.md +151 -0
- package/docs/architecture/3.0-r8-ci-release.md +248 -0
- package/docs/architecture/3.0-r9-final-audit.md +370 -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 +30 -22
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +9 -57
- package/index.js +268 -761
- package/lib/agent-request-route-authority.js +1 -20
- 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 +41 -0
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +91 -278
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +37 -256
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/dsh-support-window.js +3 -3
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/http-compat.js +1 -2
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-remote-settings-permission.js +33 -7
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +134 -0
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +36 -0
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +19 -11
- 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/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +127 -34
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +179 -116
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +9 -7
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +25 -6
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +19 -2
- package/lib/vision-background-benchmark.js +41 -4
- package/lib/vision-background-failure-policy.js +41 -41
- package/lib/vision-background-stop-store-core.js +217 -0
- package/lib/vision-background-stop-store.js +8 -179
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-capability-benchmark.js +11 -61
- package/lib/vision-capability-evidence.js +35 -0
- package/lib/vision-capability-fingerprint.js +72 -0
- package/lib/vision-capability-identity.js +116 -61
- package/lib/vision-capability-router.js +13 -33
- package/lib/vision-chunk-assembler.js +95 -0
- package/lib/vision-diagnostics-routes.js +246 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-provider-transport.js +137 -176
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +104 -31
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +6 -9
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +56 -20
- package/lib/android-attachment-compat.js +0 -244
- 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
|
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'
|
|
@@ -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 (
|
|
72
|
+
if (transportReleasePromise) return transportReleasePromise
|
|
73
|
+
if (transportReleased) return Promise.resolve()
|
|
66
74
|
transportReleased = true
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
transportReleasePromise = Promise.resolve().then(() => transport.dispose())
|
|
76
|
+
return transportReleasePromise
|
|
69
77
|
}
|
|
70
78
|
try {
|
|
71
|
-
runtimeCtx
|
|
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
|
-
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
package/lib/replay-delegation.js
CHANGED
|
@@ -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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
}
|