dsh-vision-router 2.0.0 → 2.1.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 +33 -16
- package/README.zh.md +34 -17
- package/cordis.patch.yml +19 -9
- package/docs/architecture/2x-contract-ledger.md +165 -0
- package/docs/architecture/compat-inventory.md +121 -0
- package/docs/architecture/dsh-compatibility-matrix.md +57 -0
- package/docs/architecture/dsh-support-window.md +50 -0
- package/docs/architecture/final-closure-audit.md +148 -0
- package/docs/architecture/p2-jobs-feasibility.md +94 -0
- package/docs/architecture/p3-compat-retirement.md +46 -0
- package/docs/architecture/p3-host-native-seams.md +43 -0
- package/docs/architecture/p3-native-recovery-evaluation.md +65 -0
- package/docs/architecture/runtime-boundaries.md +124 -0
- package/docs/releases/v2.0.1.md +17 -0
- package/docs/releases/v2.1.0-support-window.md +29 -0
- package/docs/releases/v2.1.0.md +83 -0
- package/entry.js +13 -307
- package/index.js +85 -352
- package/lib/abort-signal-compat.js +191 -0
- package/lib/adapter-update-coalescer.js +113 -29
- package/lib/adversarial-hardening.js +24 -9
- package/lib/artifact-boundary.js +16 -175
- package/lib/artifact-io.js +220 -0
- package/lib/artifact-retention.js +305 -53
- package/lib/catalog-corrections.js +6 -1
- package/lib/client-host-compat-prelude.js +249 -0
- package/lib/client-presentation-boundary.js +61 -3
- package/lib/client.js +71 -72
- package/lib/core-vision-surface.js +124 -0
- package/lib/depth-guidance.js +114 -90
- package/lib/doctor-cli-p0.js +168 -0
- package/lib/doctor-cli.js +7 -0
- package/lib/doctor-vision-limits.js +72 -0
- package/lib/dsh-contract-compat.js +207 -39
- package/lib/dsh-host-capabilities.js +107 -0
- package/lib/dsh-support-window.js +48 -0
- package/lib/http-compat.js +22 -6
- package/lib/image-resource-governor.js +35 -10
- package/lib/legacy-core-vision-policy-bridge.js +25 -226
- package/lib/legacy-global-proxy-boundary.js +127 -0
- package/lib/live-model-client-prelude.js +0 -12
- package/lib/mixed-router.js +81 -81
- package/lib/public-entry.js +42 -4
- package/lib/runtime-composition.js +348 -0
- package/lib/runtime-i18n-boundary.js +683 -0
- package/lib/runtime-i18n-core-scope.js +56 -0
- package/lib/runtime-i18n-core.js +34 -0
- package/lib/runtime-i18n.js +121 -0
- package/lib/session-surface-policy.js +97 -0
- package/lib/session-vision-index.js +282 -0
- package/lib/session-vision-runtime.js +48 -0
- package/lib/settings-factory-lifecycle.js +144 -0
- package/lib/settings-ia-client-prelude.js +74 -2
- package/lib/settings-limit-client-prelude.js +110 -0
- package/lib/settings-native-card-layout.js +297 -0
- package/lib/settings-number-contract.js +29 -0
- package/lib/structured-flow-hardening.js +207 -76
- package/lib/tesseract-exec-compat.js +58 -32
- package/lib/update-check.js +17 -5
- package/lib/v2-settings-ia-integration.js +9 -4
- package/lib/vision-artifact-store.js +77 -0
- package/lib/vision-backend-runtime-policy.js +6 -2
- package/lib/vision-background-benchmark.js +130 -41
- package/lib/vision-background-failure-policy.js +70 -0
- package/lib/vision-background-stop-store.js +54 -9
- package/lib/vision-capability-benchmark-client.js +57 -66
- package/lib/vision-capability-benchmark-presentation.js +97 -0
- package/lib/vision-capability-benchmark-service.js +3 -3
- package/lib/vision-execution-order-apply.js +61 -0
- package/lib/vision-execution-order-plan.js +64 -0
- package/lib/vision-execution-order.js +38 -0
- package/lib/vision-limit-diagnostics.js +202 -0
- package/lib/vision-product-presentation.js +284 -0
- package/lib/vision-provider-transport.js +197 -0
- package/lib/vision-resilience.js +87 -45
- package/lib/vision-routing-evidence.js +366 -0
- package/lib/vision-routing-runtime.js +278 -0
- package/lib/vision-tool-runtime-boundary.js +27 -21
- package/lib/vision-turn-budget-client-prelude.js +9 -9
- package/lib/web/benchmark-panel.js +6 -0
- package/lib/web/diagnostics-panel.js +44 -0
- package/lib/web/index.js +60 -0
- package/lib/web/model-picker.js +22 -0
- package/lib/web/onboarding.js +6 -0
- package/lib/web/product-state.js +59 -0
- package/lib/web/remote-settings-client.js +8 -0
- package/lib/web/routing-section.js +6 -0
- package/lib/web/settings-controller.js +6 -0
- package/lib/web-capability-boundary.js +65 -9
- package/lib/windows-screenshot-dpi-compat.js +148 -0
- package/package.json +14 -6
- package/lib/vision-capability-shadow.js +0 -576
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
function timeoutReason(root) {
|
|
2
|
+
try {
|
|
3
|
+
const DomException = root?.DOMException
|
|
4
|
+
if (typeof DomException === 'function') {
|
|
5
|
+
return new DomException('The operation was aborted due to timeout', 'TimeoutError')
|
|
6
|
+
}
|
|
7
|
+
} catch {
|
|
8
|
+
// Fall through to an ordinary Error in runtimes without DOMException.
|
|
9
|
+
}
|
|
10
|
+
const error = new Error('The operation was aborted due to timeout')
|
|
11
|
+
error.name = 'TimeoutError'
|
|
12
|
+
return error
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function defineStatic(target, name, value) {
|
|
16
|
+
try {
|
|
17
|
+
Object.defineProperty(target, name, {
|
|
18
|
+
value,
|
|
19
|
+
writable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
})
|
|
22
|
+
} catch {
|
|
23
|
+
try { target[name] = value } catch {}
|
|
24
|
+
}
|
|
25
|
+
return typeof target?.[name] === 'function'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAbortSignalLike(value) {
|
|
29
|
+
return Boolean(
|
|
30
|
+
value &&
|
|
31
|
+
typeof value === 'object' &&
|
|
32
|
+
typeof value.aborted === 'boolean' &&
|
|
33
|
+
typeof value.addEventListener === 'function',
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function signalList(signals) {
|
|
38
|
+
if (signals === undefined || signals === null || typeof signals[Symbol.iterator] !== 'function') {
|
|
39
|
+
throw new TypeError('AbortSignal.any: signals must be an iterable of AbortSignal values')
|
|
40
|
+
}
|
|
41
|
+
const list = Array.from(signals)
|
|
42
|
+
for (const signal of list) {
|
|
43
|
+
if (!isAbortSignalLike(signal)) {
|
|
44
|
+
throw new TypeError('AbortSignal.any: every input must be an AbortSignal')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return list
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function timeoutDelay(delay) {
|
|
51
|
+
if (typeof delay !== 'number') {
|
|
52
|
+
throw new TypeError('AbortSignal.timeout: delay must be a number')
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isInteger(delay) || delay < 0 || delay > 0xffff_ffff) {
|
|
55
|
+
throw new RangeError('AbortSignal.timeout: delay must be an integer between 0 and 4294967295')
|
|
56
|
+
}
|
|
57
|
+
return delay
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function removeAbortListeners(listeners) {
|
|
61
|
+
for (const [signal, listener] of listeners) {
|
|
62
|
+
try { signal.removeEventListener?.('abort', listener) } catch {}
|
|
63
|
+
}
|
|
64
|
+
listeners.length = 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function strongAbortRelay(controller, source, cleanup) {
|
|
68
|
+
return () => {
|
|
69
|
+
cleanup()
|
|
70
|
+
if (!controller.signal.aborted) controller.abort(source.reason)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function weakAbortRelay(outputRef, controllerBySignal, source, cleanup, finalizer, token) {
|
|
75
|
+
return () => {
|
|
76
|
+
const output = outputRef.deref()
|
|
77
|
+
cleanup()
|
|
78
|
+
try { finalizer.unregister(token) } catch {}
|
|
79
|
+
if (!output || output.aborted) return
|
|
80
|
+
const controller = controllerBySignal.get(output)
|
|
81
|
+
if (controller && !output.aborted) controller.abort(source.reason)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function weakAnySupport(root) {
|
|
86
|
+
const WeakRefCtor = root?.WeakRef
|
|
87
|
+
const FinalizationRegistryCtor = root?.FinalizationRegistry
|
|
88
|
+
const WeakMapCtor = root?.WeakMap
|
|
89
|
+
if (
|
|
90
|
+
typeof WeakRefCtor !== 'function' ||
|
|
91
|
+
typeof FinalizationRegistryCtor !== 'function' ||
|
|
92
|
+
typeof WeakMapCtor !== 'function'
|
|
93
|
+
) return undefined
|
|
94
|
+
const controllerBySignal = new WeakMapCtor()
|
|
95
|
+
const finalizer = new FinalizationRegistryCtor((cleanup) => {
|
|
96
|
+
try { cleanup?.() } catch {}
|
|
97
|
+
})
|
|
98
|
+
return { WeakRefCtor, controllerBySignal, finalizer }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* DSH normally runs on Node versions that already provide AbortSignal.any()
|
|
103
|
+
* and AbortSignal.timeout(). A few supported host/browser bridges, however,
|
|
104
|
+
* can surface an AbortSignal constructor without those static helpers. The
|
|
105
|
+
* plugin has several cancellation boundaries that legitimately combine more
|
|
106
|
+
* than one signal, so install small standards-shaped fallbacks once at the
|
|
107
|
+
* public package boundary rather than sprinkling version checks everywhere.
|
|
108
|
+
*
|
|
109
|
+
* This is intentionally a process-global, missing-only compatibility seam:
|
|
110
|
+
* native functions are never replaced, and ESM module evaluation installs the
|
|
111
|
+
* fallback once for all Vision Router contexts in the process. That avoids a
|
|
112
|
+
* teardown race where unloading one profile could remove helpers still used by
|
|
113
|
+
* another live profile. When WeakRef/FinalizationRegistry are available, the
|
|
114
|
+
* `any()` fallback also removes source listeners after an unused composite is
|
|
115
|
+
* collected instead of pinning long-lived Host lifecycle signals forever.
|
|
116
|
+
*/
|
|
117
|
+
export function installAbortSignalCompat(root = globalThis) {
|
|
118
|
+
const Signal = root?.AbortSignal
|
|
119
|
+
const Controller = root?.AbortController
|
|
120
|
+
const state = {
|
|
121
|
+
any: typeof Signal?.any === 'function',
|
|
122
|
+
timeout: typeof Signal?.timeout === 'function',
|
|
123
|
+
}
|
|
124
|
+
if (typeof Signal !== 'function' || typeof Controller !== 'function') return state
|
|
125
|
+
|
|
126
|
+
if (!state.any) {
|
|
127
|
+
const weakSupport = weakAnySupport(root)
|
|
128
|
+
const any = function any(signals) {
|
|
129
|
+
const list = signalList(signals)
|
|
130
|
+
const controller = new Controller()
|
|
131
|
+
const output = controller.signal
|
|
132
|
+
for (const signal of list) {
|
|
133
|
+
if (!signal.aborted) continue
|
|
134
|
+
controller.abort(signal.reason)
|
|
135
|
+
return output
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const listeners = []
|
|
139
|
+
const cleanup = () => removeAbortListeners(listeners)
|
|
140
|
+
let outputRef
|
|
141
|
+
let finalizerToken
|
|
142
|
+
if (weakSupport) {
|
|
143
|
+
weakSupport.controllerBySignal.set(output, controller)
|
|
144
|
+
outputRef = new weakSupport.WeakRefCtor(output)
|
|
145
|
+
finalizerToken = {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
for (const signal of list) {
|
|
150
|
+
const listener = outputRef
|
|
151
|
+
? weakAbortRelay(
|
|
152
|
+
outputRef,
|
|
153
|
+
weakSupport.controllerBySignal,
|
|
154
|
+
signal,
|
|
155
|
+
cleanup,
|
|
156
|
+
weakSupport.finalizer,
|
|
157
|
+
finalizerToken,
|
|
158
|
+
)
|
|
159
|
+
: strongAbortRelay(controller, signal, cleanup)
|
|
160
|
+
listeners.push([signal, listener])
|
|
161
|
+
signal.addEventListener('abort', listener, { once: true })
|
|
162
|
+
}
|
|
163
|
+
if (outputRef) weakSupport.finalizer.register(output, cleanup, finalizerToken)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
cleanup()
|
|
166
|
+
if (finalizerToken) {
|
|
167
|
+
try { weakSupport.finalizer.unregister(finalizerToken) } catch {}
|
|
168
|
+
}
|
|
169
|
+
throw error
|
|
170
|
+
}
|
|
171
|
+
return output
|
|
172
|
+
}
|
|
173
|
+
state.any = defineStatic(Signal, 'any', any)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!state.timeout) {
|
|
177
|
+
const timeout = function timeout(delay) {
|
|
178
|
+
const parsed = timeoutDelay(delay)
|
|
179
|
+
const controller = new Controller()
|
|
180
|
+
const schedule = typeof root?.setTimeout === 'function' ? root.setTimeout.bind(root) : setTimeout
|
|
181
|
+
const timer = schedule(() => controller.abort(timeoutReason(root)), parsed)
|
|
182
|
+
try { timer?.unref?.() } catch {}
|
|
183
|
+
return controller.signal
|
|
184
|
+
}
|
|
185
|
+
state.timeout = defineStatic(Signal, 'timeout', timeout)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return state
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const abortSignalCompat = installAbortSignalCompat()
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
const wrappedContexts = new WeakMap()
|
|
2
2
|
const wrappedLlmServices = new WeakMap()
|
|
3
3
|
const preparedAdapterCompat = new WeakMap()
|
|
4
|
+
const fullAdapterContractCompat = new WeakMap()
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_ADAPTER_RECONCILE_MAX_PASSES = 32
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* not inherit LlmAdapter's default implementation. Normalize only adapters
|
|
11
|
-
* registered through Vision Router's wrapped context so host/foreign adapters
|
|
12
|
-
* keep their native identity and behavior.
|
|
13
|
-
*/
|
|
14
|
-
export function ensureAdapterPrepareCall(adapter) {
|
|
15
|
-
if (!adapter || (typeof adapter !== 'object' && typeof adapter !== 'function')) return adapter
|
|
16
|
-
if (typeof adapter.prepareCall === 'function') return adapter
|
|
8
|
+
function isAdapter(value) {
|
|
9
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
10
|
+
}
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
function hasFunction(adapter, property) {
|
|
13
|
+
try {
|
|
14
|
+
return typeof Reflect.get(adapter, property, adapter) === 'function'
|
|
15
|
+
} catch {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
function createPrepareCallFallback(adapter) {
|
|
21
|
+
return async (provider, model, signal) => {
|
|
22
22
|
const fallbackModel = { provider, id: model, name: model }
|
|
23
23
|
const resolved =
|
|
24
24
|
typeof adapter.resolveModel === 'function'
|
|
@@ -31,44 +31,127 @@ export function ensureAdapterPrepareCall(adapter) {
|
|
|
31
31
|
},
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
function noImageRequestPricing() {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize Vision Router-owned duck-typed adapters to the Host adapter
|
|
42
|
+
* contracts that have base-class defaults in newer DSH releases.
|
|
43
|
+
*
|
|
44
|
+
* DSH 0.1.1 started dispatching through adapter.prepareCall(). DSH 0.1.2 adds
|
|
45
|
+
* adapter.imageRequestPricing() and the token meter resolves it synchronously
|
|
46
|
+
* on every measurement. Vision Router intentionally registers plain adapter
|
|
47
|
+
* objects, so neither inherited default exists unless we materialize it here.
|
|
48
|
+
*
|
|
49
|
+
* Request-image pricing is deliberately NOT delegated to some other adapter.
|
|
50
|
+
* Wrapper/twin/chain routes can rewrite, replace, or aggregate image input, so
|
|
51
|
+
* only that exact route can truthfully declare its projection. `undefined` is
|
|
52
|
+
* LlmAdapter's default and keeps the Host's provider-neutral heuristic. An
|
|
53
|
+
* adapter that explicitly implements imageRequestPricing remains authoritative.
|
|
54
|
+
*
|
|
55
|
+
* The compatibility method is installed non-enumerably on extensible adapters
|
|
56
|
+
* to preserve registration identity. Frozen/sealed adapters fall back to one
|
|
57
|
+
* proxy that supplies all missing methods while binding existing methods back
|
|
58
|
+
* to the original object (important for class/private-field adapters).
|
|
59
|
+
*/
|
|
60
|
+
export function ensureAdapterContracts(adapter, options = {}) {
|
|
61
|
+
if (!isAdapter(adapter)) return adapter
|
|
62
|
+
|
|
63
|
+
const includeImageRequestPricing = options.imageRequestPricing === true
|
|
64
|
+
const needsPrepareCall = !hasFunction(adapter, 'prepareCall')
|
|
65
|
+
const needsImageRequestPricing =
|
|
66
|
+
includeImageRequestPricing && !hasFunction(adapter, 'imageRequestPricing')
|
|
67
|
+
if (!needsPrepareCall && !needsImageRequestPricing) return adapter
|
|
68
|
+
|
|
69
|
+
const cache = includeImageRequestPricing ? fullAdapterContractCompat : preparedAdapterCompat
|
|
70
|
+
const cached = cache.get(adapter)
|
|
71
|
+
if (cached) return cached
|
|
72
|
+
|
|
73
|
+
const prepareCall = needsPrepareCall ? createPrepareCallFallback(adapter) : undefined
|
|
74
|
+
const imageRequestPricing = needsImageRequestPricing ? noImageRequestPricing : undefined
|
|
75
|
+
const descriptors = {}
|
|
76
|
+
if (prepareCall !== undefined) {
|
|
77
|
+
descriptors.prepareCall = {
|
|
41
78
|
configurable: true,
|
|
42
79
|
writable: true,
|
|
43
80
|
value: prepareCall,
|
|
44
|
-
}
|
|
45
|
-
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (imageRequestPricing !== undefined) {
|
|
84
|
+
descriptors.imageRequestPricing = {
|
|
85
|
+
configurable: true,
|
|
86
|
+
writable: true,
|
|
87
|
+
value: imageRequestPricing,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Preserve the exact adapter object whenever possible. Some runtime paths
|
|
92
|
+
// retain registration.adapter and compare/reuse it later, so mutation is less
|
|
93
|
+
// surprising than substituting a wrapper.
|
|
94
|
+
try {
|
|
95
|
+
Object.defineProperties(adapter, descriptors)
|
|
96
|
+
cache.set(adapter, adapter)
|
|
46
97
|
return adapter
|
|
47
98
|
} catch {
|
|
99
|
+
const boundFunctions = new Map()
|
|
48
100
|
const wrapped = new Proxy(adapter, {
|
|
49
101
|
get(target, property) {
|
|
50
|
-
if (property === 'prepareCall') return prepareCall
|
|
102
|
+
if (property === 'prepareCall' && prepareCall !== undefined) return prepareCall
|
|
103
|
+
if (property === 'imageRequestPricing' && imageRequestPricing !== undefined) {
|
|
104
|
+
return imageRequestPricing
|
|
105
|
+
}
|
|
51
106
|
const value = Reflect.get(target, property, target)
|
|
52
|
-
|
|
107
|
+
if (typeof value !== 'function') return value
|
|
108
|
+
let bound = boundFunctions.get(value)
|
|
109
|
+
if (bound === undefined) {
|
|
110
|
+
bound = value.bind(target)
|
|
111
|
+
boundFunctions.set(value, bound)
|
|
112
|
+
}
|
|
113
|
+
return bound
|
|
53
114
|
},
|
|
54
115
|
})
|
|
55
|
-
|
|
116
|
+
cache.set(adapter, wrapped)
|
|
56
117
|
return wrapped
|
|
57
118
|
}
|
|
58
119
|
}
|
|
59
120
|
|
|
121
|
+
/** Preserve the public helper used by the DSH 0.1.1 compatibility tests. */
|
|
122
|
+
export function ensureAdapterPrepareCall(adapter) {
|
|
123
|
+
return ensureAdapterContracts(adapter)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function llmSupportsImageRequestPricing(llm) {
|
|
127
|
+
try {
|
|
128
|
+
return typeof Reflect.get(llm, 'imageRequestPricing', llm) === 'function'
|
|
129
|
+
} catch {
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
60
134
|
function wrapLlmService(llm) {
|
|
61
|
-
if (!
|
|
135
|
+
if (!isAdapter(llm)) return llm
|
|
62
136
|
const cached = wrappedLlmServices.get(llm)
|
|
63
137
|
if (cached) return cached
|
|
64
138
|
|
|
139
|
+
// Capability detection, not a version check: rc.6/rc.7/rc.8 keep the exact
|
|
140
|
+
// historical adapter shape, while Hosts exposing the new route-pricing seam
|
|
141
|
+
// receive the base-class-compatible default on our duck-typed adapters.
|
|
142
|
+
const includeImageRequestPricing = llmSupportsImageRequestPricing(llm)
|
|
65
143
|
const wrapped = new Proxy(llm, {
|
|
66
144
|
get(target, property) {
|
|
67
145
|
if (property === 'registerAdapter') {
|
|
68
146
|
const registerAdapter = Reflect.get(target, property, target)
|
|
69
147
|
if (typeof registerAdapter !== 'function') return registerAdapter
|
|
70
148
|
return (providers, adapter, ...rest) =>
|
|
71
|
-
registerAdapter.call(
|
|
149
|
+
registerAdapter.call(
|
|
150
|
+
target,
|
|
151
|
+
providers,
|
|
152
|
+
ensureAdapterContracts(adapter, { imageRequestPricing: includeImageRequestPricing }),
|
|
153
|
+
...rest,
|
|
154
|
+
)
|
|
72
155
|
}
|
|
73
156
|
const value = Reflect.get(target, property, target)
|
|
74
157
|
return typeof value === 'function' ? value.bind(target) : value
|
|
@@ -154,9 +237,10 @@ export function createCoalescingRunner(run, options = {}) {
|
|
|
154
237
|
* this wrapped context are coalesced; host and other plugin listeners keep
|
|
155
238
|
* their native event semantics.
|
|
156
239
|
*
|
|
157
|
-
* The same boundary also normalizes Vision Router-owned
|
|
158
|
-
*
|
|
159
|
-
*
|
|
240
|
+
* The same deepest registration boundary also normalizes Vision Router-owned
|
|
241
|
+
* adapter contracts: prepareCall for DSH 0.1.1+, and imageRequestPricing when
|
|
242
|
+
* the Host exposes the DSH 0.1.2 route-pricing seam. This stays scoped to the
|
|
243
|
+
* wrapped context instead of monkey-patching the Host LLM service process-wide.
|
|
160
244
|
*/
|
|
161
245
|
export function contextWithCoalescedAdapterUpdates(ctx) {
|
|
162
246
|
if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return ctx
|
|
@@ -478,24 +478,39 @@ export function installAdversarialHardening(ctx, config = {}, core) {
|
|
|
478
478
|
return effect.call(target, () => {
|
|
479
479
|
const originalFetch = globalThis.fetch
|
|
480
480
|
const disposer = factory()
|
|
481
|
-
|
|
481
|
+
let installedFetch = globalThis.fetch
|
|
482
482
|
let active = true
|
|
483
483
|
// Later plugins may capture this function. Making it an inert
|
|
484
484
|
// delegator on unload removes Vision Router from the chain even if
|
|
485
485
|
// another plugin still sits above it, and prevents it resurfacing
|
|
486
486
|
// when that later plugin restores the fetch value it captured.
|
|
487
|
-
const guardedFetch = (...args) =>
|
|
488
|
-
|
|
489
|
-
|
|
487
|
+
const guardedFetch = (...args) => {
|
|
488
|
+
const fetchImpl = active && typeof installedFetch === 'function'
|
|
489
|
+
? installedFetch
|
|
490
|
+
: originalFetch
|
|
491
|
+
return fetchImpl(...args)
|
|
492
|
+
}
|
|
490
493
|
globalThis.fetch = guardedFetch
|
|
491
494
|
return () => {
|
|
495
|
+
if (!active) return
|
|
492
496
|
active = false
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
497
|
+
// Drop the raw Vision Router fetch closure as soon as ownership
|
|
498
|
+
// ends. This releases its cached ProxyAgent/undici closure even
|
|
499
|
+
// when a later plugin still retains guardedFetch.
|
|
500
|
+
installedFetch = undefined
|
|
501
|
+
const currentFetch = globalThis.fetch
|
|
502
|
+
try {
|
|
503
|
+
// Always run the underlying effect disposer. It may own more
|
|
504
|
+
// than the process-level pointer; skipping it merely because a
|
|
505
|
+
// later plugin wrapped fetch leaks that lifecycle forever.
|
|
498
506
|
if (typeof disposer === 'function') disposer()
|
|
507
|
+
} finally {
|
|
508
|
+
// The core disposer currently restores originalFetch
|
|
509
|
+
// unconditionally. Preserve any later plugin that was above us,
|
|
510
|
+
// while an outermost Vision Router cleanup returns to the host.
|
|
511
|
+
globalThis.fetch = currentFetch === guardedFetch
|
|
512
|
+
? originalFetch
|
|
513
|
+
: currentFetch
|
|
499
514
|
}
|
|
500
515
|
}
|
|
501
516
|
}, label)
|
package/lib/artifact-boundary.js
CHANGED
|
@@ -1,184 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { lstat, mkdir, realpath, rename, unlink, writeFile } from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import {
|
|
5
|
-
currentVisionTurnBudget,
|
|
6
|
-
currentVisionTurnBudgetSignal,
|
|
7
|
-
} from './turn-budget-context.js'
|
|
8
|
-
import {
|
|
9
|
-
isManagedArtifactRunName,
|
|
10
|
-
scheduleArtifactRetention,
|
|
11
|
-
} from './artifact-retention.js'
|
|
1
|
+
import { createVisionArtifactStore } from './vision-artifact-store.js'
|
|
12
2
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function isMissing(error) {
|
|
21
|
-
return error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function artifactAbortError() {
|
|
25
|
-
const error = new Error('vision-router: artifact publication aborted')
|
|
26
|
-
error.name = 'AbortError'
|
|
27
|
-
error.code = 'ABORT_ERR'
|
|
28
|
-
return error
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function throwIfVisionAborted() {
|
|
32
|
-
if (currentVisionTurnBudgetSignal()?.aborted) throw artifactAbortError()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function currentArtifactRunId() {
|
|
36
|
-
const value = currentVisionTurnBudget()?.artifactRunId
|
|
37
|
-
return isManagedArtifactRunName(value) ? value : undefined
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function normalizeArtifactsDir(value) {
|
|
41
|
-
if (typeof value !== 'string' || value.trim() === '') return DEFAULT_ARTIFACTS_DIR
|
|
42
|
-
const raw = value.trim()
|
|
43
|
-
if (path.isAbsolute(raw) || path.win32.isAbsolute(raw)) return DEFAULT_ARTIFACTS_DIR
|
|
44
|
-
const parts = raw.split(/[\\/]+/)
|
|
45
|
-
if (parts.some((part) => part === '..')) return DEFAULT_ARTIFACTS_DIR
|
|
46
|
-
const normalized = path.normalize(raw)
|
|
47
|
-
if (normalized === '' || normalized === '.' || normalized === path.sep) return DEFAULT_ARTIFACTS_DIR
|
|
48
|
-
return normalized
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normalizeRelativeArtifactPath(value) {
|
|
52
|
-
if (typeof value !== 'string' || value.trim() === '') {
|
|
53
|
-
throw new Error('vision-router: artifact path must be a non-empty relative path')
|
|
54
|
-
}
|
|
55
|
-
const raw = value.trim()
|
|
56
|
-
if (path.isAbsolute(raw) || path.win32.isAbsolute(raw)) {
|
|
57
|
-
throw new Error('vision-router: artifact path must stay inside the artifacts directory')
|
|
58
|
-
}
|
|
59
|
-
const parts = raw.split(/[\\/]+/)
|
|
60
|
-
if (parts.some((part) => part === '..')) {
|
|
61
|
-
throw new Error('vision-router: artifact path must stay inside the artifacts directory')
|
|
62
|
-
}
|
|
63
|
-
const normalized = path.normalize(raw)
|
|
64
|
-
if (normalized === '' || normalized === '.' || normalized === path.sep) {
|
|
65
|
-
throw new Error('vision-router: artifact path must name a file')
|
|
66
|
-
}
|
|
67
|
-
return normalized
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function realpathNearestExisting(candidate, realpathImpl) {
|
|
71
|
-
let current = candidate
|
|
72
|
-
while (true) {
|
|
73
|
-
try {
|
|
74
|
-
return await realpathImpl(current)
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (!isMissing(error)) throw error
|
|
77
|
-
const parent = path.dirname(current)
|
|
78
|
-
if (parent === current) throw error
|
|
79
|
-
current = parent
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function safeLstat(target, lstatImpl) {
|
|
85
|
-
try {
|
|
86
|
-
return await lstatImpl(target)
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (isMissing(error)) return undefined
|
|
89
|
-
throw error
|
|
90
|
-
}
|
|
91
|
-
}
|
|
3
|
+
export {
|
|
4
|
+
DEFAULT_ARTIFACTS_DIR,
|
|
5
|
+
normalizeArtifactsDir,
|
|
6
|
+
} from './artifact-io.js'
|
|
92
7
|
|
|
93
8
|
/**
|
|
94
|
-
*
|
|
95
|
-
* authority
|
|
96
|
-
*
|
|
9
|
+
* Compatibility boundary retained for existing production callers during P2.
|
|
10
|
+
* Resolution authority now flows through VisionArtifactStore while the public
|
|
11
|
+
* function signature and return value remain unchanged.
|
|
97
12
|
*/
|
|
98
|
-
export
|
|
99
|
-
|
|
100
|
-
const mkdirImpl = deps.mkdir ?? mkdir
|
|
101
|
-
const lstatImpl = deps.lstat ?? lstat
|
|
102
|
-
|
|
103
|
-
throwIfVisionAborted()
|
|
104
|
-
const workspaceReal = await realpathImpl(path.resolve(String(workspace ?? '')))
|
|
105
|
-
throwIfVisionAborted()
|
|
106
|
-
const relativeBase = normalizeArtifactsDir(artifactsDir)
|
|
107
|
-
const runId = currentArtifactRunId()
|
|
108
|
-
const requestedTarget = normalizeRelativeArtifactPath(relativePath)
|
|
109
|
-
const relativeTarget = runId
|
|
110
|
-
? normalizeRelativeArtifactPath(path.join(runId, requestedTarget))
|
|
111
|
-
: requestedTarget
|
|
112
|
-
const lexicalBase = path.resolve(workspaceReal, relativeBase)
|
|
113
|
-
if (!isPathInside(workspaceReal, lexicalBase)) {
|
|
114
|
-
throw new Error('vision-router: artifactsDir must stay inside the session workspace')
|
|
115
|
-
}
|
|
116
|
-
const lexicalTarget = path.resolve(lexicalBase, relativeTarget)
|
|
117
|
-
if (!isPathInside(lexicalBase, lexicalTarget)) {
|
|
118
|
-
throw new Error('vision-router: artifact target must stay inside the artifacts directory')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const lexicalParent = path.dirname(lexicalTarget)
|
|
122
|
-
const existingAncestorReal = await realpathNearestExisting(lexicalParent, realpathImpl)
|
|
123
|
-
throwIfVisionAborted()
|
|
124
|
-
if (!isPathInside(workspaceReal, existingAncestorReal)) {
|
|
125
|
-
throw new Error('vision-router: artifact parent escapes the session workspace through a symlink')
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
await mkdirImpl(lexicalParent, { recursive: true })
|
|
129
|
-
throwIfVisionAborted()
|
|
130
|
-
const parentReal = await realpathImpl(lexicalParent)
|
|
131
|
-
const artifactsBaseReal = await realpathImpl(lexicalBase)
|
|
132
|
-
throwIfVisionAborted()
|
|
133
|
-
if (!isPathInside(workspaceReal, parentReal) || !isPathInside(workspaceReal, artifactsBaseReal)) {
|
|
134
|
-
throw new Error('vision-router: artifact parent escapes the session workspace through a symlink')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const target = path.join(parentReal, path.basename(lexicalTarget))
|
|
138
|
-
const existing = await safeLstat(target, lstatImpl)
|
|
139
|
-
throwIfVisionAborted()
|
|
140
|
-
if (existing?.isDirectory?.()) {
|
|
141
|
-
throw new Error('vision-router: artifact target is a directory')
|
|
142
|
-
}
|
|
143
|
-
return { target, parentReal, artifactsBaseReal, existing, runId }
|
|
13
|
+
export function resolveArtifactTarget(workspace, artifactsDir, relativePath, deps = {}) {
|
|
14
|
+
return createVisionArtifactStore({ workspace, artifactsDir, deps }).resolve(relativePath)
|
|
144
15
|
}
|
|
145
16
|
|
|
146
17
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
18
|
+
* Compatibility writer retained while callers migrate to VisionArtifactStore.
|
|
19
|
+
* The store delegates to the exact hardened IO primitive that previously lived
|
|
20
|
+
* here, so path layout, atomic publication, cancellation and retention semantics
|
|
21
|
+
* remain unchanged.
|
|
150
22
|
*/
|
|
151
|
-
export
|
|
152
|
-
|
|
153
|
-
const renameImpl = deps.rename ?? rename
|
|
154
|
-
const unlinkImpl = deps.unlink ?? unlink
|
|
155
|
-
const resolved = await resolveArtifactTarget(workspace, artifactsDir, relativePath, deps)
|
|
156
|
-
const temp = path.join(
|
|
157
|
-
resolved.parentReal,
|
|
158
|
-
`.dsh-vision-router-${process.pid}-${randomUUID()}.tmp`,
|
|
159
|
-
)
|
|
160
|
-
let published = false
|
|
161
|
-
try {
|
|
162
|
-
throwIfVisionAborted()
|
|
163
|
-
await writeFileImpl(temp, data, { mode: 0o600 })
|
|
164
|
-
throwIfVisionAborted()
|
|
165
|
-
if (resolved.existing !== undefined) {
|
|
166
|
-
try {
|
|
167
|
-
await unlinkImpl(resolved.target)
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (!isMissing(error)) throw error
|
|
170
|
-
}
|
|
171
|
-
throwIfVisionAborted()
|
|
172
|
-
}
|
|
173
|
-
await renameImpl(temp, resolved.target)
|
|
174
|
-
published = true
|
|
175
|
-
if (resolved.runId) {
|
|
176
|
-
scheduleArtifactRetention(resolved.artifactsBaseReal, { protectRunId: resolved.runId })
|
|
177
|
-
}
|
|
178
|
-
return resolved.target
|
|
179
|
-
} finally {
|
|
180
|
-
if (!published) {
|
|
181
|
-
try { await unlinkImpl(temp) } catch { /* best effort */ }
|
|
182
|
-
}
|
|
183
|
-
}
|
|
23
|
+
export function writeArtifactFile(workspace, artifactsDir, relativePath, data, deps = {}) {
|
|
24
|
+
return createVisionArtifactStore({ workspace, artifactsDir, deps }).publish(relativePath, data)
|
|
184
25
|
}
|