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.
Files changed (49) hide show
  1. package/README.md +7 -7
  2. package/README.zh.md +7 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/compat-inventory.md +26 -34
  8. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  9. package/docs/architecture/dsh-support-window.md +27 -21
  10. package/docs/architecture/final-closure-audit.md +2 -0
  11. package/docs/releases/v2.3.0.md +53 -0
  12. package/entry.js +8 -56
  13. package/index.js +161 -691
  14. package/lib/catalog-corrections.js +1 -2
  15. package/lib/client-maintenance-actions.js +145 -0
  16. package/lib/client-presentation-boundary-main.js +75 -17
  17. package/lib/client-presentation-boundary.js +53 -28
  18. package/lib/client.js +246 -144
  19. package/lib/core-apply-lifecycle.js +47 -0
  20. package/lib/core-primitives.js +88 -135
  21. package/lib/desktop-screenshot-tool.js +262 -0
  22. package/lib/dsh-contract-compat.js +11 -26
  23. package/lib/dsh-support-window.js +3 -3
  24. package/lib/http-compat.js +1 -2
  25. package/lib/local-remote-settings-permission.js +33 -7
  26. package/lib/maintenance-routes.js +136 -0
  27. package/lib/public-config.js +57 -0
  28. package/lib/public-entry.js +2 -7
  29. package/lib/remote-settings-bridge.js +11 -0
  30. package/lib/remote-settings-risk-client-boundary.js +155 -0
  31. package/lib/remote-settings-risk-confirmation.js +17 -105
  32. package/lib/runtime-composition.js +105 -30
  33. package/lib/runtime-i18n-boundary.js +10 -11
  34. package/lib/session-message-source-compat.js +7 -3
  35. package/lib/session-vision-index.js +154 -9
  36. package/lib/settings-client-loader-lifecycle.js +129 -0
  37. package/lib/sharp-runtime.js +0 -7
  38. package/lib/structured-flow-hardening.js +3 -2
  39. package/lib/vision-backend-runtime-policy.js +3 -2
  40. package/lib/vision-capability-benchmark-service.js +1 -0
  41. package/lib/vision-diagnostics-routes.js +216 -0
  42. package/lib/vision-evidence-guidance.js +3 -1
  43. package/lib/vision-provider-transport.js +0 -29
  44. package/lib/vision-toggle-root-hardening.js +22 -5
  45. package/lib/vision-tool-runtime-boundary.js +5 -1
  46. package/lib/web/remote-settings-client.js +2 -2
  47. package/package.json +23 -13
  48. package/lib/android-attachment-compat.js +0 -244
  49. 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.2.8",
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": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.2 || >=0.2.0-rc.1 <0.3.0-0",
58
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.2 || >=0.2.0-rc.1 <0.3.0-0",
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": { "optional": true },
63
- "@deepseek-ai/dsh-llm-deepseek": { "optional": true },
64
- "sharp": { "optional": true }
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": "^0.1.0-rc.6",
68
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
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/rc6-real-settings-persistence.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/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/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-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/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",
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/rc6-rc7-compat.test.js tests/rc6-real-settings-persistence.test.js tests/android-attachment-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",
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/rc6-rc7-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",
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": { "patch": "./cordis.patch.yml" }
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
- }