@vellumai/assistant 0.6.1 → 0.6.3

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 (463) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docker-entrypoint.sh +12 -2
  4. package/docs/architecture/memory.md +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  7. package/openapi.yaml +184 -69
  8. package/package.json +41 -41
  9. package/scripts/generate-openapi.ts +1 -2
  10. package/src/__tests__/acp-session.test.ts +43 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +1 -0
  13. package/src/__tests__/app-source-watcher.test.ts +37 -11
  14. package/src/__tests__/approval-routes-http.test.ts +178 -1
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  17. package/src/__tests__/browser-manager.test.ts +40 -27
  18. package/src/__tests__/catalog-files.test.ts +862 -0
  19. package/src/__tests__/channel-approvals.test.ts +53 -0
  20. package/src/__tests__/checker.test.ts +104 -170
  21. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  22. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  23. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  24. package/src/__tests__/config-schema.test.ts +125 -48
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  26. package/src/__tests__/context-overflow-approval.test.ts +21 -6
  27. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  28. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  29. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  30. package/src/__tests__/conversation-attachments.test.ts +80 -4
  31. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  32. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  33. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  34. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  35. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  36. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  37. package/src/__tests__/conversation-queue.test.ts +45 -2
  38. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  39. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  40. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  42. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  43. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  44. package/src/__tests__/conversation-store.test.ts +195 -0
  45. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  46. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
  47. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  48. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  49. package/src/__tests__/credential-vault.test.ts +152 -13
  50. package/src/__tests__/credentials-cli.test.ts +2 -2
  51. package/src/__tests__/date-context.test.ts +4 -4
  52. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  53. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  54. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  55. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  56. package/src/__tests__/gemini-provider.test.ts +2 -2
  57. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  58. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  59. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  60. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  61. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  62. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  63. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  64. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  65. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  66. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  67. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  68. package/src/__tests__/host-browser-routes.test.ts +198 -0
  69. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  70. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  71. package/src/__tests__/host-file-proxy.test.ts +185 -1
  72. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  73. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  74. package/src/__tests__/host-shell-tool.test.ts +1 -11
  75. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  76. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  77. package/src/__tests__/inline-command-runner.test.ts +7 -5
  78. package/src/__tests__/integration-status.test.ts +6 -7
  79. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  80. package/src/__tests__/log-export-workspace.test.ts +190 -0
  81. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  82. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  83. package/src/__tests__/mcp-health-check.test.ts +10 -3
  84. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  85. package/src/__tests__/migration-export-http.test.ts +61 -2
  86. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  87. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  88. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  89. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  90. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  91. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  92. package/src/__tests__/oauth-cli.test.ts +707 -60
  93. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  94. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  95. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  96. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  97. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  98. package/src/__tests__/oauth-store.test.ts +1386 -182
  99. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  100. package/src/__tests__/onboarding-template-contract.test.ts +74 -55
  101. package/src/__tests__/openai-provider.test.ts +2 -2
  102. package/src/__tests__/outlook-categories.test.ts +1 -1
  103. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  104. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  105. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  106. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  107. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  108. package/src/__tests__/outlook-trash.test.ts +1 -1
  109. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  110. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  111. package/src/__tests__/permission-mode.test.ts +28 -56
  112. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  113. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  114. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  115. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  116. package/src/__tests__/require-fresh-approval.test.ts +40 -3
  117. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  118. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  119. package/src/__tests__/schedule-routes.test.ts +162 -0
  120. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  121. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  123. package/src/__tests__/set-permission-mode.test.ts +13 -250
  124. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  125. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  126. package/src/__tests__/slack-channel-config.test.ts +12 -15
  127. package/src/__tests__/subagent-detail.test.ts +44 -2
  128. package/src/__tests__/subagent-disposal.test.ts +1 -0
  129. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  130. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  131. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  132. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  134. package/src/__tests__/subagent-tools.test.ts +1 -0
  135. package/src/__tests__/subagent-types.test.ts +1 -0
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  137. package/src/__tests__/system-prompt.test.ts +72 -1
  138. package/src/__tests__/task-scheduler.test.ts +32 -6
  139. package/src/__tests__/telegram-config.test.ts +10 -13
  140. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  141. package/src/__tests__/terminal-tools.test.ts +11 -5
  142. package/src/__tests__/test-preload.ts +14 -0
  143. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  144. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  145. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  146. package/src/__tests__/tool-executor.test.ts +0 -1
  147. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  148. package/src/__tests__/top-level-renderer.test.ts +73 -1
  149. package/src/__tests__/transport-hints-queue.test.ts +62 -0
  150. package/src/__tests__/trust-store.test.ts +4 -4
  151. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  152. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  153. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  154. package/src/__tests__/workspace-policy.test.ts +2 -7
  155. package/src/acp/client-handler.ts +30 -4
  156. package/src/agent/loop.ts +12 -35
  157. package/src/approvals/guardian-request-resolvers.ts +21 -15
  158. package/src/browser-session/__tests__/manager.test.ts +297 -0
  159. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  160. package/src/browser-session/backends/extension.ts +26 -0
  161. package/src/browser-session/backends/local.ts +24 -0
  162. package/src/browser-session/events.ts +164 -0
  163. package/src/browser-session/index.ts +27 -0
  164. package/src/browser-session/manager.ts +159 -0
  165. package/src/browser-session/types.ts +28 -0
  166. package/src/channels/__tests__/types.test.ts +134 -0
  167. package/src/channels/types.ts +55 -0
  168. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  169. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  170. package/src/cli/commands/browser-relay.ts +339 -409
  171. package/src/cli/commands/credentials.ts +3 -3
  172. package/src/cli/commands/default-action.ts +68 -1
  173. package/src/cli/commands/email.ts +18 -13
  174. package/src/cli/commands/mcp.ts +16 -4
  175. package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
  176. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  177. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  178. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  179. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  180. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  181. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  182. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  183. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  184. package/src/cli/commands/oauth/apps.ts +7 -4
  185. package/src/cli/commands/oauth/connect.ts +16 -2
  186. package/src/cli/commands/oauth/disconnect.ts +1 -1
  187. package/src/cli/commands/oauth/providers.ts +200 -36
  188. package/src/cli/commands/oauth/shared.ts +5 -5
  189. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  190. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  191. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  192. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/platform/index.ts +107 -10
  194. package/src/cli/commands/usage.ts +10 -9
  195. package/src/cli/lib/daemon-credential-client.ts +4 -0
  196. package/src/cli/program.ts +10 -3
  197. package/src/config/assistant-feature-flags.ts +59 -55
  198. package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
  199. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  200. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  201. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  202. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  203. package/src/config/bundled-skills/document/SKILL.md +4 -0
  204. package/src/config/bundled-skills/gmail/SKILL.md +12 -7
  205. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  207. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  208. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  209. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  210. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  211. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  212. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  213. package/src/config/env-registry.ts +14 -0
  214. package/src/config/env.ts +21 -0
  215. package/src/config/feature-flag-registry.json +46 -7
  216. package/src/config/loader.ts +56 -1
  217. package/src/config/sanitize-for-transfer.ts +47 -0
  218. package/src/config/schema.ts +46 -5
  219. package/src/config/schemas/host-browser.ts +66 -0
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrieval.ts +103 -0
  222. package/src/config/schemas/security.ts +0 -6
  223. package/src/config/schemas/services.ts +16 -0
  224. package/src/config/types.ts +0 -1
  225. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  226. package/src/context/window-manager.ts +19 -1
  227. package/src/credential-execution/approval-bridge.ts +49 -16
  228. package/src/credential-execution/managed-catalog.ts +3 -7
  229. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  230. package/src/daemon/app-source-watcher.ts +35 -0
  231. package/src/daemon/config-watcher.ts +6 -2
  232. package/src/daemon/context-overflow-approval.ts +5 -1
  233. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  234. package/src/daemon/conversation-agent-loop.ts +74 -19
  235. package/src/daemon/conversation-attachments.ts +40 -1
  236. package/src/daemon/conversation-messaging.ts +3 -0
  237. package/src/daemon/conversation-process.ts +66 -3
  238. package/src/daemon/conversation-queue-manager.ts +8 -0
  239. package/src/daemon/conversation-runtime-assembly.ts +159 -20
  240. package/src/daemon/conversation-surfaces.ts +78 -12
  241. package/src/daemon/conversation-tool-setup.ts +74 -11
  242. package/src/daemon/conversation-workspace.ts +12 -0
  243. package/src/daemon/conversation.ts +227 -11
  244. package/src/daemon/date-context.ts +10 -10
  245. package/src/daemon/first-greeting.ts +3 -2
  246. package/src/daemon/handlers/conversations.ts +9 -139
  247. package/src/daemon/handlers/shared.ts +65 -0
  248. package/src/daemon/handlers/skills.ts +232 -37
  249. package/src/daemon/host-bash-proxy.ts +48 -13
  250. package/src/daemon/host-browser-proxy.ts +191 -0
  251. package/src/daemon/host-cu-proxy.ts +36 -11
  252. package/src/daemon/host-file-proxy.ts +57 -9
  253. package/src/daemon/lifecycle.ts +86 -12
  254. package/src/daemon/message-protocol.ts +7 -0
  255. package/src/daemon/message-types/conversations.ts +59 -13
  256. package/src/daemon/message-types/host-browser.ts +100 -0
  257. package/src/daemon/message-types/messages.ts +5 -6
  258. package/src/daemon/message-types/notifications.ts +12 -0
  259. package/src/daemon/message-types/settings.ts +12 -0
  260. package/src/daemon/message-types/skills.ts +10 -0
  261. package/src/daemon/message-types/subagents.ts +2 -0
  262. package/src/daemon/server.ts +112 -35
  263. package/src/daemon/tool-side-effects.ts +6 -0
  264. package/src/daemon/transport-hints.ts +14 -0
  265. package/src/inbound/platform-callback-registration.ts +18 -17
  266. package/src/index.ts +1 -1
  267. package/src/mcp/client.ts +59 -24
  268. package/src/memory/app-store.ts +31 -1
  269. package/src/memory/conversation-crud.ts +38 -10
  270. package/src/memory/conversation-directories.ts +39 -0
  271. package/src/memory/conversation-group-migration.ts +65 -5
  272. package/src/memory/conversation-starters-cadence.ts +76 -0
  273. package/src/memory/conversation-title-service.ts +5 -2
  274. package/src/memory/db-init.ts +12 -0
  275. package/src/memory/embedding-backend.test.ts +75 -0
  276. package/src/memory/embedding-backend.ts +131 -5
  277. package/src/memory/embedding-gemini.test.ts +54 -0
  278. package/src/memory/embedding-gemini.ts +20 -9
  279. package/src/memory/embedding-local.ts +177 -18
  280. package/src/memory/graph/capability-seed.ts +3 -5
  281. package/src/memory/graph/consolidation.ts +10 -23
  282. package/src/memory/graph/extraction-job.ts +15 -0
  283. package/src/memory/graph/retriever.ts +40 -22
  284. package/src/memory/graph/store.test.ts +7 -3
  285. package/src/memory/graph/store.ts +47 -12
  286. package/src/memory/group-crud.ts +25 -9
  287. package/src/memory/llm-usage-store.ts +45 -4
  288. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  289. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  290. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  291. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  292. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  293. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  294. package/src/memory/migrations/index.ts +6 -0
  295. package/src/memory/migrations/registry.ts +8 -0
  296. package/src/memory/schema/conversations.ts +1 -0
  297. package/src/memory/schema/oauth.ts +18 -13
  298. package/src/messaging/provider.ts +1 -1
  299. package/src/notifications/broadcaster.ts +6 -0
  300. package/src/notifications/conversation-pairing.ts +12 -4
  301. package/src/notifications/emit-signal.ts +14 -0
  302. package/src/notifications/signal.ts +11 -0
  303. package/src/oauth/AGENTS.md +76 -0
  304. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  305. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  306. package/src/oauth/byo-connection.test.ts +8 -8
  307. package/src/oauth/byo-connection.ts +7 -7
  308. package/src/oauth/connect-orchestrator.ts +23 -21
  309. package/src/oauth/connect-types.ts +3 -3
  310. package/src/oauth/connection-resolver.test.ts +17 -4
  311. package/src/oauth/connection-resolver.ts +16 -16
  312. package/src/oauth/connection.ts +1 -1
  313. package/src/oauth/manual-token-connection.ts +13 -13
  314. package/src/oauth/oauth-store.ts +214 -100
  315. package/src/oauth/platform-connection.test.ts +5 -5
  316. package/src/oauth/platform-connection.ts +4 -4
  317. package/src/oauth/provider-serializer.ts +31 -5
  318. package/src/oauth/revoke.ts +76 -0
  319. package/src/oauth/seed-providers.ts +127 -87
  320. package/src/oauth/token-persistence.ts +1 -1
  321. package/src/permissions/checker.ts +3 -3
  322. package/src/permissions/defaults.ts +7 -8
  323. package/src/permissions/permission-mode.ts +4 -11
  324. package/src/permissions/prompter.ts +13 -3
  325. package/src/permissions/v2-consent-policy.ts +87 -0
  326. package/src/platform/client.ts +1 -1
  327. package/src/prompts/system-prompt.ts +18 -21
  328. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  329. package/src/prompts/templates/BOOTSTRAP.md +59 -96
  330. package/src/prompts/templates/SOUL.md +11 -11
  331. package/src/providers/anthropic/client.ts +1 -0
  332. package/src/providers/types.ts +1 -1
  333. package/src/runtime/AGENTS.md +23 -0
  334. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  335. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  336. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  337. package/src/runtime/assistant-event-hub.ts +24 -2
  338. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  339. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  341. package/src/runtime/auth/middleware.ts +98 -0
  342. package/src/runtime/auth/route-policy.ts +6 -7
  343. package/src/runtime/auth/token-service.ts +8 -0
  344. package/src/runtime/capability-tokens.ts +414 -0
  345. package/src/runtime/channel-approvals.ts +18 -5
  346. package/src/runtime/chrome-extension-registry.ts +332 -0
  347. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  348. package/src/runtime/guardian-decision-types.ts +7 -0
  349. package/src/runtime/http-server.ts +425 -70
  350. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  351. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  352. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  353. package/src/runtime/migrations/migration-transport.ts +6 -0
  354. package/src/runtime/migrations/migration-wizard.ts +22 -2
  355. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  356. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  357. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  358. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  359. package/src/runtime/pending-interactions.ts +29 -13
  360. package/src/runtime/routes/approval-routes.ts +90 -16
  361. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  362. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  363. package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
  364. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  365. package/src/runtime/routes/conversation-routes.ts +308 -28
  366. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  367. package/src/runtime/routes/group-routes.ts +22 -8
  368. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  369. package/src/runtime/routes/host-browser-routes.ts +279 -0
  370. package/src/runtime/routes/host-file-routes.ts +9 -1
  371. package/src/runtime/routes/identity-routes.ts +259 -16
  372. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  373. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  374. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  375. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  376. package/src/runtime/routes/log-export-routes.ts +60 -25
  377. package/src/runtime/routes/memory-item-routes.ts +1 -7
  378. package/src/runtime/routes/migration-routes.ts +87 -2
  379. package/src/runtime/routes/oauth-apps.ts +15 -17
  380. package/src/runtime/routes/oauth-providers.ts +4 -0
  381. package/src/runtime/routes/schedule-routes.ts +24 -11
  382. package/src/runtime/routes/settings-routes.ts +9 -97
  383. package/src/runtime/routes/skills-routes.ts +52 -2
  384. package/src/runtime/routes/subagents-routes.ts +14 -10
  385. package/src/runtime/routes/usage-routes.ts +8 -7
  386. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  387. package/src/runtime/routes/workspace-routes.ts +8 -1
  388. package/src/runtime/routes/workspace-utils.ts +2 -0
  389. package/src/schedule/scheduler.ts +7 -5
  390. package/src/security/ces-credential-client.ts +20 -0
  391. package/src/security/ces-rpc-credential-backend.ts +17 -0
  392. package/src/security/credential-backend.ts +5 -0
  393. package/src/security/oauth2.ts +42 -25
  394. package/src/security/secure-keys.ts +118 -25
  395. package/src/security/token-manager.ts +23 -10
  396. package/src/skills/catalog-files.ts +492 -0
  397. package/src/skills/inline-command-runner.ts +12 -14
  398. package/src/subagent/manager.ts +131 -26
  399. package/src/subagent/types.ts +19 -0
  400. package/src/tools/apps/executors.ts +11 -2
  401. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  402. package/src/tools/browser/auth-detector.ts +43 -12
  403. package/src/tools/browser/browser-execution.ts +645 -340
  404. package/src/tools/browser/browser-manager.ts +36 -12
  405. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  406. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  407. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  408. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  409. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  410. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  411. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  412. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  413. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  414. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  415. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  416. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  417. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  418. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  419. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  420. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  421. package/src/tools/browser/cdp-client/errors.ts +34 -0
  422. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  423. package/src/tools/browser/cdp-client/factory.ts +204 -0
  424. package/src/tools/browser/cdp-client/index.ts +14 -0
  425. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  426. package/src/tools/browser/cdp-client/types.ts +52 -0
  427. package/src/tools/filesystem/edit.ts +1 -1
  428. package/src/tools/filesystem/list.ts +1 -1
  429. package/src/tools/filesystem/read.ts +1 -1
  430. package/src/tools/filesystem/write.ts +2 -1
  431. package/src/tools/host-filesystem/edit.ts +1 -1
  432. package/src/tools/host-filesystem/read.ts +12 -15
  433. package/src/tools/host-filesystem/write.ts +1 -1
  434. package/src/tools/host-terminal/host-shell.ts +21 -16
  435. package/src/tools/permission-checker.ts +77 -100
  436. package/src/tools/registry.ts +0 -2
  437. package/src/tools/secret-detection-handler.ts +34 -1
  438. package/src/tools/shared/filesystem/image-read.ts +61 -40
  439. package/src/tools/skills/sandbox-runner.ts +3 -6
  440. package/src/tools/subagent/spawn.ts +47 -3
  441. package/src/tools/subagent/status.ts +2 -0
  442. package/src/tools/system/register.ts +2 -16
  443. package/src/tools/terminal/safe-env.ts +7 -0
  444. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  445. package/src/tools/terminal/sandbox.ts +4 -1
  446. package/src/tools/terminal/shell.ts +24 -21
  447. package/src/tools/tool-approval-handler.ts +48 -2
  448. package/src/tools/types.ts +2 -3
  449. package/src/util/platform.ts +14 -19
  450. package/src/watcher/provider-types.ts +1 -1
  451. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  452. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  453. package/src/workspace/migrations/registry.ts +2 -0
  454. package/src/workspace/top-level-renderer.ts +19 -1
  455. package/src/__tests__/chrome-cdp.test.ts +0 -419
  456. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  457. package/src/__tests__/permission-mode-store.test.ts +0 -277
  458. package/src/browser-extension-relay/protocol.ts +0 -63
  459. package/src/browser-extension-relay/server.ts +0 -203
  460. package/src/config/schemas/sandbox.ts +0 -14
  461. package/src/permissions/permission-mode-store.ts +0 -180
  462. package/src/tools/browser/chrome-cdp.ts +0 -239
  463. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Workspace allowlist module for the daemon log export endpoint.
