dsh-vision-router 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +12 -3
  2. package/README.zh.md +12 -3
  3. package/docs/architecture/3.0-architecture-plan.md +252 -0
  4. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  5. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  6. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  7. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  8. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  9. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  10. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  11. package/docs/architecture/3.0-r7-public-api.md +151 -0
  12. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  13. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  14. package/docs/architecture/dsh-support-window.md +5 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/entry.js +1 -1
  17. package/index.js +108 -71
  18. package/lib/agent-request-route-authority.js +1 -20
  19. package/lib/core-apply-lifecycle.js +37 -43
  20. package/lib/core-cache.js +87 -0
  21. package/lib/core-primitives.js +3 -143
  22. package/lib/core-vision-surface-domain.js +63 -0
  23. package/lib/core-vision-surface.js +11 -77
  24. package/lib/delegated-call-config.js +15 -24
  25. package/lib/doctor-cli.js +0 -0
  26. package/lib/doctor-runtime.js +4 -1
  27. package/lib/dsh-contract-compat.js +26 -230
  28. package/lib/dsh-host-capabilities.js +3 -9
  29. package/lib/dsh-settings-017-compat.js +17 -10
  30. package/lib/file-logger.js +8 -3
  31. package/lib/grounding-coordinate-runtime.js +1 -2
  32. package/lib/http-body-limit.js +97 -79
  33. package/lib/image-offload-compat.js +30 -21
  34. package/lib/image-resource-governor.js +210 -228
  35. package/lib/legacy-global-proxy-boundary.js +46 -3
  36. package/lib/live-model-client-prelude.js +12 -4
  37. package/lib/live-model-discovery.js +134 -43
  38. package/lib/local-vision-stabilizer.js +31 -17
  39. package/lib/maintenance-routes.js +1 -3
  40. package/lib/native-image-coexistence.js +15 -3
  41. package/lib/ollama-cold-start.js +87 -23
  42. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  43. package/lib/proxy-dispatcher-pool.js +151 -92
  44. package/lib/proxy-routing.js +75 -51
  45. package/lib/proxy-url-compat.js +3 -10
  46. package/lib/public-config.js +23 -44
  47. package/lib/public-entry.d.ts +148 -0
  48. package/lib/public-entry.js +17 -4
  49. package/lib/replay-delegation.js +12 -7
  50. package/lib/replay-envelope-v2-compat.js +12 -7
  51. package/lib/runtime-composition.js +22 -4
  52. package/lib/screenshot-source-boundary.js +2 -2
  53. package/lib/session-affinity-runtime.js +113 -97
  54. package/lib/session-affinity.js +89 -76
  55. package/lib/session-event-history-compat.js +30 -0
  56. package/lib/session-query-readers.js +263 -0
  57. package/lib/session-surface-compat.js +30 -31
  58. package/lib/session-surface-policy-domain.js +85 -0
  59. package/lib/session-surface-policy.js +2 -111
  60. package/lib/session-turn-resolver.js +2 -6
  61. package/lib/session-vision-attachment-index.js +143 -0
  62. package/lib/session-vision-delegation-boundary.js +34 -0
  63. package/lib/session-vision-delegation.js +22 -0
  64. package/lib/session-vision-index.js +25 -107
  65. package/lib/session-vision-mode-authority.js +229 -192
  66. package/lib/session-vision-mode-boundary.js +65 -22
  67. package/lib/session-vision-policy-domain.js +304 -0
  68. package/lib/session-vision-policy.js +104 -0
  69. package/lib/session-vision-state.js +368 -378
  70. package/lib/settings-client-017-compat.js +9 -7
  71. package/lib/structured-flow-hardening.js +22 -4
  72. package/lib/tesseract-exec-compat.js +9 -1
  73. package/lib/turn-budget-context.js +47 -41
  74. package/lib/update-check.js +11 -2
  75. package/lib/vision-attachment-handle-runtime.js +176 -156
  76. package/lib/vision-backend-runtime-policy.js +16 -0
  77. package/lib/vision-background-benchmark.js +41 -4
  78. package/lib/vision-background-failure-policy.js +41 -41
  79. package/lib/vision-background-stop-store-core.js +217 -0
  80. package/lib/vision-background-stop-store.js +8 -179
  81. package/lib/vision-capability-benchmark.js +11 -61
  82. package/lib/vision-capability-evidence.js +35 -0
  83. package/lib/vision-capability-fingerprint.js +72 -0
  84. package/lib/vision-capability-identity.js +116 -61
  85. package/lib/vision-capability-router.js +13 -33
  86. package/lib/vision-chunk-assembler.js +95 -0
  87. package/lib/vision-diagnostics-routes.js +34 -4
  88. package/lib/vision-execution-order-apply.js +45 -55
  89. package/lib/vision-execution-order.js +19 -19
  90. package/lib/vision-execution-policy.js +12 -7
  91. package/lib/vision-model-registry.js +30 -17
  92. package/lib/vision-provider-transport.js +137 -147
  93. package/lib/vision-routing-authority.js +47 -48
  94. package/lib/vision-routing-product.js +26 -29
  95. package/lib/vision-routing-settings-prelude.js +8 -3
  96. package/lib/vision-runtime-performance-store.js +166 -0
  97. package/lib/vision-runtime-performance.js +13 -142
  98. package/lib/vision-tool-runtime-boundary.js +99 -30
  99. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  100. package/lib/web/remote-settings-client.js +4 -7
  101. package/lib/web-capability-boundary.js +17 -7
  102. package/lib/wrapper-directory.js +14 -23
  103. package/package.json +37 -11
