dsh-vision-router 2.2.8 → 2.3.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 +7 -7
- package/README.zh.md +7 -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/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +27 -21
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/entry.js +8 -56
- package/index.js +161 -691
- 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 +47 -0
- package/lib/core-primitives.js +88 -135
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/dsh-contract-compat.js +11 -26
- package/lib/dsh-support-window.js +3 -3
- package/lib/http-compat.js +1 -2
- package/lib/local-remote-settings-permission.js +33 -7
- package/lib/maintenance-routes.js +136 -0
- package/lib/public-config.js +57 -0
- package/lib/public-entry.js +2 -7
- 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/runtime-composition.js +105 -30
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-vision-index.js +154 -9
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +3 -2
- package/lib/vision-backend-runtime-policy.js +3 -2
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-diagnostics-routes.js +216 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-provider-transport.js +0 -29
- package/lib/vision-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +5 -1
- package/lib/web/remote-settings-client.js +2 -2
- package/package.json +23 -13
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain + pixel-level tools. Remote vision providers, including the default free fallback, receive image content unless strict local-only use is configured.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,30 +54,36 @@
|
|
|
54
54
|
"undici": "^7.29.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@deepseek-ai/dsh-anonymous-user-id": "
|
|
58
|
-
"@deepseek-ai/dsh-llm-deepseek": "
|
|
57
|
+
"@deepseek-ai/dsh-anonymous-user-id": ">=0.1.5-rc.1 <0.2.0-0 || >=0.2.0-rc.2 <0.3.0-0",
|
|
58
|
+
"@deepseek-ai/dsh-llm-deepseek": ">=0.1.5-rc.1 <0.2.0-0 || >=0.2.0-rc.2 <0.3.0-0",
|
|
59
59
|
"sharp": ">=0.35.3 <1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
|
-
"@deepseek-ai/dsh-anonymous-user-id": {
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
"@deepseek-ai/dsh-anonymous-user-id": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"@deepseek-ai/dsh-llm-deepseek": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"sharp": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
65
71
|
},
|
|
66
72
|
"devDependencies": {
|
|
67
|
-
"@deepseek-ai/dsh-anonymous-user-id": "
|
|
68
|
-
"@deepseek-ai/dsh-llm-deepseek": "
|
|
73
|
+
"@deepseek-ai/dsh-anonymous-user-id": "0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.2.0-rc.2",
|
|
69
75
|
"fast-check": "4.10.0",
|
|
70
76
|
"sharp": "^0.35.4"
|
|
71
77
|
},
|
|
72
78
|
"scripts": {
|
|
73
|
-
"test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/cache-atomic-write-concurrency.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/
|
|
79
|
+
"test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/cache-atomic-write-concurrency.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/release-registry-identity.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/architecture-2.3-gate.test.js tests/esm-cycle-audit.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/local-only-vision.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/vision-diagnostics-routes.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/free-cloud-first.test.js tests/dsh-host-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/issue-512-tool-restriction.test.js tests/issue-606-session-source-ownership.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/long-ocr-empty-retry.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
|
|
74
80
|
"test:core": "node --test tests/core.test.js tests/degraded-local-evidence.test.js tests/runtime-e2e.test.js tests/structured-bootstrap.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-execution-policy.test.js tests/vision-backend-runtime-policy.test.js",
|
|
75
81
|
"test:routing": "node --test tests/issue-374-delegated-call-config.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/vision-routing-product.test.js tests/vision-capability-router.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow-health.test.js tests/vision-runtime-performance.test.js tests/vision-background-benchmark.test.js tests/vision-image-input-verdict.test.js",
|
|
76
82
|
"test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/replay-delegation.test.js tests/legacy-session-repair.test.js tests/session-repair-v2.test.js tests/issue-276-session-image-ownership.test.js",
|
|
77
83
|
"test:resources": "node --test tests/artifact-path-security.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js",
|
|
78
|
-
"test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/
|
|
84
|
+
"test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
|
|
79
85
|
"test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js",
|
|
80
|
-
"test:contract": "node --test tests/manifest-dependencies.test.js tests/bundle-defaults.test.js tests/zero-regression-gate.test.js tests/issue-289-native-nonintervention.test.js tests/issue-374-delegated-call-config.test.js tests/runtime-boundary-fixes.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/adapter-prepare-call-compat.test.js tests/
|
|
86
|
+
"test:contract": "node --test tests/manifest-dependencies.test.js tests/bundle-defaults.test.js tests/zero-regression-gate.test.js tests/issue-289-native-nonintervention.test.js tests/issue-374-delegated-call-config.test.js tests/runtime-boundary-fixes.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/adapter-prepare-call-compat.test.js tests/dsh-host-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js",
|
|
81
87
|
"test:grounding": "node --test tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/vision-capability-grounding-proof-contract.test.js",
|
|
82
88
|
"test:stress": "node scripts/image-resource-stress.mjs && node --test tests/large-image-resource-integration.test.js tests/resource-retention-lifecycle.test.js",
|
|
83
89
|
"test:adversarial-fuzz": "node scripts/security-adversarial-fuzz.mjs",
|
|
@@ -86,7 +92,9 @@
|
|
|
86
92
|
"quality:round2:render": "node quality/vision-round2/render-fixtures.mjs",
|
|
87
93
|
"quality:round2:template": "node quality/vision-round2/make-run-template.mjs",
|
|
88
94
|
"quality:round2:run": "node quality/vision-round2/run-host-api.mjs",
|
|
89
|
-
"quality:round2:report": "node quality/vision-round2/report-results.mjs"
|
|
95
|
+
"quality:round2:report": "node quality/vision-round2/report-results.mjs",
|
|
96
|
+
"client:sync": "node scripts/sync-client-embedded-modules.mjs",
|
|
97
|
+
"client:check": "node scripts/sync-client-embedded-modules.mjs --check"
|
|
90
98
|
},
|
|
91
99
|
"dsh": {
|
|
92
100
|
"client": {
|
|
@@ -99,6 +107,8 @@
|
|
|
99
107
|
"@deepseek-ai/dsh-api-remotes"
|
|
100
108
|
]
|
|
101
109
|
},
|
|
102
|
-
"bundle": {
|
|
110
|
+
"bundle": {
|
|
111
|
+
"patch": "./cordis.patch.yml"
|
|
112
|
+
}
|
|
103
113
|
}
|
|
104
114
|
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
const wrappedContexts = new WeakMap()
|
|
4
|
-
const MIB = 1024 * 1024
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_ANDROID_TRANSIENT_LIMITS = Object.freeze({
|
|
7
|
-
maxEntries: 64,
|
|
8
|
-
maxBytes: 128 * MIB,
|
|
9
|
-
maxSingleBytes: 64 * MIB,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
function nonEmptyString(value) {
|
|
13
|
-
return typeof value === 'string' && value.length > 0
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function positiveInteger(value, fallback) {
|
|
17
|
-
const n = Number(value)
|
|
18
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isAndroidTermuxRuntime(options = {}) {
|
|
22
|
-
const platform = options.platform ?? process.platform
|
|
23
|
-
const env = options.env ?? process.env
|
|
24
|
-
if (platform === 'android') return true
|
|
25
|
-
if (nonEmptyString(env?.TERMUX_VERSION)) return true
|
|
26
|
-
const prefix = nonEmptyString(env?.PREFIX) ? env.PREFIX : ''
|
|
27
|
-
return prefix.includes('/com.termux/') || prefix.includes('\\com.termux\\')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isPermissionBoundaryError(error) {
|
|
31
|
-
let current = error
|
|
32
|
-
const seen = new Set()
|
|
33
|
-
for (let depth = 0; current && depth < 8; depth += 1) {
|
|
34
|
-
if ((typeof current === 'object' || typeof current === 'function') && seen.has(current)) break
|
|
35
|
-
if (typeof current === 'object' || typeof current === 'function') seen.add(current)
|
|
36
|
-
const code = current && typeof current === 'object' ? current.code : undefined
|
|
37
|
-
if (code === 'EACCES' || code === 'EPERM') return true
|
|
38
|
-
current = current && typeof current === 'object' ? current.cause : undefined
|
|
39
|
-
}
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function displayName(value) {
|
|
44
|
-
if (!nonEmptyString(value)) return undefined
|
|
45
|
-
const leaf = value.slice(Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\')) + 1)
|
|
46
|
-
const clean = leaf.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 255)
|
|
47
|
-
return clean === '' ? undefined : clean
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function probeImage(data, injectedProbe) {
|
|
51
|
-
if (typeof injectedProbe === 'function') return injectedProbe(data)
|
|
52
|
-
const mod = await import('sharp')
|
|
53
|
-
const sharp = mod.default ?? mod
|
|
54
|
-
const metadata = await sharp(data, { failOn: 'none' }).metadata()
|
|
55
|
-
const width = Number(metadata.width ?? 0)
|
|
56
|
-
const height = Number(metadata.height ?? 0)
|
|
57
|
-
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
|
|
58
|
-
throw new Error('vision-router: Android attachment fallback could not read image dimensions')
|
|
59
|
-
}
|
|
60
|
-
return { width, height }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function transientTooLargeError(bytes, limit) {
|
|
64
|
-
const error = new Error(
|
|
65
|
-
`vision-router: Android attachment fallback refuses to keep a ${bytes}-byte image in process memory (limit ${limit} bytes)`,
|
|
66
|
-
)
|
|
67
|
-
error.code = 'ANDROID_TRANSIENT_ATTACHMENT_TOO_LARGE'
|
|
68
|
-
return error
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function transientStoredImage(input, injectedProbe, maxSingleBytes) {
|
|
72
|
-
const data = input?.data
|
|
73
|
-
if (!(data instanceof Uint8Array) || data.byteLength === 0) {
|
|
74
|
-
throw new Error('vision-router: Android attachment fallback received empty image bytes')
|
|
75
|
-
}
|
|
76
|
-
// Enforce the byte boundary BEFORE probing with sharp. Metadata decode can
|
|
77
|
-
// itself allocate native memory, so rejecting after probeImage() would still
|
|
78
|
-
// allow a single pathological input to cross the mobile memory boundary.
|
|
79
|
-
if (data.byteLength > maxSingleBytes) {
|
|
80
|
-
throw transientTooLargeError(data.byteLength, maxSingleBytes)
|
|
81
|
-
}
|
|
82
|
-
const mediaType = input?.mediaType
|
|
83
|
-
if (!nonEmptyString(mediaType)) {
|
|
84
|
-
throw new Error('vision-router: Android attachment fallback received no media type')
|
|
85
|
-
}
|
|
86
|
-
const { width, height } = await probeImage(data, injectedProbe)
|
|
87
|
-
const sha256 = createHash('sha256').update(data).digest('hex')
|
|
88
|
-
const name = displayName(input?.name)
|
|
89
|
-
const ref = {
|
|
90
|
-
attachmentId: `sha256:${sha256}`,
|
|
91
|
-
mediaType,
|
|
92
|
-
bytes: data.byteLength,
|
|
93
|
-
width,
|
|
94
|
-
height,
|
|
95
|
-
...(name !== undefined ? { name } : {}),
|
|
96
|
-
}
|
|
97
|
-
return { ref, data }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Byte-weighted LRU for the Android process-local durability fallback. */
|
|
101
|
-
export function createTransientAttachmentCache(options = {}) {
|
|
102
|
-
const maxEntries = positiveInteger(options.maxEntries, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxEntries)
|
|
103
|
-
const maxBytes = positiveInteger(options.maxBytes, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxBytes)
|
|
104
|
-
const map = new Map()
|
|
105
|
-
let bytes = 0
|
|
106
|
-
|
|
107
|
-
const remove = (id) => {
|
|
108
|
-
const old = map.get(id)
|
|
109
|
-
if (old === undefined) return false
|
|
110
|
-
bytes = Math.max(0, bytes - Number(old.data?.byteLength ?? 0))
|
|
111
|
-
return map.delete(id)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const trim = () => {
|
|
115
|
-
while (map.size > maxEntries || bytes > maxBytes) {
|
|
116
|
-
const oldest = map.keys().next().value
|
|
117
|
-
if (oldest === undefined) break
|
|
118
|
-
remove(oldest)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
get(id) {
|
|
124
|
-
const stored = map.get(id)
|
|
125
|
-
if (stored === undefined) return undefined
|
|
126
|
-
// Refresh recency so an actively used image survives pressure from newer
|
|
127
|
-
// one-shot attachments.
|
|
128
|
-
map.delete(id)
|
|
129
|
-
map.set(id, stored)
|
|
130
|
-
return stored
|
|
131
|
-
},
|
|
132
|
-
set(stored) {
|
|
133
|
-
const id = String(stored?.ref?.attachmentId ?? '')
|
|
134
|
-
const weight = Number(stored?.data?.byteLength ?? 0)
|
|
135
|
-
if (id === '' || !Number.isFinite(weight) || weight <= 0 || weight > maxBytes) return false
|
|
136
|
-
remove(id)
|
|
137
|
-
map.set(id, stored)
|
|
138
|
-
bytes += weight
|
|
139
|
-
trim()
|
|
140
|
-
return map.has(id)
|
|
141
|
-
},
|
|
142
|
-
stats() {
|
|
143
|
-
return { entries: map.size, bytes, maxEntries, maxBytes }
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Android/Termux compatibility shim for deepseek-harness attachment-local.
|
|
150
|
-
*
|
|
151
|
-
* Current DSH attachment-local proves directory durability by fsyncing every
|
|
152
|
-
* ancestor from DSH_HOME to the filesystem root. Android SELinux lets Termux
|
|
153
|
-
* access its own /data/data/com.termux subtree but intentionally denies opening
|
|
154
|
-
* /data/data itself, so saveImage() is wrapped as ATTACHMENT_WRITE_FAILED even
|
|
155
|
-
* though the image bytes are otherwise readable.
|
|
156
|
-
*
|
|
157
|
-
* Keep the host service authoritative. Only when an Android runtime reaches an
|
|
158
|
-
* EACCES/EPERM boundary do Vision Router's own calls fall back to a byte- and
|
|
159
|
-
* count-bounded, process-local content-addressed attachment. The paired
|
|
160
|
-
* readImage() wrapper makes the direct vision-http path and plugin-side bridge
|
|
161
|
-
* code consume those bytes normally. Once upstream accepts the save, this path
|
|
162
|
-
* is never used.
|
|
163
|
-
*/
|
|
164
|
-
export function installAndroidAttachmentCompat(ctx, logger, options = {}) {
|
|
165
|
-
if (!ctx || typeof ctx !== 'object' || !isAndroidTermuxRuntime(options.runtime)) return ctx
|
|
166
|
-
const cached = wrappedContexts.get(ctx)
|
|
167
|
-
if (cached) return cached
|
|
168
|
-
|
|
169
|
-
let attachments
|
|
170
|
-
try {
|
|
171
|
-
attachments = typeof ctx.get === 'function' ? ctx.get('attachments') : undefined
|
|
172
|
-
} catch {
|
|
173
|
-
return ctx
|
|
174
|
-
}
|
|
175
|
-
if (!attachments || typeof attachments.saveImage !== 'function' || typeof attachments.readImage !== 'function') {
|
|
176
|
-
return ctx
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const maxTransientBytes = positiveInteger(
|
|
180
|
-
options.maxTransientBytes,
|
|
181
|
-
DEFAULT_ANDROID_TRANSIENT_LIMITS.maxBytes,
|
|
182
|
-
)
|
|
183
|
-
const maxTransientAttachments = positiveInteger(
|
|
184
|
-
options.maxTransientAttachments,
|
|
185
|
-
DEFAULT_ANDROID_TRANSIENT_LIMITS.maxEntries,
|
|
186
|
-
)
|
|
187
|
-
const maxTransientSingleBytes = Math.min(
|
|
188
|
-
maxTransientBytes,
|
|
189
|
-
positiveInteger(options.maxTransientSingleBytes, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxSingleBytes),
|
|
190
|
-
)
|
|
191
|
-
const transient = createTransientAttachmentCache({
|
|
192
|
-
maxEntries: maxTransientAttachments,
|
|
193
|
-
maxBytes: maxTransientBytes,
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
const wrappedAttachments = new Proxy(attachments, {
|
|
197
|
-
get(target, property) {
|
|
198
|
-
if (property === 'saveImage') {
|
|
199
|
-
return async (input) => {
|
|
200
|
-
try {
|
|
201
|
-
return await target.saveImage(input)
|
|
202
|
-
} catch (error) {
|
|
203
|
-
if (!isPermissionBoundaryError(error)) throw error
|
|
204
|
-
const stored = await transientStoredImage(input, options.probeImage, maxTransientSingleBytes)
|
|
205
|
-
if (!transient.set(stored)) {
|
|
206
|
-
throw transientTooLargeError(stored.data.byteLength, maxTransientBytes)
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
logger?.warn?.(
|
|
210
|
-
'vision-router: Android/Termux attachment save hit an inaccessible ancestor; using a bounded process-local image ref until DSH attachment-local is fixed upstream',
|
|
211
|
-
)
|
|
212
|
-
} catch {
|
|
213
|
-
/* diagnostics must never block the fallback */
|
|
214
|
-
}
|
|
215
|
-
return stored.ref
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (property === 'readImage') {
|
|
220
|
-
return async (ref, signal) => {
|
|
221
|
-
signal?.throwIfAborted?.()
|
|
222
|
-
const id = String(ref?.attachmentId ?? '')
|
|
223
|
-
const stored = transient.get(id)
|
|
224
|
-
if (stored !== undefined) return stored
|
|
225
|
-
return target.readImage(ref, signal)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const value = Reflect.get(target, property, target)
|
|
229
|
-
return typeof value === 'function' ? value.bind(target) : value
|
|
230
|
-
},
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
const wrapped = new Proxy(ctx, {
|
|
234
|
-
get(target, property) {
|
|
235
|
-
if (property === 'get') {
|
|
236
|
-
return (name) => (name === 'attachments' ? wrappedAttachments : target.get(name))
|
|
237
|
-
}
|
|
238
|
-
const value = Reflect.get(target, property, target)
|
|
239
|
-
return typeof value === 'function' ? value.bind(target) : value
|
|
240
|
-
},
|
|
241
|
-
})
|
|
242
|
-
wrappedContexts.set(ctx, wrapped)
|
|
243
|
-
return wrapped
|
|
244
|
-
}
|