3
+ *
4
+ * `POST /v1/export` collects audit DB rows, daemon logs, and a sanitized
5
+ * `config.json` snapshot. This module governs which subpaths of the user's
6
+ * workspace directory (`~/.vellum/workspace/`) are *opted in* to the export
7
+ * archive. The default is "nothing from the workspace ships" — every entry
8
+ * here must be justified against the rules in `./AGENTS.md`.
9
+ *
10
+ * The first allowlisted entry is `<workspace>/conversations/`, which honors
11
+ * both the time filter (via the parsed timestamp prefix on each conversation
12
+ * directory name) and the conversationId filter (via exact match on the id
13
+ * suffix). Directory names that don't match the canonical
14
+ * `<ISO-with-dashes>_<conversationId>` format are silently skipped (Rule 3).
15
+ */
16
+
17
+ import {
18
+ closeSync,
19
+ cpSync,
20
+ existsSync,
21
+ lstatSync,
22
+ mkdirSync,
23
+ openSync,
24
+ readdirSync,
25
+ readSync,
26
+ } from "node:fs";
27
+ import { join } from "node:path";
28
+ import { StringDecoder } from "node:string_decoder";
29
+
30
+ import { parseConversationDirName } from "../../../memory/conversation-directories.js";
31
+ import { getLogger } from "../../../util/logger.js";
32
+ import { getConversationsDir } from "../../../util/platform.js";
33
+
34
+ const log = getLogger("log-export-workspace");
35
+
36
+ /**
37
+ * Maximum total bytes that the workspace allowlist may contribute to a
38
+ * single export archive. Mirrors `MAX_LOG_PAYLOAD_BYTES` in
39
+ * `log-export-routes.ts` so that the workspace section can never blow past
40
+ * the same 10 MB cap that already governs the daemon-logs section.
41
+ */
42
+ export const MAX_WORKSPACE_PAYLOAD_BYTES = 10 * 1024 * 1024;
43
+
44
+ export interface CollectWorkspaceDataOptions {
45
+ /** Absolute path of the export staging directory. */
46
+ staging: string;
47
+ /** When set, restrict allowlisted entries to this conversation. */
48
+ conversationId?: string;
49
+ /** Lower bound (epoch ms, inclusive). */
50
+ startTime?: number;
51
+ /** Upper bound (epoch ms, inclusive). */
52
+ endTime?: number;
53
+ /** Override the default 10 MB cap (used in tests). */
54
+ maxBytes?: number;
55
+ }
56
+
57
+ export interface CollectWorkspaceDataResult {
58
+ /** Allowlisted entries that were copied to staging/workspace/. */
59
+ entries: Array<{
60
+ /** Allowlist entry name (e.g. "conversations"). */
61
+ entry: string;
62
+ /** Number of items (files or subdirs) copied. */
63
+ itemCount: number;
64
+ /** Total bytes copied for this entry. */
65
+ bytes: number;
66
+ /** Items skipped because the cap would be exceeded. */
67
+ skippedDueToCap: number;
68
+ }>;
69
+ totalBytes: number;
70
+ }
71
+
72
+ /**
73
+ * Walk a directory recursively and sum the sizes of every regular file
74
+ * underneath it. Bails out early once the running total would push the
75
+ * workspace cap over `remainingBudget` bytes — that way we never burn
76
+ * cycles totalling a multi-gigabyte directory only to discard it.
77
+ *
78
+ * Returns `null` to signal "this directory is too big to fit in the
79
+ * remaining budget"; returns the exact byte total otherwise.
80
+ */
81
+ function dirSizeWithinBudget(
82
+ rootDir: string,
83
+ remainingBudget: number,
84
+ ): number | null {
85
+ let total = 0;
86
+ const stack: string[] = [rootDir];
87
+ while (stack.length > 0) {
88
+ const current = stack.pop()!;
89
+ let entries: string[];
90
+ try {
91
+ entries = readdirSync(current);
92
+ } catch (err) {
93
+ log.warn(
94
+ { err, dir: current },
95
+ "Failed to read workspace directory while sizing; skipping",
96
+ );
97
+ continue;
98
+ }
99
+ for (const name of entries) {
100
+ const child = join(current, name);
101
+ let stat: ReturnType<typeof lstatSync>;
102
+ try {
103
+ // Use lstat (not stat) so symlinks are NOT dereferenced. Without
104
+ // this, a symlink cycle inside a conversation directory (e.g.
105
+ // `loop -> .`) would cause the walker to recurse forever and
106
+ // hang `collectWorkspaceData`. With lstat, symlinks show up as
107
+ // symlinks — neither `isDirectory()` nor `isFile()` is true on
108
+ // the lstat result, so they're naturally skipped below.
109
+ stat = lstatSync(child);
110
+ } catch (err) {
111
+ log.warn(
112
+ { err, path: child },
113
+ "Failed to stat workspace path while sizing; skipping",
114
+ );
115
+ continue;
116
+ }
117
+ if (stat.isDirectory()) {
118
+ stack.push(child);
119
+ } else if (stat.isFile()) {
120
+ total += stat.size;
121
+ if (total > remainingBudget) {
122
+ return null;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ return total;
128
+ }
129
+
130
+ /**
131
+ * Chunk size used by the streaming `messages.jsonl` reader. 64 KB is
132
+ * large enough to amortize syscall overhead but small enough to keep
133
+ * the synchronous read path off the event loop for any meaningful
134
+ * stretch.
135
+ */
136
+ const MESSAGES_SCAN_CHUNK_BYTES = 64 * 1024;
137
+
138
+ /**
139
+ * Check whether a single JSONL line records a message whose `ts` falls
140
+ * in the `[startTime, endTime]` window. Returns `false` for malformed
141
+ * lines, missing/wrong-typed `ts` fields, and dates outside the window.
142
+ * Pulled out as a helper so the streaming reader can call it on each
143
+ * decoded line without duplicating the parsing logic.
144
+ */
145
+ function lineMatchesWindow(
146
+ line: string,
147
+ startTime: number | undefined,
148
+ endTime: number | undefined,
149
+ ): boolean {
150
+ if (!line) return false;
151
+ let record: { ts?: unknown };
152
+ try {
153
+ record = JSON.parse(line) as { ts?: unknown };
154
+ } catch {
155
+ return false;
156
+ }
157
+ if (typeof record.ts !== "string") return false;
158
+ const ms = Date.parse(record.ts);
159
+ if (Number.isNaN(ms)) return false;
160
+ if (startTime !== undefined && ms < startTime) return false;
161
+ if (endTime !== undefined && ms > endTime) return false;
162
+ return true;
163
+ }
164
+
165
+ /**
166
+ * Scan a conversation's `messages.jsonl` file and report whether any
167
+ * message's `ts` (an ISO 8601 string written by `conversation-disk-view`)
168
+ * falls inside the `[startTime, endTime]` window.
169
+ *
170
+ * Returns:
171
+ * - `true` if at least one message timestamp lies in the window.
172
+ * - `false` otherwise (including: file is missing, file is empty, every
173
+ * line fails to parse, or no parsed line lands in the window).
174
+ *
175
+ * Lines that fail to parse as JSON or whose `ts` is not a parseable date
176
+ * are silently skipped — they shouldn't be able to make the function
177
+ * throw, since the export pipeline must never crash on a malformed
178
+ * conversation file.
179
+ *
180
+ * The reader streams the file in fixed-size chunks (`MESSAGES_SCAN_CHUNK_BYTES`)
181
+ * via `readSync` and decodes UTF-8 across chunk boundaries with
182
+ * `StringDecoder`. It bails out as soon as it finds the first matching
183
+ * line, so the worst case for an in-window conversation is "one early
184
+ * hit", and the worst case for an out-of-window conversation is "read
185
+ * the whole file once" — without ever holding more than one chunk plus
186
+ * one in-progress line in memory.
187
+ */
188
+ function conversationHasMessageInWindow(
189
+ conversationDir: string,
190
+ startTime: number | undefined,
191
+ endTime: number | undefined,
192
+ ): boolean {
193
+ // No window means every message trivially "matches", but the only
194
+ // caller (`collectConversations`) already short-circuits in that case
195
+ // and never invokes this helper. Defensive check kept so the helper is
196
+ // safe to reuse.
197
+ if (startTime === undefined && endTime === undefined) return true;
198
+
199
+ const messagesPath = join(conversationDir, "messages.jsonl");
200
+ let fd: number;
201
+ try {
202
+ fd = openSync(messagesPath, "r");
203
+ } catch {
204
+ // Missing or unreadable messages file → no in-window evidence.
205
+ return false;
206
+ }
207
+
208
+ const buffer = Buffer.alloc(MESSAGES_SCAN_CHUNK_BYTES);
209
+ const decoder = new StringDecoder("utf8");
210
+ let leftover = "";
211
+ try {
212
+ while (true) {
213
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
214
+ if (bytesRead === 0) break;
215
+ const text = leftover + decoder.write(buffer.subarray(0, bytesRead));
216
+ const lines = text.split("\n");
217
+ // The last segment may be a partial line — hold it back for the
218
+ // next chunk to complete.
219
+ leftover = lines.pop() ?? "";
220
+ for (const line of lines) {
221
+ if (lineMatchesWindow(line, startTime, endTime)) return true;
222
+ }
223
+ }
224
+ // Drain any partial UTF-8 sequence the decoder is still holding,
225
+ // then check the final unterminated line (the file may not end with
226
+ // a newline).
227
+ const tail = leftover + decoder.end();
228
+ if (lineMatchesWindow(tail, startTime, endTime)) return true;
229
+ } finally {
230
+ try {
231
+ closeSync(fd);
232
+ } catch {
233
+ /* best-effort close */
234
+ }
235
+ }
236
+ return false;
237
+ }
238
+
239
+ function collectConversations(
240
+ opts: CollectWorkspaceDataOptions,
241
+ result: CollectWorkspaceDataResult,
242
+ ): void {
243
+ const maxBytes = opts.maxBytes ?? MAX_WORKSPACE_PAYLOAD_BYTES;
244
+ // Initialize the entry summary and push it onto `result.entries`
245
+ // immediately so the conversations entry is always present in the
246
+ // result, even if the candidate loop below throws partway through.
247
+ // The array holds a reference to this object, so all later mutations
248
+ // to `entry.itemCount`, `entry.bytes`, and `entry.skippedDueToCap`
249
+ // are visible to consumers via `result.entries`.
250
+ const entry = {
251
+ entry: "conversations",
252
+ itemCount: 0,
253
+ bytes: 0,
254
+ skippedDueToCap: 0,
255
+ };
256
+ result.entries.push(entry);
257
+
258
+ const sourceDir = getConversationsDir();
259
+ if (!existsSync(sourceDir)) {
260
+ return;
261
+ }
262
+
263
+ let names: string[];
264
+ try {
265
+ names = readdirSync(sourceDir);
266
+ } catch (err) {
267
+ log.warn(
268
+ { err, sourceDir },
269
+ "Failed to read conversations directory; skipping conversations entry",
270
+ );
271
+ return;
272
+ }
273
+
274
+ const destBase = join(opts.staging, "workspace", "conversations");
275
+
276
+ // First pass: parse the name, apply the conversationId filter, validate
277
+ // that the entry is a real directory (not a symlink, not a regular
278
+ // file), then apply the time-window filter (which may need to read
279
+ // `messages.jsonl`). Collect surviving candidates so we can sort them
280
+ // deterministically before applying the byte cap.
281
+ //
282
+ // The non-directory / symlink validation happens BEFORE the message
283
+ // scan so a canonical-named symlink can never coerce
284
+ // `conversationHasMessageInWindow` into reading from outside the
285
+ // `conversations/` boundary.
286
+ const candidates: Array<{
287
+ name: string;
288
+ parsed: { conversationId: string; createdAtMs: number };
289
+ }> = [];
290
+ for (const name of names) {
291
+ let parsed: ReturnType<typeof parseConversationDirName>;
292
+ try {
293
+ parsed = parseConversationDirName(name);
294
+ } catch (err) {
295
+ log.warn(
296
+ { err, name },
297
+ "Failed to parse conversation directory name; skipping",
298
+ );
299
+ continue;
300
+ }
301
+ if (!parsed) continue; // Rule 3 — default deny non-canonical names.
302
+
303
+ if (
304
+ opts.conversationId !== undefined &&
305
+ parsed.conversationId !== opts.conversationId
306
+ ) {
307
+ continue;
308
+ }
309
+
310
+ const srcPath = join(sourceDir, name);
311
+
312
+ // Boundary guard: a canonical-looking entry must be a real directory
313
+ // under `conversations/`. Use `lstatSync` (not `statSync`) so
314
+ // symlinks are not dereferenced — a symlink with a canonical name
315
+ // pointing at an external directory must not be allowed to escape
316
+ // the allowlist boundary, neither for the time-window message scan
317
+ // below nor for the eventual `cpSync` copy. Symlinks and regular
318
+ // files are rejected explicitly here so the message scan and the
319
+ // copy loop only ever see real directories.
320
+ let srcStat: ReturnType<typeof lstatSync>;
321
+ try {
322
+ srcStat = lstatSync(srcPath);
323
+ } catch (err) {
324
+ log.warn({ err, srcPath }, "Failed to stat conversation entry; skipping");
325
+ continue;
326
+ }
327
+ if (srcStat.isSymbolicLink()) {
328
+ log.warn(
329
+ { srcPath },
330
+ "Conversation entry is a symbolic link; skipping to preserve allowlist boundary",
331
+ );
332
+ continue;
333
+ }
334
+ if (!srcStat.isDirectory()) {
335
+ log.warn({ srcPath }, "Conversation entry is not a directory; skipping");
336
+ continue;
337
+ }
338
+
339
+ // Time-window filter: keep the conversation if EITHER its createdAt
340
+ // (parsed from the directory name) OR any individual message inside
341
+ // `messages.jsonl` falls in the requested window. This is the union
342
+ // semantics — a conversation that was started before the window but
343
+ // received messages during it should still ship, since the user
344
+ // running an export almost always wants to see the activity that
345
+ // happened during the window, not just conversations that were
346
+ // _created_ in it.
347
+ if (opts.startTime !== undefined || opts.endTime !== undefined) {
348
+ const createdAtInWindow =
349
+ (opts.startTime === undefined ||
350
+ parsed.createdAtMs >= opts.startTime) &&
351
+ (opts.endTime === undefined || parsed.createdAtMs <= opts.endTime);
352
+ if (!createdAtInWindow) {
353
+ // Fall back to scanning messages.jsonl for in-window activity.
354
+ // This is more expensive than the directory-name parse, so we
355
+ // only do it when the cheap check failed. The boundary guard
356
+ // above guarantees `srcPath` is a real in-allowlist directory,
357
+ // so the file path the scanner reads stays inside the allowlist.
358
+ let hasMessageInWindow: boolean;
359
+ try {
360
+ hasMessageInWindow = conversationHasMessageInWindow(
361
+ srcPath,
362
+ opts.startTime,
363
+ opts.endTime,
364
+ );
365
+ } catch (err) {
366
+ log.warn(
367
+ { err, srcPath },
368
+ "Failed to scan messages.jsonl for window match; skipping",
369
+ );
370
+ continue;
371
+ }
372
+ if (!hasMessageInWindow) continue;
373
+ }
374
+ }
375
+
376
+ candidates.push({ name, parsed });
377
+ }
378
+
379
+ // Newest first so cap-truncation keeps the most recent conversations.
380
+ candidates.sort((a, b) => b.parsed.createdAtMs - a.parsed.createdAtMs);
381
+
382
+ for (const { name } of candidates) {
383
+ const srcPath = join(sourceDir, name);
384
+
385
+ const remainingBudget = maxBytes - result.totalBytes;
386
+ let dirBytes: number | null;
387
+ try {
388
+ dirBytes = dirSizeWithinBudget(srcPath, remainingBudget);
389
+ } catch (err) {
390
+ log.warn(
391
+ { err, srcPath },
392
+ "Failed to compute conversation directory size; skipping",
393
+ );
394
+ continue;
395
+ }
396
+
397
+ if (dirBytes === null) {
398
+ // Including this directory would exceed the workspace cap.
399
+ entry.skippedDueToCap += 1;
400
+ continue;
401
+ }
402
+
403
+ try {
404
+ mkdirSync(destBase, { recursive: true });
405
+ cpSync(srcPath, join(destBase, name), { recursive: true });
406
+ } catch (err) {
407
+ log.warn(
408
+ { err, srcPath },
409
+ "Failed to copy conversation directory; skipping",
410
+ );
411
+ continue;
412
+ }
413
+
414
+ entry.itemCount += 1;
415
+ entry.bytes += dirBytes;
416
+ result.totalBytes += dirBytes;
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Collect allowlisted workspace data into `<staging>/workspace/`.
422
+ *
423
+ * Currently the only allowlisted entry is `conversations/`. Future entries
424
+ * should follow the rules in `./AGENTS.md` (time filter, conversation
425
+ * filter, byte cap, registry update). The function never throws — all
426
+ * filesystem errors are logged at warn level so the rest of the export
427
+ * pipeline can continue regardless.
428
+ */
429
+ export function collectWorkspaceData(
430
+ opts: CollectWorkspaceDataOptions,
431
+ ): CollectWorkspaceDataResult {
432
+ const result: CollectWorkspaceDataResult = {
433
+ entries: [],
434
+ totalBytes: 0,
435
+ };
436
+
437
+ try {
438
+ collectConversations(opts, result);
439
+ } catch (err) {
440
+ log.warn(
441
+ { err },
442
+ "Unexpected error while collecting workspace conversations entry",
443
+ );
444
+ }
445
+
446
+ log.info(
447
+ {
448
+ entries: result.entries,
449
+ totalBytes: result.totalBytes,
450
+ conversationId: opts.conversationId ?? null,
451
+ startTime: opts.startTime ?? null,
452
+ endTime: opts.endTime ?? null,
453
+ },
454
+ "Workspace allowlist collection complete",
455
+ );
456
+
457
+ return result;
458
+ }
@@ -39,6 +39,7 @@ import { APP_VERSION, COMMIT_SHA } from "../../version.js";
39
39
  import { httpError } from "../http-errors.js";
40
40
  import type { RouteDefinition } from "../http-router.js";
41
41
  import { createTarGz } from "./archive-utils.js";
42
+ import { collectWorkspaceData } from "./log-export/workspace-allowlist.js";
42
43
 
43
44
  const log = getLogger("log-export-routes");
44
45
 
@@ -48,6 +49,7 @@ const MAX_LOG_PAYLOAD_BYTES = 10 * 1024 * 1024;
48
49
  interface ExportRequestBody {
49
50
  auditLimit?: number;
50
51
  conversationId?: string; // scope to a single conversation
52
+ full?: boolean; // include all conversation data (messages + LLM logs) — use for test data debugging
51
53
  startTime?: number; // epoch ms — lower bound (inclusive)
52
54
  endTime?: number; // epoch ms — upper bound (inclusive)
53
55
  }
@@ -57,15 +59,16 @@ interface ExportRequestBody {
57
59
  * then package everything into a tar.gz archive.
58
60
  *
59
61
  * Archive layout:
60
- * audit-data.json — tool invocation records
61
- * config-snapshot.json — sanitized workspace config
62
- * daemon-logs/<name> — daemon log files
62
+ * audit-data.json — tool invocation records
63
+ * config-snapshot.json — sanitized workspace config
64
+ * daemon-logs/<name> — daemon log files
65
+ * workspace/conversations/<dir>/ — allowlisted workspace data (see ./log-export/AGENTS.md)
63
66
  */
64
67
  async function handleExport(body: ExportRequestBody): Promise<Response> {
65
68
  const staging = mkdtempSync(join(tmpdir(), "vellum-export-"));
66
69
 
67
70
  try {
68
- const { conversationId, startTime, endTime } = body;
71
+ const { conversationId, full, startTime, endTime } = body;
69
72
 
70
73
  // --- Audit data ---
71
74
  const limit = body.auditLimit ?? 1000;
@@ -96,14 +99,19 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
96
99
  "utf-8",
97
100
  );
98
101
 
99
- // --- Conversation-scoped data tables ---
100
- if (conversationId) {
102
+ // --- Conversation data tables ---
103
+ // Included when scoped to a single conversation OR when all conversations are requested.
104
+ if (conversationId || full) {
105
+ const conversationFilter = conversationId
106
+ ? [eq(messages.conversationId, conversationId)]
107
+ : [];
108
+
101
109
  const messageRows = db
102
110
  .select()
103
111
  .from(messages)
104
112
  .where(
105
113
  and(
106
- eq(messages.conversationId, conversationId),
114
+ ...conversationFilter,
107
115
  startTime ? gte(messages.createdAt, startTime) : undefined,
108
116
  endTime ? lte(messages.createdAt, endTime) : undefined,
109
117
  ),
@@ -116,12 +124,16 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
116
124
  "utf-8",
117
125
  );
118
126
 
127
+ const llmConversationFilter = conversationId
128
+ ? [eq(llmRequestLogs.conversationId, conversationId)]
129
+ : [];
130
+
119
131
  const llmLogRows = db
120
132
  .select()
121
133
  .from(llmRequestLogs)
122
134
  .where(
123
135
  and(
124
- eq(llmRequestLogs.conversationId, conversationId),
136
+ ...llmConversationFilter,
125
137
  startTime ? gte(llmRequestLogs.createdAt, startTime) : undefined,
126
138
  endTime ? lte(llmRequestLogs.createdAt, endTime) : undefined,
127
139
  ),
@@ -134,12 +146,16 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
134
146
  "utf-8",
135
147
  );
136
148
 
149
+ const usageConversationFilter = conversationId
150
+ ? [eq(llmUsageEvents.conversationId, conversationId)]
151
+ : [];
152
+
137
153
  const usageRows = db
138
154
  .select()
139
155
  .from(llmUsageEvents)
140
156
  .where(
141
157
  and(
142
- eq(llmUsageEvents.conversationId, conversationId),
158
+ ...usageConversationFilter,
143
159
  startTime ? gte(llmUsageEvents.createdAt, startTime) : undefined,
144
160
  endTime ? lte(llmUsageEvents.createdAt, endTime) : undefined,
145
161
  ),
@@ -241,6 +257,17 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
241
257
  }
242
258
  }
243
259
 
260
+ // --- Workspace allowlist ---
261
+ // Includes specific subpaths from <workspace>/ governed by the rules in
262
+ // ./log-export/AGENTS.md. Honors the same time + conversation filters as
263
+ // the rest of the export.
264
+ const workspaceResult = collectWorkspaceData({
265
+ staging,
266
+ conversationId: conversationId || undefined,
267
+ startTime: startTime || undefined,
268
+ endTime: endTime || undefined,
269
+ });
270
+
244
271
  // --- Sanitized config snapshot ---
245
272
  const configSnapshot = readSanitizedConfig();
246
273
  if (configSnapshot) {
@@ -252,22 +279,21 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
252
279
  }
253
280
 
254
281
  // --- Export manifest ---
255
- const manifest = conversationId
256
- ? {
257
- type: "conversation-export" as const,
258
- conversationId,
259
- assistantVersion: APP_VERSION,
260
- commitSha: COMMIT_SHA,
261
- ...(startTime !== undefined ? { startTime } : {}),
262
- ...(endTime !== undefined ? { endTime } : {}),
263
- exportedAt: new Date().toISOString(),
264
- }
265
- : {
266
- type: "global-export" as const,
267
- assistantVersion: APP_VERSION,
268
- commitSha: COMMIT_SHA,
269
- exportedAt: new Date().toISOString(),
270
- };
282
+ const manifestType = conversationId
283
+ ? ("conversation-export" as const)
284
+ : full
285
+ ? ("full-export" as const)
286
+ : ("global-export" as const);
287
+ const manifest = {
288
+ type: manifestType,
289
+ ...(conversationId ? { conversationId } : {}),
290
+ ...(full ? { full: true } : {}),
291
+ assistantVersion: APP_VERSION,
292
+ commitSha: COMMIT_SHA,
293
+ ...(startTime !== undefined ? { startTime } : {}),
294
+ ...(endTime !== undefined ? { endTime } : {}),
295
+ exportedAt: new Date().toISOString(),
296
+ };
271
297
  writeFileSync(
272
298
  join(staging, "export-manifest.json"),
273
299
  JSON.stringify(manifest, null, 2),
@@ -281,6 +307,9 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
281
307
  totalBytes,
282
308
  hasConfig: configSnapshot !== undefined,
283
309
  conversationId: conversationId ?? null,
310
+ full: full ?? false,
311
+ workspaceEntries: workspaceResult.entries.length,
312
+ workspaceBytes: workspaceResult.totalBytes,
284
313
  },
285
314
  "Export collected, creating tar.gz archive",
286
315
  );
@@ -426,6 +455,12 @@ export function logExportRouteDefinitions(): RouteDefinition[] {
426
455
  .string()
427
456
  .optional()
428
457
  .describe("Scope to a single conversation"),
458
+ full: z
459
+ .boolean()
460
+ .optional()
461
+ .describe(
462
+ "Full export — include messages, LLM request logs, and usage events for all conversations. Use for test data debugging.",
463
+ ),
429
464
  startTime: z.number().optional().describe("Lower bound epoch ms"),
430
465
  endTime: z.number().optional().describe("Upper bound epoch ms"),
431
466
  });
@@ -712,15 +712,9 @@ export async function handleDeleteMemoryItem(
712
712
  return httpError("NOT_FOUND", "Memory item not found", 404);
713
713
  }
714
714
 
715
- // Hard-delete the node (cascades to edges and triggers via FK)
715
+ // Soft-delete the node (deleteNode sets fidelity='gone' and enqueues Qdrant cleanup)
716
716
  deleteNode(id);
717
717
 
718
- // Clean up Qdrant vectors asynchronously
719
- enqueueMemoryJob("delete_qdrant_vectors", {
720
- targetType: "graph_node",
721
- targetId: id,
722
- });
723
-
724
718
  return new Response(null, { status: 204 });
725
719
  }
726
720