dsh-vision-router 2.3.0 → 3.0.1
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 +18 -3
- package/README.zh.md +16 -3
- 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 +447 -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/dsh-support-window.md +13 -3
- package/docs/releases/v3.0.0.md +85 -0
- package/docs/releases/v3.0.1.md +77 -0
- package/entry.js +1 -1
- package/index.js +110 -71
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/core-apply-lifecycle.js +37 -43
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +11 -144
- 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/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +26 -230
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- 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/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-vision-stabilizer.js +33 -18
- package/lib/maintenance-routes.js +1 -3
- 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 +23 -44
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +17 -4
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +30 -10
- 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-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 +25 -107
- 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 +16 -10
- package/lib/structured-flow-hardening.js +22 -4
- 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 +16 -0
- package/lib/vision-background-benchmark.js +55 -6
- 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.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 +42 -4
- 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-model-visibility-boundary-main.js +15 -16
- package/lib/vision-provider-transport.js +137 -147
- 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-tool-runtime-boundary.js +99 -30
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +4 -7
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +37 -11
|
@@ -14,14 +14,11 @@ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
14
14
|
var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
15
15
|
|
|
16
16
|
function safeGet(ctx, name) {
|
|
17
|
-
if (!ctx) return undefined;
|
|
17
|
+
if (!ctx || typeof ctx.get !== 'function') return undefined;
|
|
18
18
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
} catch (_) {}
|
|
24
|
-
try { return ctx[name]; } catch (_) { return undefined; }
|
|
19
|
+
var value = ctx.get(name);
|
|
20
|
+
return value === undefined || value === null ? undefined : value;
|
|
21
|
+
} catch (_) { return undefined; }
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
function isLoopbackLocation(locationLike) {
|
|
@@ -200,18 +197,27 @@ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
200
197
|
get: function(target, property) {
|
|
201
198
|
if (property === 'settingsScope') {
|
|
202
199
|
var original = safeGet(target, 'settingsScope');
|
|
203
|
-
if (original && typeof original.bind === 'function') return original;
|
|
204
200
|
// DSH 0.1.7 configForms only mirrors Config fields declared volatile.
|
|
205
201
|
// DVR must keep its public Config ordinary for the older supported Host
|
|
206
202
|
// window, so its namespace intentionally uses the local-only ConfigEditor
|
|
207
|
-
// bridge
|
|
203
|
+
// bridge. Even when a Host exposes a legacy-shaped settingsScope, its
|
|
204
|
+
// vision-router view may be empty; keep it only as the delegate for
|
|
205
|
+
// every other namespace instead of letting it bypass the local bridge.
|
|
208
206
|
var official = configFormsBinder(target);
|
|
209
|
-
return syntheticBinder(
|
|
207
|
+
return syntheticBinder(
|
|
208
|
+
ctx,
|
|
209
|
+
original && typeof original.bind === 'function' ? original : official
|
|
210
|
+
);
|
|
210
211
|
}
|
|
211
212
|
if (property === 'get') {
|
|
212
213
|
var getter = Reflect.get(target, property, target);
|
|
213
214
|
if (typeof getter !== 'function') return getter;
|
|
214
215
|
return function(name) {
|
|
216
|
+
if (name === 'settingsScope') {
|
|
217
|
+
var original = safeGet(target, 'settingsScope');
|
|
218
|
+
if (original && typeof original.bind === 'function') return original;
|
|
219
|
+
return syntheticBinder(ctx, configFormsBinder(target));
|
|
220
|
+
}
|
|
215
221
|
var value = getter.call(target, name);
|
|
216
222
|
return name === 'connection' ? normalizeConnection(value) : value;
|
|
217
223
|
};
|
|
@@ -630,7 +630,7 @@ function wrapSettingsScope(scope) {
|
|
|
630
630
|
})
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
-
function wrapSettingsService(settings,
|
|
633
|
+
function wrapSettingsService(settings, bindScope) {
|
|
634
634
|
if (!settings || typeof settings !== 'object') return settings
|
|
635
635
|
return new Proxy(settings, {
|
|
636
636
|
get(target, property) {
|
|
@@ -643,7 +643,7 @@ function wrapSettingsService(settings, setScope) {
|
|
|
643
643
|
return (namespace, ...args) => {
|
|
644
644
|
const scope = register.call(target, namespace, ...args)
|
|
645
645
|
if (namespace !== 'vision-router') return scope
|
|
646
|
-
|
|
646
|
+
if (bindScope(scope) !== true) return scope
|
|
647
647
|
return wrapSettingsScope(scope)
|
|
648
648
|
}
|
|
649
649
|
},
|
|
@@ -652,7 +652,8 @@ function wrapSettingsService(settings, setScope) {
|
|
|
652
652
|
|
|
653
653
|
function wrapSettingsContext(childCtx, setScope) {
|
|
654
654
|
if (!childCtx || typeof childCtx !== 'object' || !childCtx.settings) return childCtx
|
|
655
|
-
const
|
|
655
|
+
const bindScope = (scope) => setScope(scope, childCtx)
|
|
656
|
+
const settings = wrapSettingsService(childCtx.settings, bindScope)
|
|
656
657
|
return new Proxy(childCtx, {
|
|
657
658
|
get(target, property) {
|
|
658
659
|
if (property === 'settings') return settings
|
|
@@ -666,7 +667,24 @@ export function installStructuredFlowHardening(ctx, config = {}) {
|
|
|
666
667
|
if (!ctx || typeof ctx !== 'object') return ctx
|
|
667
668
|
const states = new WeakMap()
|
|
668
669
|
let settingsScope
|
|
669
|
-
const setSettingsScope = (scope) => {
|
|
670
|
+
const setSettingsScope = (scope, ownerCtx) => {
|
|
671
|
+
if (!scope || (typeof scope !== 'object' && typeof scope !== 'function')) return false
|
|
672
|
+
// Settings is a dynamic child service. Never retain a scope unless the
|
|
673
|
+
// child fiber owns its lifetime; otherwise an HMR gap can read stale config.
|
|
674
|
+
try {
|
|
675
|
+
if (typeof ownerCtx?.effect !== 'function') return false
|
|
676
|
+
ownerCtx.effect(
|
|
677
|
+
() => () => {
|
|
678
|
+
if (settingsScope === scope) settingsScope = undefined
|
|
679
|
+
},
|
|
680
|
+
'vision-router: structured flow settings lifecycle',
|
|
681
|
+
)
|
|
682
|
+
} catch {
|
|
683
|
+
return false
|
|
684
|
+
}
|
|
685
|
+
settingsScope = scope
|
|
686
|
+
return true
|
|
687
|
+
}
|
|
670
688
|
const activeConfig = () => {
|
|
671
689
|
try {
|
|
672
690
|
const value = settingsScope && typeof settingsScope.get === 'function' ? settingsScope.get() : config
|
|
@@ -326,7 +326,15 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
if (ctx && typeof ctx.effect === 'function') {
|
|
329
|
-
|
|
329
|
+
try {
|
|
330
|
+
ctx.effect(() => dispose, 'vision-router: execFile compatibility')
|
|
331
|
+
} catch (error) {
|
|
332
|
+
// The process-global execFile seam was already mutated above. If Cordis
|
|
333
|
+
// cannot own this generation, undo exactly this installation before the
|
|
334
|
+
// failure escapes so no orphan patch survives HMR/startup failure.
|
|
335
|
+
dispose()
|
|
336
|
+
throw error
|
|
337
|
+
}
|
|
330
338
|
}
|
|
331
339
|
return dispose
|
|
332
340
|
}
|
|
@@ -1,58 +1,64 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
-
|
|
3
|
-
const turnBudgetScope = new AsyncLocalStorage()
|
|
4
|
-
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const turnBudgetScope = new AsyncLocalStorage();
|
|
5
3
|
function usableSignal(value) {
|
|
6
|
-
|
|
4
|
+
return value !== null
|
|
5
|
+
&& typeof value === 'object'
|
|
6
|
+
&& typeof value.aborted === 'boolean'
|
|
7
|
+
? value
|
|
8
|
+
: undefined;
|
|
7
9
|
}
|
|
8
|
-
|
|
9
10
|
function finiteDeadline(value) {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const number = Number(value);
|
|
12
|
+
return Number.isFinite(number) ? number : undefined;
|
|
13
|
+
}
|
|
14
|
+
function objectBudget(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: undefined;
|
|
12
18
|
}
|
|
13
|
-
|
|
14
19
|
/**
|
|
15
20
|
* Run one async tool execution inside an ambient turn-level budget.
|
|
16
21
|
* Nested budgets may tighten the parent, but may not discard an earlier parent
|
|
17
22
|
* deadline, cancellation signal or artifact-run identity.
|
|
18
23
|
*/
|
|
19
24
|
export function runWithVisionTurnBudget(budget, fn) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
parent
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
25
|
+
if (typeof fn !== 'function')
|
|
26
|
+
throw new TypeError('runWithVisionTurnBudget: fn must be a function');
|
|
27
|
+
const parent = turnBudgetScope.getStore();
|
|
28
|
+
const parentSignal = usableSignal(parent?.signal);
|
|
29
|
+
const childSignal = usableSignal(budget?.signal);
|
|
30
|
+
const parentDeadline = finiteDeadline(parent?.deadlineAt);
|
|
31
|
+
const childDeadline = finiteDeadline(budget?.deadlineAt);
|
|
32
|
+
let next = objectBudget(budget);
|
|
33
|
+
if (parentSignal && childSignal && parentSignal !== childSignal) {
|
|
34
|
+
next = { ...(next ?? {}), signal: AbortSignal.any([parentSignal, childSignal]) };
|
|
35
|
+
}
|
|
36
|
+
else if (parentSignal && !childSignal) {
|
|
37
|
+
next = { ...(next ?? {}), signal: parentSignal };
|
|
38
|
+
}
|
|
39
|
+
if (parentDeadline !== undefined && (childDeadline === undefined || parentDeadline < childDeadline)) {
|
|
40
|
+
next = { ...(next ?? {}), deadlineAt: parentDeadline };
|
|
41
|
+
}
|
|
42
|
+
if (typeof parent?.artifactRunId === 'string'
|
|
43
|
+
&& parent.artifactRunId !== ''
|
|
44
|
+
&& !(typeof next?.artifactRunId === 'string' && next.artifactRunId !== '')) {
|
|
45
|
+
next = { ...(next ?? {}), artifactRunId: parent.artifactRunId };
|
|
46
|
+
}
|
|
47
|
+
return turnBudgetScope.run(next, fn);
|
|
43
48
|
}
|
|
44
|
-
|
|
45
49
|
export function currentVisionTurnBudget() {
|
|
46
|
-
|
|
50
|
+
return turnBudgetScope.getStore();
|
|
47
51
|
}
|
|
48
|
-
|
|
49
52
|
export function currentVisionTurnBudgetSignal() {
|
|
50
|
-
|
|
53
|
+
return usableSignal(turnBudgetScope.getStore()?.signal);
|
|
51
54
|
}
|
|
52
|
-
|
|
53
55
|
export function remainingVisionTurnBudgetMs(now = Date.now) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const budget = turnBudgetScope.getStore();
|
|
57
|
+
const deadlineAt = finiteDeadline(budget?.deadlineAt);
|
|
58
|
+
if (deadlineAt === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const clock = typeof now === 'function'
|
|
61
|
+
? now
|
|
62
|
+
: Date.now;
|
|
63
|
+
return Math.max(0, deadlineAt - Number(clock()));
|
|
58
64
|
}
|
package/lib/update-check.js
CHANGED
|
@@ -5,6 +5,15 @@ import { stripTrailingSlashes } from './string-normalization.js'
|
|
|
5
5
|
const require = createRequire(import.meta.url)
|
|
6
6
|
const packageJson = require('../package.json')
|
|
7
7
|
|
|
8
|
+
// Capture the process fetch once, at module evaluation, exactly like the
|
|
9
|
+
// Router-owned provider transport. Reading `globalThis.fetch` per call would
|
|
10
|
+
// re-admit DVR's own later-installed process-wide wrappers (legacy proxy,
|
|
11
|
+
// i18n/pi-ai bridge) into DVR's product-metadata requests, and would make the
|
|
12
|
+
// transport used for a request depend on when the request happens to run.
|
|
13
|
+
const moduleFetch = typeof globalThis.fetch === 'function'
|
|
14
|
+
? globalThis.fetch.bind(globalThis)
|
|
15
|
+
: undefined
|
|
16
|
+
|
|
8
17
|
export const PACKAGE_NAME = 'dsh-vision-router'
|
|
9
18
|
export const CURRENT_VERSION = String(packageJson.version ?? '')
|
|
10
19
|
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
@@ -149,7 +158,7 @@ async function fetchLatestReleaseVersion({ fetchImpl, releaseApi, signal, timeou
|
|
|
149
158
|
* read-only metadata request against npmjs instead of showing a false failure.
|
|
150
159
|
*/
|
|
151
160
|
export async function checkPackageUpdate({
|
|
152
|
-
fetchImpl =
|
|
161
|
+
fetchImpl = moduleFetch,
|
|
153
162
|
currentVersion = CURRENT_VERSION,
|
|
154
163
|
registry = registryBaseFromEnv(),
|
|
155
164
|
fallbackRegistry = DEFAULT_NPM_REGISTRY,
|
|
@@ -247,7 +256,7 @@ export async function checkPackageUpdate({
|
|
|
247
256
|
* settings card reuses that result, and the manual button can force a refresh.
|
|
248
257
|
*/
|
|
249
258
|
export function createCachedUpdateChecker({
|
|
250
|
-
fetchImpl =
|
|
259
|
+
fetchImpl = moduleFetch,
|
|
251
260
|
currentVersion = CURRENT_VERSION,
|
|
252
261
|
registry = registryBaseFromEnv(),
|
|
253
262
|
fallbackRegistry = DEFAULT_NPM_REGISTRY,
|
|
@@ -1,84 +1,91 @@
|
|
|
1
|
-
const SHA256_HANDLE_PREFIX = /^sha256:/i
|
|
2
|
-
const CANONICAL_SHA256_ID = /^sha256:[0-9a-f]{64}$/i
|
|
3
|
-
const PROJECTED_SHA256_HANDLE = /^sha256:[0-9a-f]{8,63}$/i
|
|
4
|
-
|
|
1
|
+
const SHA256_HANDLE_PREFIX = /^sha256:/i;
|
|
2
|
+
const CANONICAL_SHA256_ID = /^sha256:[0-9a-f]{64}$/i;
|
|
3
|
+
const PROJECTED_SHA256_HANDLE = /^sha256:[0-9a-f]{8,63}$/i;
|
|
5
4
|
const TOOL_FIELDS = Object.freeze({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
vision_describe: { arrays: ['attachmentIds', 'paths'] },
|
|
6
|
+
vision_bootstrap: { arrays: ['attachmentIds', 'paths'] },
|
|
7
|
+
vision_materialize: { scalars: ['image'] },
|
|
8
|
+
vision_ground: { scalars: ['image'] },
|
|
9
|
+
vision_detect: { scalars: ['image'] },
|
|
10
|
+
vision_crop: { scalars: ['image'] },
|
|
11
|
+
vision_present: { scalars: ['image'] },
|
|
12
|
+
vision_pixel_diff: { scalars: ['original', 'rebuilt'] },
|
|
13
|
+
vision_colors: { scalars: ['image'] },
|
|
14
|
+
vision_ocr: { scalars: ['image'] },
|
|
15
|
+
vision_trace: { scalars: ['image'] },
|
|
16
|
+
vision_extract_foreground: { scalars: ['image'] },
|
|
17
|
+
});
|
|
18
|
+
function objectRecord(value) {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
20
|
+
? value
|
|
21
|
+
: undefined;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
23
|
function attachmentIdOf(ref) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const value = objectRecord(ref);
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
const raw = value.attachmentId ?? value.id;
|
|
28
|
+
if (raw === undefined || raw === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const id = String(raw).trim();
|
|
31
|
+
return id === '' ? undefined : id;
|
|
30
32
|
}
|
|
31
|
-
|
|
32
33
|
function sessionMessages(session) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
try {
|
|
35
|
+
if (!session || typeof session.deriveMessages !== 'function')
|
|
36
|
+
return [];
|
|
37
|
+
const messages = session.deriveMessages();
|
|
38
|
+
return Array.isArray(messages) ? messages : [];
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
|
-
|
|
42
44
|
function collectImageRefsFromBlocks(blocks, out) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if (!Array.isArray(blocks))
|
|
46
|
+
return;
|
|
47
|
+
for (const block of blocks) {
|
|
48
|
+
const value = objectRecord(block);
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (value.type === 'image') {
|
|
52
|
+
const attachment = objectRecord(value.attachment);
|
|
53
|
+
if (attachment !== undefined)
|
|
54
|
+
out.push(attachment);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value.content))
|
|
57
|
+
collectImageRefsFromBlocks(value.content, out);
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
|
-
|
|
51
60
|
function collectImageRefsFromMessages(messages, out) {
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
if (!Array.isArray(messages))
|
|
62
|
+
return;
|
|
63
|
+
for (const message of messages) {
|
|
64
|
+
collectImageRefsFromBlocks(objectRecord(message)?.content, out);
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
|
-
|
|
56
67
|
function authorizedRefs(agent) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const refs = [];
|
|
69
|
+
collectImageRefsFromMessages(sessionMessages(agent?.session), refs);
|
|
70
|
+
collectImageRefsFromMessages(agent?.inbox?.nextTurn, refs);
|
|
71
|
+
collectImageRefsFromMessages(agent?.inbox?.nextStep, refs);
|
|
72
|
+
const unique = new Map();
|
|
73
|
+
for (const ref of refs) {
|
|
74
|
+
const id = attachmentIdOf(ref);
|
|
75
|
+
if (id !== undefined && !unique.has(id))
|
|
76
|
+
unique.set(id, ref);
|
|
77
|
+
}
|
|
78
|
+
return [...unique.values()];
|
|
68
79
|
}
|
|
69
|
-
|
|
70
80
|
export function isProjectedAttachmentHandle(value) {
|
|
71
|
-
|
|
81
|
+
return typeof value === 'string' && PROJECTED_SHA256_HANDLE.test(value.trim());
|
|
72
82
|
}
|
|
73
|
-
|
|
74
83
|
function isSha256HandleLike(value) {
|
|
75
|
-
|
|
84
|
+
return typeof value === 'string' && SHA256_HANDLE_PREFIX.test(value.trim());
|
|
76
85
|
}
|
|
77
|
-
|
|
78
86
|
function isCanonicalSha256Id(value) {
|
|
79
|
-
|
|
87
|
+
return CANONICAL_SHA256_ID.test(value.trim());
|
|
80
88
|
}
|
|
81
|
-
|
|
82
89
|
/**
|
|
83
90
|
* Resolve DSH's model-facing text-only image alias back to one canonical
|
|
84
91
|
* attachment ref without changing durable attachment identity semantics.
|
|
@@ -89,91 +96,95 @@ function isCanonicalSha256Id(value) {
|
|
|
89
96
|
* prefix. Zero or multiple matches fail closed.
|
|
90
97
|
*/
|
|
91
98
|
export function resolveProjectedAttachmentHandle(handle, { sessionVisionIndex, agent } = {}) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
99
|
+
const token = typeof handle === 'string' ? handle.trim() : '';
|
|
100
|
+
if (!isProjectedAttachmentHandle(token))
|
|
101
|
+
return { kind: 'not-projected', value: handle };
|
|
102
|
+
const session = agent?.session;
|
|
103
|
+
const refs = authorizedRefs(agent);
|
|
104
|
+
const wanted = token.toLowerCase();
|
|
105
|
+
const matches = refs.filter((ref) => {
|
|
106
|
+
const id = attachmentIdOf(ref);
|
|
107
|
+
return id !== undefined && id.toLowerCase().startsWith(wanted);
|
|
108
|
+
});
|
|
109
|
+
if (matches.length === 0)
|
|
110
|
+
return { kind: 'unknown', handle: token };
|
|
111
|
+
if (matches.length > 1)
|
|
112
|
+
return { kind: 'ambiguous', handle: token };
|
|
113
|
+
const candidate = matches[0];
|
|
114
|
+
const canonicalId = attachmentIdOf(candidate);
|
|
115
|
+
if (candidate === undefined || canonicalId === undefined)
|
|
116
|
+
return { kind: 'unknown', handle: token };
|
|
117
|
+
// The Session event/inbox proves authorization; warm the canonical bounded
|
|
118
|
+
// index with that exact Host-owned ref, then require the index to hand it
|
|
119
|
+
// back. This keeps all downstream tools on the same recovery/identity seam.
|
|
120
|
+
try {
|
|
121
|
+
sessionVisionIndex?.recordAttachments?.(session, [candidate]);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Authorization is still fail-closed if the bounded cache cannot be warmed.
|
|
125
|
+
}
|
|
126
|
+
if (!sessionVisionIndex || typeof sessionVisionIndex.lookupAttachment !== 'function') {
|
|
127
|
+
return { kind: 'unknown', handle: token };
|
|
128
|
+
}
|
|
129
|
+
const ref = sessionVisionIndex.lookupAttachment(session, canonicalId);
|
|
130
|
+
if (ref === undefined)
|
|
131
|
+
return { kind: 'unknown', handle: token };
|
|
132
|
+
return { kind: 'resolved', handle: token, canonicalId, ref };
|
|
124
133
|
}
|
|
125
|
-
|
|
126
134
|
function canonicalizeValue(value, context) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
135
|
+
if (!isSha256HandleLike(value))
|
|
136
|
+
return value;
|
|
137
|
+
if (isCanonicalSha256Id(value))
|
|
138
|
+
return value;
|
|
139
|
+
if (!isProjectedAttachmentHandle(value)) {
|
|
140
|
+
throw new Error(`${context.toolName}: invalid attachment handle "${value.trim()}" ` +
|
|
141
|
+
'(sha256 attachment handles must be a canonical id or a DSH-projected 8+ hex prefix)');
|
|
142
|
+
}
|
|
143
|
+
const result = resolveProjectedAttachmentHandle(value, context);
|
|
144
|
+
if (result.kind === 'resolved')
|
|
145
|
+
return result.canonicalId;
|
|
146
|
+
if (result.kind === 'ambiguous') {
|
|
147
|
+
throw new Error(`${context.toolName}: ambiguous attachment handle "${result.handle}" ` +
|
|
148
|
+
'(multiple images in this conversation share that prefix; use a full attachment id or attach the image again)');
|
|
149
|
+
}
|
|
150
|
+
if (result.kind === 'unknown') {
|
|
151
|
+
throw new Error(`${context.toolName}: unknown attachment handle "${result.handle}" ` +
|
|
152
|
+
'(it is not authorized by an image in this conversation; attach the image again if needed)');
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
147
155
|
}
|
|
148
|
-
|
|
149
156
|
function canonicalizeArgs(args, context, fields) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
const record = objectRecord(args);
|
|
158
|
+
if (record === undefined)
|
|
159
|
+
return args;
|
|
160
|
+
let next;
|
|
161
|
+
for (const field of fields.scalars ?? []) {
|
|
162
|
+
if (!Object.hasOwn(record, field))
|
|
163
|
+
continue;
|
|
164
|
+
const value = canonicalizeValue(record[field], context);
|
|
165
|
+
if (value !== record[field]) {
|
|
166
|
+
next ??= { ...record };
|
|
167
|
+
next[field] = value;
|
|
168
|
+
}
|
|
158
169
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
for (const field of fields.arrays ?? []) {
|
|
171
|
+
const values = record[field];
|
|
172
|
+
if (!Array.isArray(values))
|
|
173
|
+
continue;
|
|
174
|
+
let changed = false;
|
|
175
|
+
const mapped = values.map((value) => {
|
|
176
|
+
const canonical = canonicalizeValue(value, context);
|
|
177
|
+
if (canonical !== value)
|
|
178
|
+
changed = true;
|
|
179
|
+
return canonical;
|
|
180
|
+
});
|
|
181
|
+
if (changed) {
|
|
182
|
+
next ??= { ...record };
|
|
183
|
+
next[field] = mapped;
|
|
184
|
+
}
|
|
172
185
|
}
|
|
173
|
-
|
|
174
|
-
return next ?? args
|
|
186
|
+
return next ?? args;
|
|
175
187
|
}
|
|
176
|
-
|
|
177
188
|
/**
|
|
178
189
|
* Canonicalize only image-source arguments of Vision Router tools. Ordinary
|
|
179
190
|
* prose fields are never scanned, so a user mentioning `sha256:deadbeef` in a
|
|
@@ -182,19 +193,28 @@ function canonicalizeArgs(args, context, fields) {
|
|
|
182
193
|
* fails closed rather than falling through to cwd-relative filesystem lookup.
|
|
183
194
|
*/
|
|
184
195
|
export function wrapVisionAttachmentHandleDefinition(def, options = {}) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
// Tool definitions arrive from the Host registry at runtime. Static generic
|
|
197
|
+
// constraints cannot replace this boundary guard: partial/legacy Hosts may
|
|
198
|
+
// still pass nullish or malformed values before their own validation runs.
|
|
199
|
+
if (def === null || typeof def !== 'object' || typeof def.execute !== 'function')
|
|
200
|
+
return def;
|
|
201
|
+
const toolName = def.name;
|
|
202
|
+
const fields = typeof toolName === 'string'
|
|
203
|
+
? TOOL_FIELDS[toolName]
|
|
204
|
+
: undefined;
|
|
205
|
+
const sessionVisionIndex = options.sessionVisionIndex;
|
|
206
|
+
if (!fields || !sessionVisionIndex || typeof toolName !== 'string')
|
|
207
|
+
return def;
|
|
208
|
+
const execute = def.execute;
|
|
209
|
+
return {
|
|
210
|
+
...def,
|
|
211
|
+
execute(args, exec) {
|
|
212
|
+
const nextArgs = canonicalizeArgs(args, {
|
|
213
|
+
sessionVisionIndex,
|
|
214
|
+
...(exec?.agent === undefined ? {} : { agent: exec.agent }),
|
|
215
|
+
toolName,
|
|
216
|
+
}, fields);
|
|
217
|
+
return execute.call(def, nextArgs, exec);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
200
220
|
}
|