@@ -1,96 +1,155 @@
1
- async function closeDispatcher(dispatcher) {
2
- if (!dispatcher || (typeof dispatcher !== 'object' && typeof dispatcher !== 'function')) return
3
- try {
4
- if (typeof dispatcher.close === 'function') {
5
- await dispatcher.close()
6
- return
7
- }
8
- if (typeof dispatcher.destroy === 'function') await dispatcher.destroy()
9
- } catch {
10
- // Cleanup must not turn a completed request into an unload failure.
11
- }
1
+ import { proxyDispatcherLike, } from './proxy-routing.js';
2
+ function objectRecord(value) {
3
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
4
+ ? value
5
+ : undefined;
12
6
  }
13
-
14
- export function createProxyDispatcherPool({ importUndici = () => import('undici'), label = 'vision proxy transport' } = {}) {
15
- let cachedEntry
16
- let disposed = false
17
- let disposePromise
18
- const liveEntries = new Set()
19
-
20
- const maybeCloseEntry = (entry) => {
21
- if (!entry.obsolete || entry.refs !== 0 || !entry.dispatcher || entry.closing) return
22
- entry.closing = Promise.resolve().then(() => closeDispatcher(entry.dispatcher)).finally(() => {
23
- liveEntries.delete(entry)
24
- entry.resolveDone()
25
- })
26
- }
27
- const retireEntry = (entry) => {
28
- if (!entry || entry.obsolete) return
29
- entry.obsolete = true
30
- maybeCloseEntry(entry)
31
- }
32
- const createEntry = (proxyUrl) => {
33
- let resolveDone
34
- const entry = {
35
- proxyUrl, refs: 0, obsolete: false, dispatcher: undefined, getGlobalDispatcher: undefined, closing: undefined,
36
- done: new Promise((resolve) => { resolveDone = resolve }), resolveDone: () => resolveDone(), promise: undefined,
37
- }
38
- liveEntries.add(entry)
39
- entry.promise = Promise.resolve().then(() => importUndici()).then(({ ProxyAgent, getGlobalDispatcher }) => {
40
- if (typeof ProxyAgent !== 'function') throw new Error(`${label}: undici ProxyAgent is unavailable`)
41
- if (typeof getGlobalDispatcher !== 'function') throw new Error(`${label}: undici getGlobalDispatcher is unavailable`)
42
- entry.getGlobalDispatcher = getGlobalDispatcher
43
- entry.dispatcher = new ProxyAgent(proxyUrl)
44
- maybeCloseEntry(entry)
45
- return entry.dispatcher
46
- }).catch((error) => {
47
- if (cachedEntry === entry) cachedEntry = undefined
48
- liveEntries.delete(entry)
49
- entry.resolveDone()
50
- throw error
51
- })
52
- return entry
53
- }
54
- const reconcile = (proxyUrl) => {
55
- if (cachedEntry && cachedEntry.proxyUrl !== proxyUrl) {
56
- const previous = cachedEntry
57
- cachedEntry = undefined
58
- retireEntry(previous)
59
- }
60
- }
61
- const acquire = async (proxyUrl) => {
62
- if (disposed) throw new Error(`${label}: dispatcher pool is disposed`)
63
- let entry = cachedEntry
64
- if (!entry || entry.proxyUrl !== proxyUrl || entry.obsolete) {
65
- if (entry) retireEntry(entry)
66
- entry = createEntry(proxyUrl)
67
- cachedEntry = entry
68
- }
69
- entry.refs += 1
7
+ async function closeDispatcher(dispatcher) {
8
+ const value = objectRecord(dispatcher);
9
+ if (value === undefined)
10
+ return;
70
11
  try {
71
- const dispatcher = await entry.promise
72
- let released = false
73
- return { dispatcher, getGlobalDispatcher: entry.getGlobalDispatcher, release() {
74
- if (released) return
75
- released = true
76
- entry.refs = Math.max(0, entry.refs - 1)
77
- maybeCloseEntry(entry)
78
- } }
79
- } catch (error) {
80
- entry.refs = Math.max(0, entry.refs - 1)
81
- maybeCloseEntry(entry)
82
- throw error
12
+ if (typeof value.close === 'function') {
13
+ await value.close.call(dispatcher);
14
+ return;
15
+ }
16
+ if (typeof value.destroy === 'function')
17
+ await value.destroy.call(dispatcher);
83
18
  }
84
- }
85
- const dispose = () => {
86
- if (disposePromise) return disposePromise
87
- disposed = true
88
- const current = cachedEntry
89
- cachedEntry = undefined
90
- retireEntry(current)
91
- for (const entry of liveEntries) retireEntry(entry)
92
- disposePromise = Promise.all([...liveEntries].map((entry) => entry.done)).then(() => undefined)
93
- return disposePromise
94
- }
95
- return Object.freeze({ acquire, reconcile, dispose })
19
+ catch {
20
+ // Cleanup must not turn a completed request into an unload failure.
21
+ }
22
+ }
23
+ export function createProxyDispatcherPool({ importUndici = () => import('undici'), label = 'vision proxy transport', } = {}) {
24
+ let cachedEntry;
25
+ let disposed = false;
26
+ let disposePromise;
27
+ const liveEntries = new Set();
28
+ const maybeCloseEntry = (entry) => {
29
+ if (!entry.obsolete || entry.refs !== 0 || !entry.dispatcher || entry.closing)
30
+ return;
31
+ entry.closing = Promise.resolve()
32
+ .then(() => closeDispatcher(entry.dispatcher))
33
+ .finally(() => {
34
+ liveEntries.delete(entry);
35
+ entry.resolveDone();
36
+ });
37
+ };
38
+ const retireEntry = (entry) => {
39
+ if (!entry || entry.obsolete)
40
+ return;
41
+ entry.obsolete = true;
42
+ maybeCloseEntry(entry);
43
+ };
44
+ const createEntry = (proxyUrl) => {
45
+ let resolveDone;
46
+ const done = new Promise((resolve) => { resolveDone = resolve; });
47
+ const entry = {
48
+ proxyUrl,
49
+ refs: 0,
50
+ obsolete: false,
51
+ dispatcher: undefined,
52
+ getGlobalDispatcher: undefined,
53
+ closing: undefined,
54
+ done,
55
+ resolveDone,
56
+ promise: undefined,
57
+ };
58
+ liveEntries.add(entry);
59
+ entry.promise = Promise.resolve()
60
+ .then(importUndici)
61
+ .then((moduleValue) => {
62
+ const module = objectRecord(moduleValue);
63
+ const ProxyAgent = module?.ProxyAgent;
64
+ const getGlobalDispatcher = module?.getGlobalDispatcher;
65
+ if (typeof ProxyAgent !== 'function') {
66
+ throw new Error(`${label}: undici ProxyAgent is unavailable`);
67
+ }
68
+ if (typeof getGlobalDispatcher !== 'function') {
69
+ throw new Error(`${label}: undici getGlobalDispatcher is unavailable`);
70
+ }
71
+ const Constructor = ProxyAgent;
72
+ const dispatcher = new Constructor(proxyUrl);
73
+ if (!proxyDispatcherLike(dispatcher)) {
74
+ throw new Error(`${label}: undici ProxyAgent has no dispatcher contract`);
75
+ }
76
+ entry.getGlobalDispatcher = () => {
77
+ const globalDispatcher = getGlobalDispatcher.call(moduleValue);
78
+ if (!proxyDispatcherLike(globalDispatcher)) {
79
+ throw new Error(`${label}: undici global dispatcher has no dispatcher contract`);
80
+ }
81
+ return globalDispatcher;
82
+ };
83
+ entry.dispatcher = dispatcher;
84
+ maybeCloseEntry(entry);
85
+ return entry.dispatcher;
86
+ })
87
+ .catch((error) => {
88
+ if (cachedEntry === entry)
89
+ cachedEntry = undefined;
90
+ liveEntries.delete(entry);
91
+ entry.resolveDone();
92
+ throw error;
93
+ });
94
+ return entry;
95
+ };
96
+ const reconcile = (proxyUrl) => {
97
+ if (cachedEntry && cachedEntry.proxyUrl !== proxyUrl) {
98
+ const previous = cachedEntry;
99
+ cachedEntry = undefined;
100
+ retireEntry(previous);
101
+ }
102
+ };
103
+ const acquire = async (proxyUrl) => {
104
+ if (disposed)
105
+ throw new Error(`${label}: dispatcher pool is disposed`);
106
+ let entry = cachedEntry;
107
+ if (!entry || entry.proxyUrl !== proxyUrl || entry.obsolete) {
108
+ if (entry)
109
+ retireEntry(entry);
110
+ entry = createEntry(proxyUrl);
111
+ cachedEntry = entry;
112
+ }
113
+ entry.refs += 1;
114
+ try {
115
+ const promise = entry.promise;
116
+ if (!promise)
117
+ throw new Error(`${label}: dispatcher entry failed to initialize`);
118
+ const dispatcher = await promise;
119
+ const getGlobalDispatcher = entry.getGlobalDispatcher;
120
+ if (!getGlobalDispatcher) {
121
+ throw new Error(`${label}: global dispatcher accessor is unavailable`);
122
+ }
123
+ let released = false;
124
+ return {
125
+ dispatcher,
126
+ getGlobalDispatcher,
127
+ release() {
128
+ if (released)
129
+ return;
130
+ released = true;
131
+ entry.refs = Math.max(0, entry.refs - 1);
132
+ maybeCloseEntry(entry);
133
+ },
134
+ };
135
+ }
136
+ catch (error) {
137
+ entry.refs = Math.max(0, entry.refs - 1);
138
+ maybeCloseEntry(entry);
139
+ throw error;
140
+ }
141
+ };
142
+ const dispose = () => {
143
+ if (disposePromise)
144
+ return disposePromise;
145
+ disposed = true;
146
+ const current = cachedEntry;
147
+ cachedEntry = undefined;
148
+ retireEntry(current);
149
+ for (const entry of liveEntries)
150
+ retireEntry(entry);
151
+ disposePromise = Promise.all([...liveEntries].map((entry) => entry.done)).then(() => undefined);
152
+ return disposePromise;
153
+ };
154
+ return Object.freeze({ acquire, reconcile, dispose });
96
155
  }
@@ -1,76 +1,100 @@
1
- import { domainToASCII } from 'node:url'
2
-
1
+ import { URL, domainToASCII } from 'node:url';
3
2
  function objectLike(value) {
4
- return value !== null && (typeof value === 'object' || typeof value === 'function')
3
+ return value !== null && (typeof value === 'object' || typeof value === 'function');
5
4
  }
6
-
7
5
  /** True only when the user explicitly configured a Vision Router proxy override. */
8
6
  export function visionProxyOverrideConfigured(config = {}) {
9
- const value = config && typeof config === 'object' ? config.proxy : undefined
10
- return typeof value === 'string' && value.trim() !== ''
7
+ const value = objectLike(config)
8
+ ? config.proxy
9
+ : undefined;
10
+ return typeof value === 'string' && value.trim() !== '';
11
11
  }
12
-
13
12
  /**
14
13
  * Canonicalize a configured/request hostname for proxy admission only.
15
14
  * Persisted settings stay untouched; this removes DNS presentation differences
16
15
  * (case, one trailing dot and Unicode) without broadening the suffix policy.
17
16
  */
18
17
  export function canonicalProxyHost(value) {
19
- let host = String(value ?? '').trim()
20
- if (host === '') return ''
21
- if (host.endsWith('.')) host = host.slice(0, -1)
22
- if (host === '') return ''
23
-
24
- // WHATWG URL.hostname keeps IPv6 literals bracketed. domainToASCII does not
25
- // accept a bare IPv6 literal, so preserve the bracketed spelling and only
26
- // case-fold it. Proxy hosts are otherwise DNS names and can be IDNA-folded.
27
- if (host.startsWith('[') && host.endsWith(']')) return host.toLowerCase()
28
- const ascii = domainToASCII(host)
29
- return (ascii || host).toLowerCase()
18
+ let host = String(value ?? '').trim();
19
+ if (host === '')
20
+ return '';
21
+ if (host.endsWith('.'))
22
+ host = host.slice(0, -1);
23
+ if (host === '')
24
+ return '';
25
+ // WHATWG URL.hostname keeps IPv6 literals bracketed. domainToASCII does not
26
+ // accept a bare IPv6 literal, so preserve the bracketed spelling and only
27
+ // case-fold it. Proxy hosts are otherwise DNS names and can be IDNA-folded.
28
+ if (host.startsWith('[') && host.endsWith(']'))
29
+ return host.toLowerCase();
30
+ const ascii = domainToASCII(host);
31
+ return (ascii || host).toLowerCase();
30
32
  }
31
-
32
33
  /** Exact host or subdomain match using one shared proxy-host interpretation. */
33
- export function proxyHostMatchesAny(hostname, hosts) {
34
- const host = canonicalProxyHost(hostname)
35
- if (host === '') return false
36
- return (hosts ?? []).some((raw) => {
37
- const candidate = canonicalProxyHost(raw)
38
- return candidate !== '' && (host === candidate || host.endsWith(`.${candidate}`))
39
- })
34
+ export function proxyHostMatchesAny(hostname, hosts = []) {
35
+ const host = canonicalProxyHost(hostname);
36
+ if (host === '')
37
+ return false;
38
+ return hosts.some((raw) => {
39
+ const candidate = canonicalProxyHost(raw);
40
+ return candidate !== '' && (host === candidate || host.endsWith(`.${candidate}`));
41
+ });
40
42
  }
41
-
42
43
  export function proxyDispatcherLike(value) {
43
- return objectLike(value) && typeof value.dispatch === 'function'
44
+ return objectLike(value)
45
+ && typeof value.dispatch === 'function';
44
46
  }
45
-
46
47
  function proxyDispatchOriginUrl(options) {
47
- try {
48
- const origin = options?.origin
49
- if (origin instanceof URL) return origin
50
- if (origin !== undefined && origin !== null) return new URL(String(origin))
51
- } catch {
52
- // Unknown dispatcher shapes never authorize the plugin proxy.
53
- }
54
- return undefined
48
+ try {
49
+ const origin = objectLike(options)
50
+ ? options.origin
51
+ : undefined;
52
+ if (origin instanceof URL)
53
+ return origin;
54
+ if (origin !== undefined && origin !== null)
55
+ return new URL(String(origin));
56
+ }
57
+ catch {
58
+ // Unknown dispatcher shapes never authorize the plugin proxy.
59
+ }
60
+ return undefined;
55
61
  }
56
-
57
62
  /**
58
63
  * Preserve Fetch/Undici redirect semantics while keeping proxyHosts an upper
59
64
  * bound on every hop after a request has entered the DVR proxy override.
60
65
  * The fallback dispatcher is borrowed, never owned or closed by this selector.
61
66
  */
62
67
  export function createPerHopProxyDispatcher(proxyDispatcher, fallbackDispatcher, proxyHosts = []) {
63
- if (!proxyDispatcherLike(proxyDispatcher) || !proxyDispatcherLike(fallbackDispatcher)) {
64
- throw new TypeError('vision proxy hop selector requires proxy and fallback dispatchers')
65
- }
66
- const hosts = [...proxyHosts]
67
- return Object.freeze({
68
- dispatch(options, handler) {
69
- const url = proxyDispatchOriginUrl(options)
70
- const target = url && proxyHostMatchesAny(url.hostname, hosts)
71
- ? proxyDispatcher
72
- : fallbackDispatcher
73
- return target.dispatch(options, handler)
74
- },
75
- })
68
+ if (!proxyDispatcherLike(proxyDispatcher) || !proxyDispatcherLike(fallbackDispatcher)) {
69
+ throw new TypeError('vision proxy hop selector requires proxy and fallback dispatchers');
70
+ }
71
+ const hosts = [...proxyHosts];
72
+ const proxy = proxyDispatcher;
73
+ const fallback = fallbackDispatcher;
74
+ // Fetch types model a full Undici Dispatcher, while fetch dispatch itself
75
+ // consumes the dispatch() seam. A Host dispatcher may expose dispatch as a
76
+ // frozen/non-configurable own property; using that borrowed object directly
77
+ // as a Proxy target would make an overriding get trap violate ECMAScript
78
+ // Proxy invariants. Use a neutral shell with the same prototype instead.
79
+ //
80
+ // Every non-dispatch read still resolves against and binds to the real
81
+ // fallback Dispatcher, so the wrapper does not become a second owner for
82
+ // close/destroy/event state and instanceof-style prototype checks remain
83
+ // compatible with the borrowed dispatcher.
84
+ const shell = Object.create(Object.getPrototypeOf(fallback));
85
+ return new Proxy(shell, {
86
+ get(_target, property) {
87
+ if (property === 'dispatch') {
88
+ return (options, handler) => {
89
+ const url = proxyDispatchOriginUrl(options);
90
+ const selected = url && proxyHostMatchesAny(url.hostname, hosts)
91
+ ? proxy
92
+ : fallback;
93
+ return selected.dispatch(options, handler);
94
+ };
95
+ }
96
+ const value = Reflect.get(fallback, property, fallback);
97
+ return typeof value === 'function' ? value.bind(fallback) : value;
98
+ },
99
+ });
76
100
  }
@@ -1,12 +1,5 @@
1
- /**
2
- * Project the persisted proxy setting into the syntax accepted by Undici.
3
- *
4
- * Older Vision Router settings copy explicitly recommended `socks5h://`.
5
- * Keep that persisted user intent untouched and translate only at the runtime
6
- * dispatcher boundary. Deliberately do not validate or reinterpret any other
7
- * proxy scheme here: Undici remains the execution authority.
8
- */
9
1
  export function effectiveProxyUrlForUndici(value) {
10
- if (typeof value !== 'string') return value
11
- return value.replace(/^(\s*)socks5h:/i, '$1socks5:')
2
+ if (typeof value !== 'string')
3
+ return value;
4
+ return value.replace(/^(\s*)socks5h:/i, '$1socks5:');
12
5
  }
@@ -1,10 +1,8 @@
1
- import z from '@deepseek-ai/schemastery'
2
-
1
+ import z from '@deepseek-ai/schemastery';
3
2
  // Increment whenever the browser-visible settings contract gains a field whose
4
3
  // absence changes write semantics. The resolved schema also publishes this as
5
4
  // a read-only-by-convention handshake for browser/Host compatibility checks.
6
- export const SETTINGS_CONTRACT_REVISION = 7
7
-
5
+ export const SETTINGS_CONTRACT_REVISION = 7;
8
6
  /**
9
7
  * Compose DVR's public product settings onto the mature Core schema.
10
8
  *
@@ -14,44 +12,25 @@ export const SETTINGS_CONTRACT_REVISION = 7
14
12
  * exported contract and runtime validation drift apart.
15
13
  */
16
14
  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
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;
57
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