@vellumai/assistant 0.5.1 → 0.5.2

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 (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -1,523 +0,0 @@
1
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
-
6
- import { RiskLevel } from "../permissions/types.js";
7
-
8
- const testDir = mkdtempSync(join(tmpdir(), "asset-materialize-test-"));
9
- const sandboxDir = join(testDir, "sandbox");
10
-
11
- mock.module("../util/platform.js", () => ({
12
- getDataDir: () => testDir,
13
- isMacOS: () => process.platform === "darwin",
14
- isLinux: () => process.platform === "linux",
15
- isWindows: () => process.platform === "win32",
16
- getPidPath: () => join(testDir, "test.pid"),
17
- getDbPath: () => join(testDir, "test.db"),
18
- getLogPath: () => join(testDir, "test.log"),
19
- ensureDataDir: () => {},
20
- getRootDir: () => testDir,
21
- }));
22
-
23
- mock.module("../util/logger.js", () => ({
24
- getLogger: () =>
25
- new Proxy({} as Record<string, unknown>, {
26
- get: () => () => {},
27
- }),
28
- }));
29
-
30
- mock.module("../config/loader.js", () => ({
31
- getConfig: () => ({
32
- ui: {},
33
-
34
- model: "test",
35
- provider: "test",
36
- memory: { enabled: false },
37
- rateLimit: { maxRequestsPerMinute: 0 },
38
- }),
39
- }));
40
-
41
- import {
42
- linkAttachmentToMessage,
43
- uploadAttachment,
44
- } from "../memory/attachments-store.js";
45
- import { addMessage, createConversation } from "../memory/conversation-crud.js";
46
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
47
- import { assetMaterializeTool } from "../tools/assets/materialize.js";
48
- import type { ToolContext } from "../tools/types.js";
49
-
50
- initializeDb();
51
-
52
- // Ensure the sandbox directory exists
53
- import { mkdirSync } from "node:fs";
54
- mkdirSync(sandboxDir, { recursive: true });
55
-
56
- afterAll(() => {
57
- resetDb();
58
- try {
59
- rmSync(testDir, { recursive: true });
60
- } catch {
61
- /* best effort */
62
- }
63
- });
64
-
65
- function resetTables() {
66
- const db = getDb();
67
- db.run("DELETE FROM message_attachments");
68
- db.run("DELETE FROM attachments");
69
- db.run("DELETE FROM messages");
70
- db.run("DELETE FROM conversations");
71
- }
72
-
73
- const dummyContext: ToolContext = {
74
- workingDir: sandboxDir,
75
- conversationId: "conv-test",
76
- trustClass: "guardian",
77
- };
78
-
79
- // ---------------------------------------------------------------------------
80
- // Input validation
81
- // ---------------------------------------------------------------------------
82
-
83
- describe("AssetMaterializeTool input validation", () => {
84
- test("returns error when attachment_id is missing", async () => {
85
- const result = await assetMaterializeTool.execute(
86
- { destination_path: "output.png" },
87
- dummyContext,
88
- );
89
- expect(result.isError).toBe(true);
90
- expect(result.content).toContain("attachment_id is required");
91
- });
92
-
93
- test("returns error when destination_path is missing", async () => {
94
- const result = await assetMaterializeTool.execute(
95
- { attachment_id: "some-id" },
96
- dummyContext,
97
- );
98
- expect(result.isError).toBe(true);
99
- expect(result.content).toContain("destination_path is required");
100
- });
101
-
102
- test("returns error when both params are missing", async () => {
103
- const result = await assetMaterializeTool.execute({}, dummyContext);
104
- expect(result.isError).toBe(true);
105
- });
106
- });
107
-
108
- // ---------------------------------------------------------------------------
109
- // Sandbox path enforcement
110
- // ---------------------------------------------------------------------------
111
-
112
- describe("AssetMaterializeTool sandbox path enforcement", () => {
113
- beforeEach(resetTables);
114
-
115
- test("rejects path that escapes sandbox via ../", async () => {
116
- const stored = uploadAttachment("test.png", "image/png", "AAAA");
117
- const result = await assetMaterializeTool.execute(
118
- { attachment_id: stored.id, destination_path: "../../etc/evil.png" },
119
- dummyContext,
120
- );
121
- expect(result.isError).toBe(true);
122
- expect(result.content).toContain("outside");
123
- });
124
-
125
- test("rejects absolute path outside sandbox", async () => {
126
- const stored = uploadAttachment("test.png", "image/png", "AAAA");
127
- const result = await assetMaterializeTool.execute(
128
- {
129
- attachment_id: stored.id,
130
- destination_path: "/tmp/outside-sandbox/evil.png",
131
- },
132
- dummyContext,
133
- );
134
- expect(result.isError).toBe(true);
135
- expect(result.content).toContain("outside");
136
- });
137
-
138
- test("accepts relative path inside sandbox", async () => {
139
- const stored = uploadAttachment("test.png", "image/png", "AAAA");
140
- const result = await assetMaterializeTool.execute(
141
- { attachment_id: stored.id, destination_path: "output.png" },
142
- dummyContext,
143
- );
144
- expect(result.isError).toBe(false);
145
- expect(result.content).toContain("Materialized");
146
- });
147
-
148
- test("accepts nested path inside sandbox with auto-created subdirs", async () => {
149
- const stored = uploadAttachment("test.png", "image/png", "AAAA");
150
- const result = await assetMaterializeTool.execute(
151
- { attachment_id: stored.id, destination_path: "subdir/deep/output.png" },
152
- dummyContext,
153
- );
154
- expect(result.isError).toBe(false);
155
- expect(existsSync(join(sandboxDir, "subdir", "deep", "output.png"))).toBe(
156
- true,
157
- );
158
- });
159
- });
160
-
161
- // ---------------------------------------------------------------------------
162
- // Attachment lookup
163
- // ---------------------------------------------------------------------------
164
-
165
- describe("AssetMaterializeTool attachment lookup", () => {
166
- beforeEach(resetTables);
167
-
168
- test("returns error for non-existent attachment ID", async () => {
169
- const result = await assetMaterializeTool.execute(
170
- { attachment_id: "nonexistent-id", destination_path: "out.png" },
171
- dummyContext,
172
- );
173
- expect(result.isError).toBe(true);
174
- expect(result.content).toContain("not found");
175
- });
176
- });
177
-
178
- // ---------------------------------------------------------------------------
179
- // Successful materialization
180
- // ---------------------------------------------------------------------------
181
-
182
- describe("AssetMaterializeTool materialization", () => {
183
- beforeEach(resetTables);
184
-
185
- test("writes correct binary content to disk", async () => {
186
- // Create known content: "Hello, World!" in base64
187
- const originalContent = "Hello, World!";
188
- const base64Content = Buffer.from(originalContent).toString("base64");
189
-
190
- const stored = uploadAttachment("hello.txt", "text/plain", base64Content);
191
-
192
- const destPath = "materialized-hello.txt";
193
- const result = await assetMaterializeTool.execute(
194
- { attachment_id: stored.id, destination_path: destPath },
195
- dummyContext,
196
- );
197
-
198
- expect(result.isError).toBe(false);
199
- expect(result.content).toContain("Materialized");
200
- expect(result.content).toContain("hello.txt");
201
- expect(result.content).toContain("text/plain");
202
-
203
- const writtenContent = readFileSync(join(sandboxDir, destPath), "utf-8");
204
- expect(writtenContent).toBe(originalContent);
205
- });
206
-
207
- test("writes binary (image) content correctly", async () => {
208
- // Small valid PNG-like bytes encoded as base64
209
- const binaryBytes = Buffer.from([
210
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
211
- ]);
212
- const base64Content = binaryBytes.toString("base64");
213
-
214
- const stored = uploadAttachment("tiny.png", "image/png", base64Content);
215
-
216
- const result = await assetMaterializeTool.execute(
217
- { attachment_id: stored.id, destination_path: "images/tiny.png" },
218
- dummyContext,
219
- );
220
-
221
- expect(result.isError).toBe(false);
222
-
223
- const writtenBytes = readFileSync(join(sandboxDir, "images", "tiny.png"));
224
- expect(Buffer.compare(writtenBytes, binaryBytes)).toBe(0);
225
- });
226
-
227
- test("result includes resolved path", async () => {
228
- const base64Content = Buffer.from("test").toString("base64");
229
- const stored = uploadAttachment("doc.txt", "text/plain", base64Content);
230
-
231
- const result = await assetMaterializeTool.execute(
232
- { attachment_id: stored.id, destination_path: "output/doc.txt" },
233
- dummyContext,
234
- );
235
-
236
- expect(result.isError).toBe(false);
237
- expect(result.content).toContain(join(sandboxDir, "output", "doc.txt"));
238
- });
239
-
240
- test("result includes filename, MIME type and size info", async () => {
241
- const base64Content = Buffer.from("some data here").toString("base64");
242
- const stored = uploadAttachment(
243
- "report.pdf",
244
- "application/pdf",
245
- base64Content,
246
- );
247
-
248
- const result = await assetMaterializeTool.execute(
249
- { attachment_id: stored.id, destination_path: "report.pdf" },
250
- dummyContext,
251
- );
252
-
253
- expect(result.isError).toBe(false);
254
- expect(result.content).toContain("report.pdf");
255
- expect(result.content).toContain("application/pdf");
256
- });
257
- });
258
-
259
- // ---------------------------------------------------------------------------
260
- // Size limit enforcement
261
- // ---------------------------------------------------------------------------
262
-
263
- describe("AssetMaterializeTool size limit", () => {
264
- beforeEach(resetTables);
265
-
266
- test("rejects attachment exceeding 100MB limit", async () => {
267
- // Simulate a large attachment by inserting directly into the DB
268
- // with a sizeBytes value over the limit
269
- const db = getDb();
270
- const fakeId = "oversized-attachment";
271
- db.run(
272
- `INSERT INTO attachments (id, original_filename, mime_type, size_bytes, kind, data_base64, created_at)
273
- VALUES ('${fakeId}', 'huge.bin', 'application/octet-stream', ${
274
- 101 * 1024 * 1024
275
- }, 'document', 'AAAA', ${Date.now()})`,
276
- );
277
-
278
- const result = await assetMaterializeTool.execute(
279
- { attachment_id: fakeId, destination_path: "huge.bin" },
280
- dummyContext,
281
- );
282
-
283
- expect(result.isError).toBe(true);
284
- expect(result.content).toContain("exceeds");
285
- expect(result.content).toContain("materialization limit");
286
- });
287
- });
288
-
289
- // ---------------------------------------------------------------------------
290
- // Tool metadata
291
- // ---------------------------------------------------------------------------
292
-
293
- describe("AssetMaterializeTool metadata", () => {
294
- test("tool definition has correct name", () => {
295
- const def = assetMaterializeTool.getDefinition();
296
- expect(def.name).toBe("asset_materialize");
297
- });
298
-
299
- test("tool definition has required params", () => {
300
- const def = assetMaterializeTool.getDefinition();
301
- expect((def.input_schema as Record<string, unknown>).required).toEqual([
302
- "attachment_id",
303
- "destination_path",
304
- ]);
305
- });
306
-
307
- test("tool definition has attachment_id and destination_path properties", () => {
308
- const def = assetMaterializeTool.getDefinition();
309
- expect(
310
- (def.input_schema as Record<string, unknown>).properties,
311
- ).toHaveProperty("attachment_id");
312
- expect(
313
- (def.input_schema as Record<string, unknown>).properties,
314
- ).toHaveProperty("destination_path");
315
- });
316
-
317
- test("tool has LOW risk level", () => {
318
- expect(assetMaterializeTool.defaultRiskLevel).toBe(RiskLevel.Low);
319
- });
320
-
321
- test("tool category is assets", () => {
322
- expect(assetMaterializeTool.category).toBe("assets");
323
- });
324
-
325
- test("tool name is asset_materialize", () => {
326
- expect(assetMaterializeTool.name).toBe("asset_materialize");
327
- });
328
- });
329
-
330
- // ---------------------------------------------------------------------------
331
- // Visibility policy enforcement
332
- // ---------------------------------------------------------------------------
333
-
334
- describe("AssetMaterializeTool visibility policy", () => {
335
- beforeEach(resetTables);
336
-
337
- test("materializing from a standard thread works from any context", async () => {
338
- const standardConv = createConversation({ title: "standard-conv" });
339
- const base64Content = Buffer.from("standard content").toString("base64");
340
- const attachment = uploadAttachment(
341
- "public.txt",
342
- "text/plain",
343
- base64Content,
344
- );
345
- const msg = await addMessage(standardConv.id, "user", "standard message");
346
- linkAttachmentToMessage(msg.id, attachment.id, 0);
347
-
348
- // Materialize from a different standard conversation
349
- const otherConv = createConversation({ title: "other-conv" });
350
- const context: ToolContext = {
351
- workingDir: sandboxDir,
352
- conversationId: otherConv.id,
353
- trustClass: "guardian",
354
- };
355
-
356
- const result = await assetMaterializeTool.execute(
357
- { attachment_id: attachment.id, destination_path: "public-output.txt" },
358
- context,
359
- );
360
- expect(result.isError).toBe(false);
361
- expect(result.content).toContain("Materialized");
362
- });
363
-
364
- test("materializing from a private thread works within the same private thread", async () => {
365
- const privateConv = createConversation({
366
- title: "private-conv",
367
- conversationType: "private",
368
- });
369
- const base64Content = Buffer.from("private content").toString("base64");
370
- const attachment = uploadAttachment(
371
- "secret.txt",
372
- "text/plain",
373
- base64Content,
374
- );
375
- const msg = await addMessage(privateConv.id, "user", "private message");
376
- linkAttachmentToMessage(msg.id, attachment.id, 0);
377
-
378
- // Materialize from the same private conversation
379
- const context: ToolContext = {
380
- workingDir: sandboxDir,
381
- conversationId: privateConv.id,
382
- trustClass: "guardian",
383
- };
384
-
385
- const result = await assetMaterializeTool.execute(
386
- { attachment_id: attachment.id, destination_path: "private-output.txt" },
387
- context,
388
- );
389
- expect(result.isError).toBe(false);
390
- expect(result.content).toContain("Materialized");
391
- });
392
-
393
- test("materializing from a private thread is REJECTED from a different conversation", async () => {
394
- const privateConv = createConversation({
395
- title: "private-conv",
396
- conversationType: "private",
397
- });
398
- const base64Content = Buffer.from("private content").toString("base64");
399
- const attachment = uploadAttachment(
400
- "secret.txt",
401
- "text/plain",
402
- base64Content,
403
- );
404
- const msg = await addMessage(privateConv.id, "user", "private message");
405
- linkAttachmentToMessage(msg.id, attachment.id, 0);
406
-
407
- // Attempt to materialize from a different conversation
408
- const otherConv = createConversation({ title: "other-conv" });
409
- const context: ToolContext = {
410
- workingDir: sandboxDir,
411
- conversationId: otherConv.id,
412
- trustClass: "guardian",
413
- };
414
-
415
- const result = await assetMaterializeTool.execute(
416
- { attachment_id: attachment.id, destination_path: "stolen.txt" },
417
- context,
418
- );
419
- expect(result.isError).toBe(true);
420
- expect(result.content).toContain("private conversation");
421
- expect(result.content).toContain("cannot be accessed");
422
- });
423
-
424
- test("error message is user-actionable", async () => {
425
- const privateConv = createConversation({
426
- title: "private-conv",
427
- conversationType: "private",
428
- });
429
- const base64Content = Buffer.from("private content").toString("base64");
430
- const attachment = uploadAttachment(
431
- "confidential.pdf",
432
- "application/pdf",
433
- base64Content,
434
- );
435
- const msg = await addMessage(privateConv.id, "user", "private message");
436
- linkAttachmentToMessage(msg.id, attachment.id, 0);
437
-
438
- // From a standard conversation
439
- const standardConv = createConversation({ title: "standard-conv" });
440
- const context: ToolContext = {
441
- workingDir: sandboxDir,
442
- conversationId: standardConv.id,
443
- trustClass: "guardian",
444
- };
445
-
446
- const result = await assetMaterializeTool.execute(
447
- { attachment_id: attachment.id, destination_path: "stolen.pdf" },
448
- context,
449
- );
450
- expect(result.isError).toBe(true);
451
- // Should mention the filename so the user knows which file
452
- expect(result.content).toContain("confidential.pdf");
453
- // Should explain how to access it
454
- expect(result.content).toContain("from within the private conversation");
455
- });
456
-
457
- test("materializing from a different private thread is REJECTED", async () => {
458
- const privateConv1 = createConversation({
459
- title: "private-conv-1",
460
- conversationType: "private",
461
- });
462
- const base64Content = Buffer.from("private content").toString("base64");
463
- const attachment = uploadAttachment(
464
- "secret.txt",
465
- "text/plain",
466
- base64Content,
467
- );
468
- const msg = await addMessage(privateConv1.id, "user", "private message");
469
- linkAttachmentToMessage(msg.id, attachment.id, 0);
470
-
471
- // Attempt from a different private conversation
472
- const privateConv2 = createConversation({
473
- title: "private-conv-2",
474
- conversationType: "private",
475
- });
476
- const context: ToolContext = {
477
- workingDir: sandboxDir,
478
- conversationId: privateConv2.id,
479
- trustClass: "guardian",
480
- };
481
-
482
- const result = await assetMaterializeTool.execute(
483
- { attachment_id: attachment.id, destination_path: "cross-thread.txt" },
484
- context,
485
- );
486
- expect(result.isError).toBe(true);
487
- expect(result.content).toContain("private conversation");
488
- });
489
-
490
- test("attachment linked to both private and standard threads can be materialized from anywhere", async () => {
491
- const privateConv = createConversation({
492
- title: "private-conv",
493
- conversationType: "private",
494
- });
495
- const standardConv = createConversation({ title: "standard-conv" });
496
- const base64Content = Buffer.from("shared content").toString("base64");
497
- const attachment = uploadAttachment(
498
- "shared.txt",
499
- "text/plain",
500
- base64Content,
501
- );
502
-
503
- const msg1 = await addMessage(privateConv.id, "user", "private message");
504
- const msg2 = await addMessage(standardConv.id, "user", "standard message");
505
- linkAttachmentToMessage(msg1.id, attachment.id, 0);
506
- linkAttachmentToMessage(msg2.id, attachment.id, 0);
507
-
508
- // Should be materializable from a third, unrelated standard conversation
509
- const otherConv = createConversation({ title: "other-conv" });
510
- const context: ToolContext = {
511
- workingDir: sandboxDir,
512
- conversationId: otherConv.id,
513
- trustClass: "guardian",
514
- };
515
-
516
- const result = await assetMaterializeTool.execute(
517
- { attachment_id: attachment.id, destination_path: "shared-output.txt" },
518
- context,
519
- );
520
- expect(result.isError).toBe(false);
521
- expect(result.content).toContain("Materialized");
522
- });
523
- });