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
|
@@ -1,164 +1,192 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
-
|
|
3
|
-
const VISION_ROUTER_ADAPTER_OWNER = Symbol.for('dsh-vision-router.adapter-owner')
|
|
4
|
-
const VISION_ROUTER_OWNERSHIP = 'vision-router-owned'
|
|
5
|
-
const DEFAULT_WRAPPER_ROUTE = 'deepseek-vision'
|
|
6
|
-
const DEFAULT_CHAIN_ROUTE = 'vision-chain'
|
|
7
|
-
const DEEPSEEK_SOURCE = 'deepseek-official'
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { parseSessionVisionPolicy } from './session-vision-policy.js';
|
|
3
|
+
const VISION_ROUTER_ADAPTER_OWNER = Symbol.for('dsh-vision-router.adapter-owner');
|
|
4
|
+
const VISION_ROUTER_OWNERSHIP = 'vision-router-owned';
|
|
5
|
+
const DEFAULT_WRAPPER_ROUTE = 'deepseek-vision';
|
|
6
|
+
const DEFAULT_CHAIN_ROUTE = 'vision-chain';
|
|
7
|
+
const DEEPSEEK_SOURCE = 'deepseek-official';
|
|
8
|
+
const authorityTurn = new AsyncLocalStorage();
|
|
9
|
+
const agentAuthorities = new WeakMap();
|
|
10
|
+
const assemblyAuthorities = new WeakMap();
|
|
11
|
+
function objectRecord(value) {
|
|
12
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
13
|
+
? value
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
16
|
+
function property(value, key) {
|
|
17
|
+
return objectRecord(value)?.[key];
|
|
18
|
+
}
|
|
17
19
|
function isWeakKey(value) {
|
|
18
|
-
|
|
20
|
+
return (typeof value === 'object' && value !== null) || typeof value === 'function';
|
|
19
21
|
}
|
|
20
|
-
|
|
21
22
|
function routeFrom(value) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const record = objectRecord(value);
|
|
24
|
+
if (!record || Array.isArray(value))
|
|
25
|
+
return undefined;
|
|
26
|
+
const provider = typeof record.provider === 'string' ? record.provider.trim() : '';
|
|
27
|
+
const model = typeof record.model === 'string' ? record.model.trim() : '';
|
|
28
|
+
return provider !== '' && model !== '' ? { provider, model } : undefined;
|
|
29
|
+
}
|
|
30
|
+
function contextGet(ctx, name) {
|
|
31
|
+
const record = objectRecord(ctx);
|
|
32
|
+
const get = record?.get;
|
|
33
|
+
if (typeof get !== 'function')
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
return get.call(ctx, name);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
26
41
|
}
|
|
27
|
-
|
|
28
42
|
function selectionProjection(ctx, session) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
// sessionProjections is an optional Host capability, not a declared plugin
|
|
44
|
+
// dependency. Cordis service properties throw when read outside inject(), so
|
|
45
|
+
// capability detection must use Context#get rather than property access.
|
|
46
|
+
const projections = contextGet(ctx, 'sessionProjections');
|
|
47
|
+
const stateOf = property(projections, 'stateOf');
|
|
48
|
+
if (typeof stateOf !== 'function')
|
|
49
|
+
return undefined;
|
|
36
50
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} catch {
|
|
48
|
-
return undefined
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
51
|
+
const state = stateOf.call(projections, session, 'modelSelection');
|
|
52
|
+
const record = objectRecord(state);
|
|
53
|
+
if (!record || Array.isArray(state))
|
|
54
|
+
return undefined;
|
|
55
|
+
return routeFrom(record.pending ?? record.next ?? record.lastUsed);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
52
61
|
function fallbackRoute(session, agent) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
session
|
|
67
|
-
|
|
68
|
-
session
|
|
69
|
-
session
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
62
|
+
try {
|
|
63
|
+
const requestHeader = property(session, 'requestHeader');
|
|
64
|
+
const header = typeof requestHeader === 'function'
|
|
65
|
+
? requestHeader.call(session)
|
|
66
|
+
: undefined;
|
|
67
|
+
const direct = routeFrom(property(header, 'config'));
|
|
68
|
+
if (direct)
|
|
69
|
+
return direct;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Cold or partially restored Sessions may not expose a request header yet.
|
|
73
|
+
}
|
|
74
|
+
const agentOptions = property(agent, 'options');
|
|
75
|
+
const sessionAgent = property(session, 'agent');
|
|
76
|
+
const sessionAgentOptions = property(sessionAgent, 'options');
|
|
77
|
+
const sessionOptions = property(session, 'options');
|
|
78
|
+
const sessionHeader = property(session, 'header');
|
|
79
|
+
const candidates = [
|
|
80
|
+
property(agentOptions, 'config'),
|
|
81
|
+
agentOptions,
|
|
82
|
+
property(agent, 'config'),
|
|
83
|
+
property(sessionAgentOptions, 'config'),
|
|
84
|
+
sessionAgentOptions,
|
|
85
|
+
property(sessionOptions, 'config'),
|
|
86
|
+
sessionOptions,
|
|
87
|
+
property(session, 'config'),
|
|
88
|
+
property(sessionHeader, 'config'),
|
|
89
|
+
sessionHeader,
|
|
90
|
+
];
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
const route = routeFrom(candidate);
|
|
93
|
+
if (route)
|
|
94
|
+
return route;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
80
98
|
export function effectiveSessionModelSelection(ctx, agent) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
const session = property(agent, 'session');
|
|
100
|
+
if (!session)
|
|
101
|
+
return undefined;
|
|
102
|
+
return selectionProjection(ctx, session) ?? fallbackRoute(session, agent);
|
|
84
103
|
}
|
|
85
|
-
|
|
86
104
|
function liveConfig(ctx, fallback) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
try {
|
|
106
|
+
const settings = contextGet(ctx, 'settings');
|
|
107
|
+
const get = property(settings, 'get');
|
|
108
|
+
const value = typeof get === 'function'
|
|
109
|
+
? get.call(settings, 'vision-router')
|
|
110
|
+
: undefined;
|
|
111
|
+
const record = objectRecord(value);
|
|
112
|
+
if (record && !Array.isArray(value))
|
|
113
|
+
return record;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Composition config remains authoritative while Settings is unavailable.
|
|
117
|
+
}
|
|
118
|
+
const record = objectRecord(fallback);
|
|
119
|
+
return record && !Array.isArray(fallback) ? record : {};
|
|
120
|
+
}
|
|
97
121
|
function registrationLookup(ctx) {
|
|
98
|
-
|
|
99
|
-
|
|
122
|
+
const llm = property(ctx, 'llm');
|
|
123
|
+
const registration = property(llm, 'registration');
|
|
124
|
+
return typeof registration === 'function'
|
|
125
|
+
? { owner: llm, lookup: registration }
|
|
126
|
+
: undefined;
|
|
100
127
|
}
|
|
101
|
-
|
|
102
128
|
function currentAdapter(ctx, provider) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
const registration = registrationLookup(ctx);
|
|
130
|
+
if (!registration)
|
|
131
|
+
return undefined;
|
|
132
|
+
try {
|
|
133
|
+
return property(registration.lookup.call(registration.owner, provider), 'adapter');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
112
139
|
function adapterOwnedByVisionRouter(adapter) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
if (!isWeakKey(adapter))
|
|
141
|
+
return false;
|
|
142
|
+
try {
|
|
143
|
+
return objectRecord(adapter)?.[VISION_ROUTER_ADAPTER_OWNER] !== undefined;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
121
149
|
function sameRoute(left, right) {
|
|
122
|
-
|
|
150
|
+
const rightRoute = routeFrom(right);
|
|
151
|
+
return !!left
|
|
152
|
+
&& !!rightRoute
|
|
153
|
+
&& left.provider === rightRoute.provider
|
|
154
|
+
&& left.model === rightRoute.model;
|
|
123
155
|
}
|
|
124
|
-
|
|
125
156
|
function configuredRoute(config, key, fallback) {
|
|
126
|
-
|
|
127
|
-
|
|
157
|
+
const value = typeof config[key] === 'string' ? config[key].trim() : '';
|
|
158
|
+
return value || fallback;
|
|
128
159
|
}
|
|
129
|
-
|
|
130
160
|
function routeOwnedByVisionRouter(ctx, route, config, visionPolicy) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
161
|
+
if (!route)
|
|
162
|
+
return false;
|
|
163
|
+
const registration = registrationLookup(ctx);
|
|
164
|
+
if (registration) {
|
|
165
|
+
// On current DSH, the final Host-visible adapter marker is authoritative.
|
|
166
|
+
// If the configured route was replaced by a foreign adapter, fail closed
|
|
167
|
+
// rather than granting mode from a matching route name.
|
|
168
|
+
return adapterOwnedByVisionRouter(currentAdapter(ctx, route.provider));
|
|
169
|
+
}
|
|
170
|
+
// Minimum Host shims may not expose registration(). Native-image-coexistence
|
|
171
|
+
// already proved exact registration ownership for the same selected route;
|
|
172
|
+
// accept only that turn-local proof, never a generic -vision suffix.
|
|
173
|
+
if (property(visionPolicy, 'ownership') === VISION_ROUTER_OWNERSHIP
|
|
174
|
+
&& sameRoute(route, property(visionPolicy, 'route'))) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
// Compatibility fallback for explicit historical routes only when the Host
|
|
178
|
+
// offers no registration identity at all. Current DSH never reaches this.
|
|
179
|
+
const wrapperRoute = configuredRoute(config, 'wrapperRoute', DEFAULT_WRAPPER_ROUTE);
|
|
180
|
+
const chainRoute = configuredRoute(config, 'chainRoute', DEFAULT_CHAIN_ROUTE);
|
|
181
|
+
if (route.provider === wrapperRoute
|
|
182
|
+
|| route.provider === chainRoute
|
|
183
|
+
|| route.provider === 'vision-http') {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (config.stealth === true && route.provider === DEEPSEEK_SOURCE)
|
|
187
|
+
return true;
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
162
190
|
/**
|
|
163
191
|
* Resolve the one authoritative Vision-mode snapshot for the next Agent step.
|
|
164
192
|
*
|
|
@@ -168,36 +196,46 @@ function routeOwnedByVisionRouter(ctx, route, config, visionPolicy) {
|
|
|
168
196
|
* This prevents a stale ON header from granting tools after the user switched
|
|
169
197
|
* Vision off (and the inverse when turning it back on).
|
|
170
198
|
*/
|
|
171
|
-
export function resolveSessionVisionModeAuthority(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
export function resolveSessionVisionModeAuthority(ctx, agent, fallbackConfig = {}, options = {}) {
|
|
200
|
+
const route = routeFrom(options.route) ?? effectiveSessionModelSelection(ctx, agent);
|
|
201
|
+
const sessionPolicy = parseSessionVisionPolicy(options.sessionPolicy);
|
|
202
|
+
const config = liveConfig(ctx, fallbackConfig);
|
|
203
|
+
const routeEnabled = routeOwnedByVisionRouter(ctx, route, config, options.visionPolicy);
|
|
204
|
+
const enabled = sessionPolicy?.enabled ?? routeEnabled;
|
|
205
|
+
return Object.freeze({
|
|
206
|
+
enabled,
|
|
207
|
+
route: route ? Object.freeze({ ...route }) : undefined,
|
|
208
|
+
reason: sessionPolicy !== undefined
|
|
209
|
+
? 'session-policy'
|
|
210
|
+
: routeEnabled
|
|
211
|
+
? 'vision-router-route'
|
|
212
|
+
: route
|
|
213
|
+
? 'ordinary-route'
|
|
214
|
+
: 'unknown-route',
|
|
215
|
+
...(options.turn === undefined ? {} : { turn: options.turn }),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
188
218
|
function normalizedAuthority(authority) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
219
|
+
const record = objectRecord(authority);
|
|
220
|
+
if (record && !Array.isArray(authority)) {
|
|
221
|
+
// Compatibility callers already provide the immutable authority envelope.
|
|
222
|
+
// Preserve that exact snapshot rather than rebuilding it and changing
|
|
223
|
+
// historical property-presence semantics.
|
|
224
|
+
return authority;
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze({
|
|
227
|
+
enabled: false,
|
|
228
|
+
route: undefined,
|
|
229
|
+
reason: 'unknown-route',
|
|
230
|
+
});
|
|
192
231
|
}
|
|
193
|
-
|
|
194
232
|
/** Persist the immutable authority for the currently entered step. */
|
|
195
233
|
export function rememberSessionVisionModeAuthorityForAgent(authority, agent) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
234
|
+
const snapshot = normalizedAuthority(authority);
|
|
235
|
+
if (isWeakKey(agent))
|
|
236
|
+
agentAuthorities.set(agent, snapshot);
|
|
237
|
+
return snapshot;
|
|
199
238
|
}
|
|
200
|
-
|
|
201
239
|
/**
|
|
202
240
|
* Stage the authority captured by prompt assembly for exactly the immediately
|
|
203
241
|
* following pre-step. Keeping this separate from the active-step snapshot is
|
|
@@ -205,40 +243,37 @@ export function rememberSessionVisionModeAuthorityForAgent(authority, agent) {
|
|
|
205
243
|
* and an old completed-step snapshot must never masquerade as a new assembly.
|
|
206
244
|
*/
|
|
207
245
|
export function rememberSessionVisionModeAssemblyAuthorityForAgent(authority, agent) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
246
|
+
const snapshot = normalizedAuthority(authority);
|
|
247
|
+
if (isWeakKey(agent))
|
|
248
|
+
assemblyAuthorities.set(agent, snapshot);
|
|
249
|
+
return snapshot;
|
|
211
250
|
}
|
|
212
|
-
|
|
213
251
|
/** Consume at most one prompt-assembly snapshot for the following pre-step. */
|
|
214
252
|
export function takeSessionVisionModeAssemblyAuthorityForAgent(agent) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
253
|
+
if (!isWeakKey(agent))
|
|
254
|
+
return undefined;
|
|
255
|
+
const snapshot = assemblyAuthorities.get(agent);
|
|
256
|
+
assemblyAuthorities.delete(agent);
|
|
257
|
+
return snapshot;
|
|
219
258
|
}
|
|
220
|
-
|
|
221
259
|
/** Store one immutable step snapshot and keep it ambient while Core assembles it. */
|
|
222
260
|
export function runWithSessionVisionModeAuthority(authority, agent, callback) {
|
|
223
|
-
|
|
224
|
-
|
|
261
|
+
const snapshot = rememberSessionVisionModeAuthorityForAgent(authority, agent);
|
|
262
|
+
return authorityTurn.run({ authority: snapshot }, callback);
|
|
225
263
|
}
|
|
226
|
-
|
|
227
264
|
export function currentSessionVisionModeAuthority() {
|
|
228
|
-
|
|
265
|
+
return authorityTurn.getStore()?.authority;
|
|
229
266
|
}
|
|
230
|
-
|
|
231
267
|
export function sessionVisionModeAuthorityForAgent(agent) {
|
|
232
|
-
|
|
268
|
+
return isWeakKey(agent) ? agentAuthorities.get(agent) : undefined;
|
|
233
269
|
}
|
|
234
|
-
|
|
235
270
|
/** Drop completed/abandoned step and staged assembly snapshots. */
|
|
236
271
|
export function clearSessionVisionModeAuthorityForAgent(agent) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
272
|
+
if (!isWeakKey(agent))
|
|
273
|
+
return;
|
|
274
|
+
agentAuthorities.delete(agent);
|
|
275
|
+
assemblyAuthorities.delete(agent);
|
|
240
276
|
}
|
|
241
|
-
|
|
242
277
|
/**
|
|
243
278
|
* Execution-time fail-closed check. Real Agent steps use the prompt-assembly
|
|
244
279
|
* snapshot after pre-step promotes it to the active step, so a concurrent
|
|
@@ -246,9 +281,11 @@ export function clearSessionVisionModeAuthorityForAgent(agent) {
|
|
|
246
281
|
* current request in half. Direct/internal calls with an Agent but no captured
|
|
247
282
|
* step resolve the live DSH selection synchronously.
|
|
248
283
|
*/
|
|
249
|
-
export function visionModeEnabledForAgent(ctx, agent, fallbackConfig = {}) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
284
|
+
export function visionModeEnabledForAgent(ctx, agent, fallbackConfig = {}, options = {}) {
|
|
285
|
+
if (!isWeakKey(agent))
|
|
286
|
+
return true;
|
|
287
|
+
const snapshot = agentAuthorities.get(agent);
|
|
288
|
+
if (snapshot)
|
|
289
|
+
return snapshot.enabled === true;
|
|
290
|
+
return resolveSessionVisionModeAuthority(ctx, agent, fallbackConfig, options).enabled === true;
|
|
254
291
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { currentSessionVisionPolicy } from './native-image-coexistence.js'
|
|
2
|
+
import { userSessionVisionPolicy } from './session-vision-policy.js'
|
|
2
3
|
import {
|
|
3
4
|
clearSessionVisionModeAuthorityForAgent,
|
|
4
5
|
rememberSessionVisionModeAssemblyAuthorityForAgent,
|
|
@@ -245,7 +246,7 @@ function projectAssemblyTools(assembly, enabled, owned) {
|
|
|
245
246
|
return { ...assembly, tools }
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
function wrapTools(tools, ctx, config, owned) {
|
|
249
|
+
function wrapTools(tools, ctx, config, owned, sessionPolicyForAgent) {
|
|
249
250
|
if (!isObject(tools)) return tools
|
|
250
251
|
return new Proxy(tools, {
|
|
251
252
|
get(target, property) {
|
|
@@ -272,7 +273,12 @@ function wrapTools(tools, ctx, config, owned) {
|
|
|
272
273
|
async execute(args, exec) {
|
|
273
274
|
if (
|
|
274
275
|
exec?.agent &&
|
|
275
|
-
!visionModeEnabledForAgent(
|
|
276
|
+
!visionModeEnabledForAgent(
|
|
277
|
+
ctx,
|
|
278
|
+
exec.agent,
|
|
279
|
+
liveConfig(ctx, config),
|
|
280
|
+
{ sessionPolicy: sessionPolicyForAgent(exec.agent) },
|
|
281
|
+
)
|
|
276
282
|
) {
|
|
277
283
|
throw visionModeDisabledError(definition.name)
|
|
278
284
|
}
|
|
@@ -322,19 +328,47 @@ function wrapTools(tools, ctx, config, owned) {
|
|
|
322
328
|
* It deliberately lives outside the retired legacy-core policy bridge, whose
|
|
323
329
|
* identity-only closure contract remains frozen.
|
|
324
330
|
*/
|
|
325
|
-
export function installSessionVisionModeBoundary(ctx, config = {}) {
|
|
331
|
+
export function installSessionVisionModeBoundary(ctx, config = {}, runtime = {}) {
|
|
326
332
|
if (!isObject(ctx)) return { ctx, config }
|
|
327
333
|
const restrictions = new WeakMap()
|
|
328
334
|
const activeRestrictions = new Set()
|
|
329
335
|
const assemblySnapshots = new WeakMap()
|
|
330
336
|
const ownedVisionTools = new Map()
|
|
337
|
+
const sessionVisionPolicyStore = runtime?.sessionVisionPolicyStore
|
|
331
338
|
let toolsView
|
|
332
339
|
|
|
340
|
+
// This boundary can install Agent-scoped restrictions whose lifetime exceeds
|
|
341
|
+
// the plugin generation. Establish the generation owner before registering
|
|
342
|
+
// prompt/event hooks that can ever create those restrictions.
|
|
343
|
+
try {
|
|
344
|
+
if (typeof ctx.effect === 'function') {
|
|
345
|
+
ctx.effect(
|
|
346
|
+
() => () => releaseAllAgentToolRestrictions(activeRestrictions),
|
|
347
|
+
'vision-router: Session Vision restriction lifecycle',
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
return { ctx, config }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const sessionPolicyForAgent = (agent) => {
|
|
355
|
+
const id = typeof agent?.id === 'string' ? agent.id : ''
|
|
356
|
+
if (id === '' || typeof sessionVisionPolicyStore?.get !== 'function') return undefined
|
|
357
|
+
try {
|
|
358
|
+
return sessionVisionPolicyStore.get(id)
|
|
359
|
+
} catch {
|
|
360
|
+
return undefined
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
333
364
|
const resolveForAgent = (agent, options = {}) => resolveSessionVisionModeAuthority(
|
|
334
365
|
ctx,
|
|
335
366
|
agent,
|
|
336
367
|
liveConfig(ctx, config),
|
|
337
|
-
|
|
368
|
+
{
|
|
369
|
+
...options,
|
|
370
|
+
sessionPolicy: options.sessionPolicy ?? sessionPolicyForAgent(agent),
|
|
371
|
+
},
|
|
338
372
|
)
|
|
339
373
|
|
|
340
374
|
const clearAgent = (agent) => {
|
|
@@ -381,7 +415,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
|
|
|
381
415
|
// Code Mode's tools:sdk are generated for real Agent-loop assemblies. The
|
|
382
416
|
// returned variable is unused and never reaches the model.
|
|
383
417
|
try {
|
|
384
|
-
const systemPrompt = ctx?.
|
|
418
|
+
const systemPrompt = ctx?.get?.('systemPrompt')
|
|
385
419
|
if (typeof systemPrompt?.variable === 'function') {
|
|
386
420
|
systemPrompt.variable(MODE_SYNC_VARIABLE, (context) => {
|
|
387
421
|
captureForAssembly(context)
|
|
@@ -437,27 +471,34 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
|
|
|
437
471
|
ctx.on?.('agent/disposed', ({ agent }) => {
|
|
438
472
|
clearAgent(agent)
|
|
439
473
|
releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
|
|
474
|
+
const id = typeof agent?.id === 'string' ? agent.id : ''
|
|
475
|
+
if (id !== '') {
|
|
476
|
+
try { sessionVisionPolicyStore?.release?.(id) } catch {}
|
|
477
|
+
}
|
|
440
478
|
})
|
|
441
|
-
} catch {
|
|
442
|
-
// Minimum Hosts may not expose every lifecycle event. Prompt assembly and
|
|
443
|
-
// execution guards preserve the safety boundary without making apply fail.
|
|
444
|
-
}
|
|
445
479
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
480
|
+
// DSH persists explicit composer/model-picker intent as model/selection.
|
|
481
|
+
// Materialize the matching DVR Session authority from that exact durable
|
|
482
|
+
// event so a delegated snapshot can always be overridden by a later user
|
|
483
|
+
// choice. Route-derived compatibility remains the fallback until a Session
|
|
484
|
+
// has made an explicit selection.
|
|
485
|
+
ctx.on?.('session/event', (session, event) => {
|
|
486
|
+
if (event?.type !== 'model/selection') return
|
|
487
|
+
const id = typeof session?.id === 'string' ? session.id : ''
|
|
488
|
+
if (id === '' || typeof sessionVisionPolicyStore?.set !== 'function') return
|
|
489
|
+
const authority = resolveSessionVisionModeAuthority(
|
|
490
|
+
ctx,
|
|
491
|
+
{ id, session },
|
|
492
|
+
liveConfig(ctx, config),
|
|
493
|
+
{ route: event.data },
|
|
457
494
|
)
|
|
458
|
-
|
|
495
|
+
void sessionVisionPolicyStore
|
|
496
|
+
.set(id, userSessionVisionPolicy(authority.enabled))
|
|
497
|
+
.catch(() => {})
|
|
498
|
+
})
|
|
459
499
|
} catch {
|
|
460
|
-
//
|
|
500
|
+
// Minimum Hosts may not expose every lifecycle event. Prompt assembly and
|
|
501
|
+
// execution guards preserve the safety boundary without making apply fail.
|
|
461
502
|
}
|
|
462
503
|
|
|
463
504
|
const wrapped = new Proxy(ctx, {
|
|
@@ -469,6 +510,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
|
|
|
469
510
|
target,
|
|
470
511
|
config,
|
|
471
512
|
ownedVisionTools,
|
|
513
|
+
sessionPolicyForAgent,
|
|
472
514
|
)
|
|
473
515
|
}
|
|
474
516
|
return toolsView
|
|
@@ -493,6 +535,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
|
|
|
493
535
|
{
|
|
494
536
|
turn: payload?.turn,
|
|
495
537
|
visionPolicy: currentSessionVisionPolicy(),
|
|
538
|
+
sessionPolicy: sessionPolicyForAgent(agent),
|
|
496
539
|
},
|
|
497
540
|
)
|
|
498
541
|
syncAgentToolRestriction(
|