@vellumai/assistant 0.6.3 → 0.6.4

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 (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -14,12 +14,38 @@ mock.module("../../../util/logger.js", () => ({
14
14
  }));
15
15
 
16
16
  import { getWorkspaceRoutesDir } from "../../../util/platform.js";
17
+ import { AssistantEventHub } from "../../assistant-event-hub.js";
18
+ import type { UserRouteContext } from "../user-route-dispatcher.js";
17
19
  import { UserRouteDispatcher } from "../user-route-dispatcher.js";
18
20
 
19
21
  // ---------------------------------------------------------------------------
20
22
  // Helpers
21
23
  // ---------------------------------------------------------------------------
22
24
 
25
+ /** Build a minimal UserRouteContext for tests. */
26
+ function makeContext(
27
+ overrides?: Partial<UserRouteContext>,
28
+ ): UserRouteContext {
29
+ return {
30
+ assistantEventHub: new AssistantEventHub(),
31
+ assistantId: "test-assistant",
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ /** Create a dispatcher with a stub context and optional overrides. */
37
+ function makeDispatcher(
38
+ options?: Partial<{
39
+ handlerTimeoutMs: number;
40
+ context: UserRouteContext;
41
+ }>,
42
+ ): UserRouteDispatcher {
43
+ return new UserRouteDispatcher({
44
+ context: options?.context ?? makeContext(),
45
+ ...options,
46
+ });
47
+ }
48
+
23
49
  function makeRequest(
24
50
  method: string,
25
51
  path = "http://localhost/v1/x/test",
@@ -60,7 +86,7 @@ afterEach(() => {
60
86
 
61
87
  describe("path traversal", () => {
62
88
  test("rejects paths containing '..'", async () => {
63
- const dispatcher = new UserRouteDispatcher();
89
+ const dispatcher = makeDispatcher();
64
90
  const res = await dispatcher.dispatch("../etc/passwd", makeRequest("GET"));
65
91
  expect(res.status).toBe(400);
66
92
  const body = await readErrorBody(res);
@@ -69,7 +95,7 @@ describe("path traversal", () => {
69
95
  });
70
96
 
71
97
  test("rejects embedded '..' segments", async () => {
72
- const dispatcher = new UserRouteDispatcher();
98
+ const dispatcher = makeDispatcher();
73
99
  const res = await dispatcher.dispatch(
74
100
  "foo/../../etc/passwd",
75
101
  makeRequest("GET"),
@@ -84,7 +110,7 @@ describe("path traversal", () => {
84
110
 
85
111
  describe("missing handler", () => {
86
112
  test("returns 404 when no handler file exists", async () => {
87
- const dispatcher = new UserRouteDispatcher();
113
+ const dispatcher = makeDispatcher();
88
114
  const res = await dispatcher.dispatch("nonexistent", makeRequest("GET"));
89
115
  expect(res.status).toBe(404);
90
116
  const body = await readErrorBody(res);
@@ -106,7 +132,7 @@ describe("successful dispatch", () => {
106
132
  }`,
107
133
  );
108
134
 
109
- const dispatcher = new UserRouteDispatcher();
135
+ const dispatcher = makeDispatcher();
110
136
  const res = await dispatcher.dispatch("hello", makeRequest("GET"));
111
137
  expect(res.status).toBe(200);
112
138
  const body = await res.json();
@@ -121,7 +147,7 @@ describe("successful dispatch", () => {
121
147
  }`,
122
148
  );
123
149
 
124
- const dispatcher = new UserRouteDispatcher();
150
+ const dispatcher = makeDispatcher();
125
151
  const res = await dispatcher.dispatch("submit", makeRequest("POST"));
126
152
  expect(res.status).toBe(201);
127
153
  const body = await res.json();
@@ -136,7 +162,7 @@ describe("successful dispatch", () => {
136
162
  }`,
137
163
  );
138
164
 
139
- const dispatcher = new UserRouteDispatcher();
165
+ const dispatcher = makeDispatcher();
140
166
  const res = await dispatcher.dispatch("legacy", makeRequest("GET"));
141
167
  expect(res.status).toBe(200);
142
168
  const body = await res.json();
@@ -157,7 +183,7 @@ describe("index file convention", () => {
157
183
  }`,
158
184
  );
159
185
 
160
- const dispatcher = new UserRouteDispatcher();
186
+ const dispatcher = makeDispatcher();
161
187
  const res = await dispatcher.dispatch("my-app", makeRequest("GET"));
162
188
  expect(res.status).toBe(200);
163
189
  const body = await res.json();
@@ -172,7 +198,7 @@ describe("index file convention", () => {
172
198
  }`,
173
199
  );
174
200
 
175
- const dispatcher = new UserRouteDispatcher();
201
+ const dispatcher = makeDispatcher();
176
202
  const res = await dispatcher.dispatch("fallback-app", makeRequest("GET"));
177
203
  expect(res.status).toBe(200);
178
204
  const body = await res.json();
@@ -193,7 +219,7 @@ describe("index file convention", () => {
193
219
  }`,
194
220
  );
195
221
 
196
- const dispatcher = new UserRouteDispatcher();
222
+ const dispatcher = makeDispatcher();
197
223
  const res = await dispatcher.dispatch("dual", makeRequest("GET"));
198
224
  expect(res.status).toBe(200);
199
225
  const body = await res.json();
@@ -214,7 +240,7 @@ describe("method not allowed", () => {
214
240
  }`,
215
241
  );
216
242
 
217
- const dispatcher = new UserRouteDispatcher();
243
+ const dispatcher = makeDispatcher();
218
244
  const res = await dispatcher.dispatch("get-only", makeRequest("POST"));
219
245
  expect(res.status).toBe(405);
220
246
  expect(res.headers.get("Allow")).toBe("GET");
@@ -228,7 +254,7 @@ describe("method not allowed", () => {
228
254
  export function DELETE(request) { return new Response("ok"); }`,
229
255
  );
230
256
 
231
- const dispatcher = new UserRouteDispatcher();
257
+ const dispatcher = makeDispatcher();
232
258
  const res = await dispatcher.dispatch("multi", makeRequest("PUT"));
233
259
  expect(res.status).toBe(405);
234
260
  const allow = res.headers.get("Allow");
@@ -252,7 +278,7 @@ describe("handler timeout", () => {
252
278
  );
253
279
 
254
280
  // Use a very short timeout for testing
255
- const dispatcher = new UserRouteDispatcher({ handlerTimeoutMs: 50 });
281
+ const dispatcher = makeDispatcher({ handlerTimeoutMs: 50 });
256
282
  const res = await dispatcher.dispatch("slow", makeRequest("GET"));
257
283
  expect(res.status).toBe(504);
258
284
  const body = await readErrorBody(res);
@@ -274,7 +300,7 @@ describe("handler errors", () => {
274
300
  }`,
275
301
  );
276
302
 
277
- const dispatcher = new UserRouteDispatcher();
303
+ const dispatcher = makeDispatcher();
278
304
  const res = await dispatcher.dispatch("throws", makeRequest("GET"));
279
305
  expect(res.status).toBe(500);
280
306
  const body = await readErrorBody(res);
@@ -290,7 +316,7 @@ describe("handler errors", () => {
290
316
  }`,
291
317
  );
292
318
 
293
- const dispatcher = new UserRouteDispatcher();
319
+ const dispatcher = makeDispatcher();
294
320
  const res = await dispatcher.dispatch("rejects", makeRequest("GET"));
295
321
  expect(res.status).toBe(500);
296
322
  const body = await readErrorBody(res);
@@ -311,7 +337,7 @@ describe("mtime cache", () => {
311
337
  }`,
312
338
  );
313
339
 
314
- const dispatcher = new UserRouteDispatcher();
340
+ const dispatcher = makeDispatcher();
315
341
 
316
342
  // First request — version 1
317
343
  const res1 = await dispatcher.dispatch("mutable", makeRequest("GET"));
@@ -349,7 +375,7 @@ describe("subdirectory routing", () => {
349
375
  }`,
350
376
  );
351
377
 
352
- const dispatcher = new UserRouteDispatcher();
378
+ const dispatcher = makeDispatcher();
353
379
  const res = await dispatcher.dispatch("api/v1/status", makeRequest("GET"));
354
380
  expect(res.status).toBe(200);
355
381
  const body = await res.json();
@@ -371,8 +397,113 @@ describe("description metadata", () => {
371
397
  }`,
372
398
  );
373
399
 
374
- const dispatcher = new UserRouteDispatcher();
400
+ const dispatcher = makeDispatcher();
375
401
  const res = await dispatcher.dispatch("with-meta", makeRequest("GET"));
376
402
  expect(res.status).toBe(200);
377
403
  });
378
404
  });
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Context injection
408
+ // ---------------------------------------------------------------------------
409
+
410
+ describe("context injection", () => {
411
+ test("passes UserRouteContext as second argument to handler", async () => {
412
+ writeHandler(
413
+ "ctx-echo.ts",
414
+ `export function GET(request, context) {
415
+ return Response.json({
416
+ hasHub: typeof context.assistantEventHub?.publish === "function",
417
+ assistantId: context.assistantId,
418
+ });
419
+ }`,
420
+ );
421
+
422
+ const ctx = makeContext({ assistantId: "custom-id" });
423
+ const dispatcher = makeDispatcher({ context: ctx });
424
+ const res = await dispatcher.dispatch("ctx-echo", makeRequest("GET"));
425
+ expect(res.status).toBe(200);
426
+ const body = await res.json();
427
+ expect(body.hasHub).toBe(true);
428
+ expect(body.assistantId).toBe("custom-id");
429
+ });
430
+
431
+ test("handler can publish events through injected hub", async () => {
432
+ writeHandler(
433
+ "ctx-publish.ts",
434
+ `export async function POST(request, context) {
435
+ const body = await request.json();
436
+ await context.assistantEventHub.publish({
437
+ id: "test-event-1",
438
+ assistantId: context.assistantId,
439
+ conversationId: body.conversationId,
440
+ emittedAt: new Date().toISOString(),
441
+ message: { type: "open_conversation", conversationId: body.conversationId },
442
+ });
443
+ return Response.json({ published: true });
444
+ }`,
445
+ );
446
+
447
+ const hub = new AssistantEventHub();
448
+ const received: unknown[] = [];
449
+ hub.subscribe(
450
+ { assistantId: "test-assistant" },
451
+ (event) => { received.push(event); },
452
+ );
453
+
454
+ const ctx = makeContext({ assistantEventHub: hub });
455
+ const dispatcher = makeDispatcher({ context: ctx });
456
+ const req = new Request("http://localhost/v1/x/ctx-publish", {
457
+ method: "POST",
458
+ headers: { "Content-Type": "application/json" },
459
+ body: JSON.stringify({ conversationId: "conv-123" }),
460
+ });
461
+ const res = await dispatcher.dispatch("ctx-publish", req);
462
+ expect(res.status).toBe(200);
463
+ const body = await res.json();
464
+ expect(body.published).toBe(true);
465
+ expect(received).toHaveLength(1);
466
+ expect((received[0] as { conversationId: string }).conversationId).toBe(
467
+ "conv-123",
468
+ );
469
+ });
470
+
471
+ test("legacy handlers that ignore context still work", async () => {
472
+ writeHandler(
473
+ "no-ctx.ts",
474
+ `export function GET(request) {
475
+ return Response.json({ legacy: true });
476
+ }`,
477
+ );
478
+
479
+ const dispatcher = makeDispatcher();
480
+ const res = await dispatcher.dispatch("no-ctx", makeRequest("GET"));
481
+ expect(res.status).toBe(200);
482
+ const body = await res.json();
483
+ expect(body.legacy).toBe(true);
484
+ });
485
+
486
+ test("context is frozen — mutations throw in strict mode", async () => {
487
+ writeHandler(
488
+ "ctx-mutate.ts",
489
+ `export function GET(request, context) {
490
+ let threw = false;
491
+ try {
492
+ context.assistantId = "hacked";
493
+ } catch {
494
+ threw = true;
495
+ }
496
+ return Response.json({ threw, assistantId: context.assistantId });
497
+ }`,
498
+ );
499
+
500
+ const ctx = makeContext({ assistantId: "original" });
501
+ const dispatcher = makeDispatcher({ context: ctx });
502
+ const res = await dispatcher.dispatch("ctx-mutate", makeRequest("GET"));
503
+ expect(res.status).toBe(200);
504
+ const body = await res.json();
505
+ // Object.freeze makes property assignment throw in strict mode (ESM)
506
+ // and silently fail in sloppy mode — either way the value is unchanged.
507
+ expect(body.assistantId).toBe("original");
508
+ });
509
+ });
@@ -23,6 +23,7 @@ import { z } from "zod";
23
23
  import { packageApp } from "../../bundler/app-bundler.js";
24
24
  import { compileApp } from "../../bundler/app-compiler.js";
25
25
  import { scanBundle } from "../../bundler/bundle-scanner.js";
26
+ import type { SignatureJson } from "../../bundler/bundle-signer.js";
26
27
  import { verifyBundleSignature } from "../../bundler/signature-verifier.js";
27
28
  import { compareSemver } from "../../daemon/handlers/shared.js";
28
29
  import { defaultGallery } from "../../gallery/default-gallery.js";
@@ -39,11 +40,11 @@ import {
39
40
  getApp,
40
41
  getAppDirPath,
41
42
  getAppPreview,
42
- inlineDistAssets,
43
43
  isMultifileApp,
44
44
  listApps,
45
45
  queryAppRecords,
46
46
  resolveAppDir,
47
+ resolveEffectiveAppHtml,
47
48
  updateApp,
48
49
  updateAppRecord,
49
50
  } from "../../memory/app-store.js";
@@ -583,16 +584,15 @@ export function appManagementRouteDefinitions(): RouteDefinition[] {
583
584
  return httpError("BAD_REQUEST", "payload is not valid JSON", 400);
584
585
  }
585
586
 
586
- const signatureJson: import("../../bundler/bundle-signer.js").SignatureJson =
587
- {
588
- algorithm: "ed25519",
589
- signer: {
590
- key_id: body.keyId,
591
- display_name: "HTTP Signer",
592
- },
593
- content_hashes: contentHashes,
594
- signature: body.signature,
595
- };
587
+ const signatureJson: SignatureJson = {
588
+ algorithm: "ed25519",
589
+ signer: {
590
+ key_id: body.keyId,
591
+ display_name: "HTTP Signer",
592
+ },
593
+ content_hashes: contentHashes,
594
+ signature: body.signature,
595
+ };
596
596
  return Response.json({ signed: true, signatureJson });
597
597
  }
598
598
 
@@ -738,8 +738,6 @@ export function appManagementRouteDefinitions(): RouteDefinition[] {
738
738
  return httpError("NOT_FOUND", `App not found: ${appId}`, 404);
739
739
  }
740
740
 
741
- let html = app.htmlDefinition;
742
-
743
741
  if (isMultifileApp(app)) {
744
742
  const appDir = getAppDirPath(appId);
745
743
  const distIndex = join(appDir, "dist", "index.html");
@@ -752,12 +750,8 @@ export function appManagementRouteDefinitions(): RouteDefinition[] {
752
750
  );
753
751
  }
754
752
  }
755
- if (existsSync(distIndex)) {
756
- html = inlineDistAssets(appDir, readFileSync(distIndex, "utf-8"));
757
- } else {
758
- html = `<p>App compilation failed. Edit a source file to trigger a rebuild.</p>`;
759
- }
760
753
  }
754
+ const html = resolveEffectiveAppHtml(app);
761
755
 
762
756
  const { dirName } = resolveAppDir(app.id);
763
757
  return Response.json({
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  mkdirSync,
3
3
  mkdtempSync,
4
+ realpathSync,
4
5
  rmSync,
5
6
  symlinkSync,
6
7
  writeFileSync,
@@ -9,10 +10,15 @@ import { tmpdir } from "node:os";
9
10
  import { join } from "node:path";
10
11
  import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
11
12
 
12
- const testWorkspaceDir = mkdtempSync(
13
- join(tmpdir(), "attachment-routes-workspace-"),
13
+ // realpathSync resolves the macOS /var → /private/var symlink so the paths
14
+ // match what resolveAllowedFileBackedAttachmentPath returns (it canonicalizes
15
+ // via realpathSync internally).
16
+ const testWorkspaceDir = realpathSync(
17
+ mkdtempSync(join(tmpdir(), "attachment-routes-workspace-")),
18
+ );
19
+ const testHomeDir = realpathSync(
20
+ mkdtempSync(join(tmpdir(), "attachment-routes-home-")),
14
21
  );
15
- const testHomeDir = mkdtempSync(join(tmpdir(), "attachment-routes-home-"));
16
22
 
17
23
  const attachmentsDir = join(testWorkspaceDir, "data", "attachments");
18
24
  const conversationsDir = join(testWorkspaceDir, "conversations");
@@ -1,7 +1,13 @@
1
1
  /**
2
2
  * Route handlers for attachment upload, download, and deletion.
3
3
  */
4
- import { existsSync, realpathSync, statSync } from "node:fs";
4
+ import {
5
+ copyFileSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ realpathSync,
9
+ statSync,
10
+ } from "node:fs";
5
11
  import { join, resolve, sep } from "node:path";
6
12
 
7
13
  import { z } from "zod";
@@ -19,6 +25,9 @@ import type { RouteDefinition } from "../http-router.js";
19
25
  /** 150 MB — base64-encoded 100 MB attachment ≈ 134 MB plus JSON wrapper overhead. */
20
26
  const MAX_UPLOAD_BODY_BYTES = 150 * 1024 * 1024;
21
27
 
28
+ /** 100 MB — maximum file size for file-backed uploads (matches client memorySafetyLimit). */
29
+ const MAX_FILE_BACKED_UPLOAD_BYTES = 100 * 1024 * 1024;
30
+
22
31
  function resolveCanonicalPath(filePath: string): string {
23
32
  try {
24
33
  return realpathSync(filePath);
@@ -88,7 +97,160 @@ export function resolveAllowedFileBackedAttachmentPath(
88
97
  return null;
89
98
  }
90
99
 
91
- export async function handleUploadAttachment(req: Request): Promise<Response> {
100
+ /** 100 MB — maximum file size for binary uploads (multipart / octet-stream). */
101
+ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
102
+
103
+ /**
104
+ * Build the standard JSON success response for an uploaded attachment.
105
+ */
106
+ function attachmentResponse(
107
+ attachment: attachmentsStore.StoredAttachment,
108
+ ): Response {
109
+ return Response.json({
110
+ id: attachment.id,
111
+ original_filename: attachment.originalFilename,
112
+ mime_type: attachment.mimeType,
113
+ size_bytes: attachment.sizeBytes,
114
+ kind: attachment.kind,
115
+ });
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Content-Type dispatched upload handlers
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /**
123
+ * Handle multipart/form-data upload.
124
+ * Expects: "file" (Blob), "filename" (string), "mimeType" (string).
125
+ */
126
+ async function handleMultipartUpload(req: Request): Promise<Response> {
127
+ // Pre-check Content-Length before parsing to reject oversized requests
128
+ // without buffering the full multipart body into memory. Binary uploads
129
+ // have no base64 overhead, so use the raw file size limit directly.
130
+ const contentLength = req.headers.get("content-length");
131
+ if (contentLength && Number(contentLength) > MAX_UPLOAD_BYTES) {
132
+ return httpError(
133
+ "BAD_REQUEST",
134
+ `File too large (limit: ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB)`,
135
+ 413,
136
+ );
137
+ }
138
+
139
+ let formData: FormData;
140
+ try {
141
+ formData = await req.formData();
142
+ } catch {
143
+ return httpError("BAD_REQUEST", "Invalid multipart form data", 400);
144
+ }
145
+
146
+ const file = formData.get("file");
147
+ if (!file || !(file instanceof Blob)) {
148
+ return httpError(
149
+ "BAD_REQUEST",
150
+ 'Multipart upload requires a "file" field',
151
+ 400,
152
+ );
153
+ }
154
+
155
+ const filename = formData.get("filename");
156
+ if (!filename || typeof filename !== "string") {
157
+ return httpError("BAD_REQUEST", "filename field is required", 400);
158
+ }
159
+
160
+ const mimeType = formData.get("mimeType");
161
+ if (!mimeType || typeof mimeType !== "string") {
162
+ return httpError("BAD_REQUEST", "mimeType field is required", 400);
163
+ }
164
+
165
+ // Check file part size against the raw file limit (Content-Length may be
166
+ // absent or inaccurate, so this is the authoritative check).
167
+ if (file.size > MAX_UPLOAD_BYTES) {
168
+ return httpError(
169
+ "BAD_REQUEST",
170
+ `File is ${Math.round(file.size / (1024 * 1024))} MB which exceeds the ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB upload limit`,
171
+ 413,
172
+ );
173
+ }
174
+
175
+ const validation = validateAttachmentUpload(filename, mimeType);
176
+ if (!validation.ok) {
177
+ return httpError("UNPROCESSABLE_ENTITY", validation.error, 415);
178
+ }
179
+
180
+ const bytes = new Uint8Array(await file.arrayBuffer());
181
+
182
+ const attachment = attachmentsStore.uploadAttachmentFromBytes(
183
+ filename,
184
+ mimeType,
185
+ bytes,
186
+ );
187
+ return attachmentResponse(attachment);
188
+ }
189
+
190
+ /**
191
+ * Handle application/octet-stream upload.
192
+ * filename and mimeType come from URL query params.
193
+ */
194
+ async function handleOctetStreamUpload(req: Request): Promise<Response> {
195
+ // Pre-check Content-Length before buffering to reject oversized requests
196
+ // without reading the full body into memory.
197
+ const contentLength = req.headers.get("content-length");
198
+ if (contentLength && Number(contentLength) > MAX_UPLOAD_BYTES) {
199
+ return httpError(
200
+ "BAD_REQUEST",
201
+ `File too large (limit: ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB)`,
202
+ 413,
203
+ );
204
+ }
205
+
206
+ const url = new URL(req.url);
207
+ const filename = url.searchParams.get("filename");
208
+ if (!filename || typeof filename !== "string") {
209
+ return httpError(
210
+ "BAD_REQUEST",
211
+ "filename query parameter is required",
212
+ 400,
213
+ );
214
+ }
215
+
216
+ const mimeType = url.searchParams.get("mimeType");
217
+ if (!mimeType || typeof mimeType !== "string") {
218
+ return httpError(
219
+ "BAD_REQUEST",
220
+ "mimeType query parameter is required",
221
+ 400,
222
+ );
223
+ }
224
+
225
+ const rawBody = await req.arrayBuffer();
226
+ // Post-read check (Content-Length may be absent or inaccurate).
227
+ if (rawBody.byteLength > MAX_UPLOAD_BYTES) {
228
+ return httpError(
229
+ "BAD_REQUEST",
230
+ `File is ${Math.round(rawBody.byteLength / (1024 * 1024))} MB which exceeds the ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB upload limit`,
231
+ 413,
232
+ );
233
+ }
234
+
235
+ const validation = validateAttachmentUpload(filename, mimeType);
236
+ if (!validation.ok) {
237
+ return httpError("UNPROCESSABLE_ENTITY", validation.error, 415);
238
+ }
239
+
240
+ const bytes = new Uint8Array(rawBody);
241
+
242
+ const attachment = attachmentsStore.uploadAttachmentFromBytes(
243
+ filename,
244
+ mimeType,
245
+ bytes,
246
+ );
247
+ return attachmentResponse(attachment);
248
+ }
249
+
250
+ /**
251
+ * Handle application/json upload (existing behaviour — base64 or file-path).
252
+ */
253
+ async function handleJsonUpload(req: Request): Promise<Response> {
92
254
  const rawBody = await req.arrayBuffer();
93
255
  if (rawBody.byteLength > MAX_UPLOAD_BODY_BYTES) {
94
256
  return httpError(
@@ -124,17 +286,45 @@ export async function handleUploadAttachment(req: Request): Promise<Response> {
124
286
 
125
287
  // File-backed upload: when filePath is provided and data is empty/missing,
126
288
  // register the attachment by path reference instead of requiring base64 data.
127
- // This supports retry of file-backed attachments (e.g. recordings) where the
128
- // client no longer holds the inline data but the file still exists on disk.
289
+ // This supports:
290
+ // 1. Desktop client file-picker uploads the file is copied into the
291
+ // workspace attachments directory so it passes the directory allowlist.
292
+ // 2. Retry of file-backed attachments (e.g. recordings) where the client
293
+ // no longer holds the inline data but the file still exists on disk.
129
294
  if (filePath && typeof filePath === "string" && (!data || data === "")) {
130
- const resolvedPath = resolveAllowedFileBackedAttachmentPath(filePath);
295
+ let resolvedPath = resolveAllowedFileBackedAttachmentPath(filePath);
296
+
297
+ // If the file isn't in an allowed directory, copy it into the workspace
298
+ // attachments directory. This handles desktop client file-picker uploads
299
+ // where the source file lives in an arbitrary user directory (e.g.
300
+ // ~/Desktop, ~/Downloads). The copy lands in the allowlisted workspace
301
+ // directory, preserving the security model.
131
302
  if (!resolvedPath) {
132
- return httpError(
133
- "BAD_REQUEST",
134
- "filePath is outside allowed upload directories",
135
- 400,
303
+ const canonicalSource = resolveCanonicalPath(filePath);
304
+ if (!existsSync(canonicalSource)) {
305
+ return httpError("BAD_REQUEST", "filePath does not exist on disk", 400);
306
+ }
307
+ const sourceSize = statSync(canonicalSource).size;
308
+ if (sourceSize > MAX_FILE_BACKED_UPLOAD_BYTES) {
309
+ const sizeMB = Math.round(sourceSize / (1024 * 1024));
310
+ return httpError(
311
+ "BAD_REQUEST",
312
+ `File is ${sizeMB} MB which exceeds the ${MAX_FILE_BACKED_UPLOAD_BYTES / (1024 * 1024)} MB upload limit`,
313
+ 413,
314
+ );
315
+ }
316
+ const workspaceAttachmentsDir = join(
317
+ getWorkspaceDir(),
318
+ "data",
319
+ "attachments",
136
320
  );
321
+ mkdirSync(workspaceAttachmentsDir, { recursive: true });
322
+ const destFilename = `${Date.now()}-${filename.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
323
+ const destPath = join(workspaceAttachmentsDir, destFilename);
324
+ copyFileSync(canonicalSource, destPath);
325
+ resolvedPath = resolveCanonicalPath(destPath);
137
326
  }
327
+
138
328
  if (!existsSync(resolvedPath)) {
139
329
  return httpError("BAD_REQUEST", "filePath does not exist on disk", 400);
140
330
  }
@@ -168,13 +358,22 @@ export async function handleUploadAttachment(req: Request): Promise<Response> {
168
358
  }
169
359
  }
170
360
 
171
- return Response.json({
172
- id: attachment.id,
173
- original_filename: attachment.originalFilename,
174
- mime_type: attachment.mimeType,
175
- size_bytes: attachment.sizeBytes,
176
- kind: attachment.kind,
177
- });
361
+ return attachmentResponse(attachment);
362
+ }
363
+
364
+ export async function handleUploadAttachment(req: Request): Promise<Response> {
365
+ const contentType = req.headers.get("content-type") ?? "";
366
+
367
+ if (contentType.includes("multipart/form-data")) {
368
+ return handleMultipartUpload(req);
369
+ }
370
+
371
+ if (contentType.includes("application/octet-stream")) {
372
+ return handleOctetStreamUpload(req);
373
+ }
374
+
375
+ // Default: JSON+base64 (existing behaviour)
376
+ return handleJsonUpload(req);
178
377
  }
179
378
 
180
379
  export async function handleDeleteAttachment(req: Request): Promise<Response> {
@@ -355,7 +554,7 @@ export function attachmentRouteDefinitions(): RouteDefinition[] {
355
554
  method: "POST",
356
555
  summary: "Upload attachment",
357
556
  description:
358
- "Upload an attachment as base64 data or file path reference.",
557
+ "Upload an attachment. Supports application/json (base64 data or file path reference), multipart/form-data (file + filename + mimeType fields), and application/octet-stream (raw bytes with filename and mimeType query params).",
359
558
  tags: ["attachments"],
360
559
  requestBody: z.object({
361
560
  filename: z.string(),