@vellumai/assistant 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -72,8 +72,12 @@ mock.module("../permissions/checker.js", () => ({
72
72
  generateScopeOptions: () => [{ label: "/tmp", scope: "/tmp" }],
73
73
  }));
74
74
 
75
+ // Mock every export so downstream test files that dynamically import modules
76
+ // with a static `from "../memory/tool-usage-store.js"` still see all symbols.
75
77
  mock.module("../memory/tool-usage-store.js", () => ({
76
78
  recordToolInvocation: () => {},
79
+ getRecentInvocations: () => [],
80
+ rotateToolInvocations: () => 0,
77
81
  }));
78
82
 
79
83
  mock.module("../tools/registry.js", () => ({
@@ -621,11 +625,11 @@ describe("ToolExecutor lifecycle events", () => {
621
625
  expect(events.map((e) => e.type)).toEqual(["start", "executed"]);
622
626
  });
623
627
 
624
- test("file_edit to USER.md emits permission_prompt under forcePromptSideEffects", async () => {
625
- // Security invariant: editing USER.md in a private thread must always
626
- // prompt, even when a trust rule would auto-allow.
628
+ test("file_edit to guardian persona emits permission_prompt under forcePromptSideEffects", async () => {
629
+ // Security invariant: editing the guardian persona file in a private
630
+ // thread must always prompt, even when a trust rule would auto-allow.
627
631
  checkerDecision = "allow";
628
- checkerReason = "Matched trust rule: file_edit:*/USER.md";
632
+ checkerReason = "Matched trust rule: file_edit:*/users/*.md";
629
633
  checkerRisk = "low";
630
634
  promptDecision = "allow";
631
635
 
@@ -635,7 +639,7 @@ describe("ToolExecutor lifecycle events", () => {
635
639
  const result = await executor.execute(
636
640
  "file_edit",
637
641
  {
638
- path: "/Users/sidd/.vellum/workspace/USER.md",
642
+ path: "/Users/sidd/.vellum/workspace/users/sidd.md",
639
643
  old_string: "old",
640
644
  new_string: "new",
641
645
  },
@@ -92,8 +92,12 @@ mock.module("../permissions/trust-store.js", () => ({
92
92
  }));
93
93
 
94
94
  // ── Tool usage store ──
95
+ // Mock every export so downstream test files that dynamically import modules
96
+ // with a static `from "../memory/tool-usage-store.js"` still see all symbols.
95
97
  mock.module("../memory/tool-usage-store.js", () => ({
96
98
  recordToolInvocation: () => {},
99
+ getRecentInvocations: () => [],
100
+ rotateToolInvocations: () => 0,
97
101
  }));
98
102
 
99
103
  // ── Path policy ──
@@ -129,8 +129,12 @@ mock.module("../permissions/checker.js", () => ({
129
129
  scopeOptionsOverride ?? [{ label: "/tmp", scope: "/tmp" }],
130
130
  }));
131
131
 
132
+ // Mock every export so downstream test files that dynamically import modules
133
+ // with a static `from "../memory/tool-usage-store.js"` still see all symbols.
132
134
  mock.module("../memory/tool-usage-store.js", () => ({
133
135
  recordToolInvocation: () => {},
136
+ getRecentInvocations: () => [],
137
+ rotateToolInvocations: () => 0,
134
138
  }));
135
139
 
136
140
  mock.module("../tools/registry.js", () => ({
@@ -1148,6 +1152,8 @@ describe("isSideEffectTool", () => {
1148
1152
  "browser_type",
1149
1153
  "browser_press_key",
1150
1154
  "browser_close",
1155
+ "browser_attach",
1156
+ "browser_detach",
1151
1157
  "browser_fill_credential",
1152
1158
  "document_create",
1153
1159
  "document_update",
@@ -1220,12 +1226,13 @@ describe("isSideEffectTool", () => {
1220
1226
  });
1221
1227
  });
1222
1228
 
1223
- // Baseline: allow rules can auto-allow file_edit for USER.md today (no forced prompting).
1224
- // The mock check() delegates to findHighestPriorityRule (via spy) so a regression
1225
- // in trust-rule matching would cause this test to fail instead of being masked by
1226
- // a blanket mock-allow.
1227
- describe("ToolExecutor baseline: allow rule auto-allows file_edit USER.md", () => {
1228
- const userMdPath = "/Users/sidd/.vellum/workspace/USER.md";
1229
+ // Baseline: allow rules can auto-allow file_edit for the guardian persona
1230
+ // today (no forced prompting). The mock check() delegates to
1231
+ // findHighestPriorityRule (via spy) so a regression in trust-rule matching
1232
+ // would cause this test to fail instead of being masked by a blanket
1233
+ // mock-allow.
1234
+ describe("ToolExecutor baseline: allow rule auto-allows file_edit guardian persona", () => {
1235
+ const guardianPersonaPath = "/Users/sidd/.vellum/workspace/users/sidd.md";
1229
1236
  let ruleSpy: ReturnType<typeof spyOn> | undefined;
1230
1237
 
1231
1238
  beforeEach(() => {
@@ -1239,18 +1246,19 @@ describe("ToolExecutor baseline: allow rule auto-allows file_edit USER.md", () =
1239
1246
  addRuleSpy = undefined;
1240
1247
  }
1241
1248
 
1242
- // Simulate a trust rule that allows file_edit on USER.md by stubbing
1243
- // findHighestPriorityRule. This mirrors the default allow rules that
1244
- // the trust-store creates for workspace prompt files.
1249
+ // Simulate a trust rule that allows file_edit on the guardian's per-user
1250
+ // persona file by stubbing findHighestPriorityRule. This mirrors the
1251
+ // default allow rules that the trust-store creates for the guardian
1252
+ // persona file (see permissions/defaults.ts).
1245
1253
  ruleSpy = spyOn(trustStore, "findHighestPriorityRule").mockImplementation(
1246
1254
  (tool: string, commands: string[], _scope: string) => {
1247
1255
  if (tool !== "file_edit") return null;
1248
1256
  for (const cmd of commands) {
1249
- if (cmd === `file_edit:${userMdPath}`) {
1257
+ if (cmd === `file_edit:${guardianPersonaPath}`) {
1250
1258
  return {
1251
- id: "default:allow-file_edit-user",
1259
+ id: "default:allow-file_edit-guardian-persona",
1252
1260
  tool: "file_edit",
1253
- pattern: `file_edit:${userMdPath}`,
1261
+ pattern: `file_edit:${guardianPersonaPath}`,
1254
1262
  scope: "everywhere",
1255
1263
  decision: "allow" as const,
1256
1264
  priority: 100,
@@ -1294,11 +1302,11 @@ describe("ToolExecutor baseline: allow rule auto-allows file_edit USER.md", () =
1294
1302
  }
1295
1303
  });
1296
1304
 
1297
- test("file_edit to USER.md is auto-allowed via trust rule", async () => {
1305
+ test("file_edit to guardian persona is auto-allowed via trust rule", async () => {
1298
1306
  const executor = new ToolExecutor(makePrompter());
1299
1307
  const result = await executor.execute(
1300
1308
  "file_edit",
1301
- { path: userMdPath, content: "hello" },
1309
+ { path: guardianPersonaPath, content: "hello" },
1302
1310
  makeContext(),
1303
1311
  );
1304
1312
  expect(result.isError).toBe(false);
@@ -1310,7 +1318,7 @@ describe("ToolExecutor baseline: allow rule auto-allows file_edit USER.md", () =
1310
1318
  expect(ruleSpy).toHaveBeenCalled();
1311
1319
  });
1312
1320
 
1313
- test("file_edit to a non-USER.md path is NOT auto-allowed without a matching rule", async () => {
1321
+ test("file_edit to a non-guardian-persona path is NOT auto-allowed without a matching rule", async () => {
1314
1322
  let promptCalled = false;
1315
1323
  const trackingPrompter = {
1316
1324
  prompt: async () => {
@@ -1530,22 +1538,23 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
1530
1538
  expect(promptCount).toBe(1);
1531
1539
  });
1532
1540
 
1533
- // ── USER.md security invariant (PR 31) ──────────
1541
+ // ── Guardian persona security invariant (PR 31) ──────────
1534
1542
 
1535
- test("file_edit to USER.md forces prompt in private conversation even with matching trust rule", async () => {
1536
- // This is a key security invariant: USER.md contains the user's persistent
1537
- // memory. In a private conversation (forcePromptSideEffects=true), edits to it
1538
- // must always require explicit approval, even when a trust rule matches.
1543
+ test("file_edit to guardian persona forces prompt in private conversation even with matching trust rule", async () => {
1544
+ // This is a key security invariant: the guardian persona file contains
1545
+ // the user's persistent memory. In a private conversation
1546
+ // (forcePromptSideEffects=true), edits to it must always require explicit
1547
+ // approval, even when a trust rule matches.
1539
1548
  checkResultOverride = {
1540
1549
  decision: "allow",
1541
- reason: "Matched trust rule: file_edit:*/USER.md",
1550
+ reason: "Matched trust rule: file_edit:*/users/*.md",
1542
1551
  };
1543
1552
 
1544
1553
  const executor = new ToolExecutor(makeTrackingPrompter());
1545
1554
  const result = await executor.execute(
1546
1555
  "file_edit",
1547
1556
  {
1548
- path: "/Users/sidd/.vellum/workspace/USER.md",
1557
+ path: "/Users/sidd/.vellum/workspace/users/sidd.md",
1549
1558
  old_string: "old pref",
1550
1559
  new_string: "new pref",
1551
1560
  },
@@ -1557,14 +1566,14 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
1557
1566
  expect(promptCalled).toBe(true);
1558
1567
  });
1559
1568
 
1560
- test("host_file_edit to USER.md forces prompt in private conversation", async () => {
1569
+ test("host_file_edit to guardian persona forces prompt in private conversation", async () => {
1561
1570
  checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1562
1571
 
1563
1572
  const executor = new ToolExecutor(makeTrackingPrompter());
1564
1573
  const result = await executor.execute(
1565
1574
  "host_file_edit",
1566
1575
  {
1567
- path: "/Users/sidd/.vellum/workspace/USER.md",
1576
+ path: "/Users/sidd/.vellum/workspace/users/sidd.md",
1568
1577
  old_string: "x",
1569
1578
  new_string: "y",
1570
1579
  },
@@ -11,6 +11,20 @@ import {
11
11
  } from "../context/tool-result-truncation.js";
12
12
  import type { ContentBlock, ToolResultContent } from "../providers/types.js";
13
13
 
14
+ function hasOrphanedSurrogate(str: string): boolean {
15
+ for (let i = 0; i < str.length; i++) {
16
+ const code = str.charCodeAt(i);
17
+ if (code >= 0xd800 && code <= 0xdbff) {
18
+ const next = i + 1 < str.length ? str.charCodeAt(i + 1) : 0;
19
+ if (next < 0xdc00 || next > 0xdfff) return true;
20
+ i++;
21
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
22
+ return true;
23
+ }
24
+ }
25
+ return false;
26
+ }
27
+
14
28
  // ---------------------------------------------------------------------------
15
29
  // Helpers
16
30
  // ---------------------------------------------------------------------------
@@ -100,6 +114,28 @@ describe("truncateToolResultText", () => {
100
114
  expect(result).toBe(text);
101
115
  expect(result).not.toContain(TRUNCATION_SUFFIX);
102
116
  });
117
+
118
+ test("does not orphan a UTF-16 surrogate pair at the cut boundary", () => {
119
+ // Regression for the "no low surrogate in string" Anthropic 400 error.
120
+ // Build a string where the cut point lands inside a surrogate pair:
121
+ // 4999 padding chars, then an emoji (2 code units), then enough filler
122
+ // to push the cut inside the pair.
123
+ const EMOJI = "\uD83C\uDF89";
124
+ // maxChars = 5_000, so cutPoint = 5_000 - TRUNCATION_SUFFIX.length.
125
+ // Put the emoji so its high surrogate lands exactly at cutPoint - 1.
126
+ const maxChars = 5_000;
127
+ const cutPoint = maxChars - TRUNCATION_SUFFIX.length;
128
+ // Fill up to cutPoint - 1 with "a"s, then place the emoji so the high
129
+ // surrogate is the character at cutPoint - 1 and the low is at cutPoint.
130
+ const prefix = "a".repeat(cutPoint - 1);
131
+ const text = prefix + EMOJI + "b".repeat(100);
132
+ // Use a long filler with no newlines so lastIndexOf("\n", cutPoint) === -1
133
+ // and the function falls back to cutPoint itself.
134
+ const result = truncateToolResultText(text, maxChars);
135
+ expect(hasOrphanedSurrogate(result)).toBe(false);
136
+ // JSON.stringify must not throw on the result.
137
+ expect(() => JSON.stringify(result)).not.toThrow();
138
+ });
103
139
  });
104
140
 
105
141
  // ---------------------------------------------------------------------------
@@ -740,8 +740,10 @@ describe("Trust Store", () => {
740
740
  ].sort();
741
741
  expect(defaultTools).toEqual([
742
742
  "bash",
743
+ "browser_attach",
743
744
  "browser_click",
744
745
  "browser_close",
746
+ "browser_detach",
745
747
  "browser_extract",
746
748
  "browser_fill_credential",
747
749
  "browser_hover",
@@ -751,6 +753,7 @@ describe("Trust Store", () => {
751
753
  "browser_scroll",
752
754
  "browser_select_option",
753
755
  "browser_snapshot",
756
+ "browser_status",
754
757
  "browser_type",
755
758
  "browser_wait_for",
756
759
  "browser_wait_for_download",
@@ -1078,13 +1081,15 @@ describe("Trust Store", () => {
1078
1081
 
1079
1082
  // ── default allow: browser tools ────────────────────────────
1080
1083
 
1081
- test("all 14 browser tools have default allow rules", () => {
1084
+ test("all browser tools have default allow rules", () => {
1082
1085
  const templates = getDefaultRuleTemplates();
1083
1086
  const browserTools = [
1084
1087
  "browser_navigate",
1085
1088
  "browser_snapshot",
1086
1089
  "browser_screenshot",
1087
1090
  "browser_close",
1091
+ "browser_attach",
1092
+ "browser_detach",
1088
1093
  "browser_click",
1089
1094
  "browser_type",
1090
1095
  "browser_press_key",
@@ -1095,6 +1100,7 @@ describe("Trust Store", () => {
1095
1100
  "browser_extract",
1096
1101
  "browser_wait_for_download",
1097
1102
  "browser_fill_credential",
1103
+ "browser_status",
1098
1104
  ];
1099
1105
 
1100
1106
  for (const tool of browserTools) {
@@ -117,7 +117,7 @@ mock.module("../prompts/user-reference.js", () => ({
117
117
  ...realUserReference,
118
118
  resolveUserReference: () => "my human",
119
119
  resolveGuardianName: (guardianDisplayName?: string | null): string => {
120
- // Mirror the real implementation: USER.md name > guardianDisplayName > default
120
+ // Mirror the real implementation: guardian persona name (from users/<slug>.md) > guardianDisplayName > default
121
121
  const userRef = "my human"; // In tests, resolveUserReference() returns this
122
122
  if (userRef !== "my human") return userRef;
123
123
  if (guardianDisplayName && guardianDisplayName.trim().length > 0) {
@@ -0,0 +1,345 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, test } from "bun:test";
4
+
5
+ import {
6
+ getCatalogProvider,
7
+ listCatalogProviderIds,
8
+ } from "../tts/provider-catalog.js";
9
+ import type { TtsProviderId } from "../tts/types.js";
10
+
11
+ /**
12
+ * Parity guard: daemon TTS provider catalog vs client TTS catalog JSON.
13
+ *
14
+ * The daemon maintains its canonical provider catalog in
15
+ * `assistant/src/tts/provider-catalog.ts`.
16
+ * The client-facing metadata lives in `meta/tts-provider-catalog.json` and is
17
+ * bundled into native clients at build time.
18
+ *
19
+ * These tests enforce that both catalogs stay in sync on the fields they
20
+ * share: provider IDs, ordering, and credential metadata needed for client
21
+ * key handling. CI will fail when they drift, forcing the developer to update
22
+ * whichever side fell behind.
23
+ */
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Resolve repo root (tests run from assistant/) */
30
+ function getRepoRoot(): string {
31
+ return join(process.cwd(), "..");
32
+ }
33
+
34
+ interface ClientCatalogCredentialsGuide {
35
+ description: string;
36
+ url: string;
37
+ linkLabel: string;
38
+ }
39
+
40
+ interface ClientCatalogEntry {
41
+ id: string;
42
+ displayName: string;
43
+ subtitle: string;
44
+ setupMode: string;
45
+ setupHint: string;
46
+ credentialMode: "api-key" | "credential";
47
+ /** Present when credentialMode is "api-key". */
48
+ apiKeyProviderName?: string;
49
+ /** Present when credentialMode is "credential". */
50
+ credentialNamespace?: string;
51
+ credentialsGuide: ClientCatalogCredentialsGuide;
52
+ }
53
+
54
+ interface ClientCatalog {
55
+ version: number;
56
+ providers: ClientCatalogEntry[];
57
+ }
58
+
59
+ function loadClientCatalog(): ClientCatalog {
60
+ const catalogPath = join(getRepoRoot(), "meta", "tts-provider-catalog.json");
61
+ const raw = readFileSync(catalogPath, "utf-8");
62
+ return JSON.parse(raw);
63
+ }
64
+
65
+ /**
66
+ * Derive the expected credentialMode and associated metadata from a daemon
67
+ * catalog entry's secret requirements.
68
+ *
69
+ * Convention:
70
+ * - If the first secretRequirement.credentialStoreKey starts with "credential/",
71
+ * the client uses credentialMode "credential" and credentialNamespace is the
72
+ * second path segment.
73
+ * - Otherwise, the client uses credentialMode "api-key" and apiKeyProviderName
74
+ * is the bare key.
75
+ */
76
+ function deriveCredentialMetadata(daemonEntry: {
77
+ secretRequirements: readonly { readonly credentialStoreKey: string }[];
78
+ }): {
79
+ credentialMode: "api-key" | "credential";
80
+ apiKeyProviderName?: string;
81
+ credentialNamespace?: string;
82
+ } {
83
+ const key = daemonEntry.secretRequirements[0]?.credentialStoreKey ?? "";
84
+ if (key.startsWith("credential/")) {
85
+ const parts = key.split("/");
86
+ return { credentialMode: "credential", credentialNamespace: parts[1] };
87
+ }
88
+ return { credentialMode: "api-key", apiKeyProviderName: key };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Tests
93
+ // ---------------------------------------------------------------------------
94
+
95
+ describe("TTS catalog parity: daemon vs client", () => {
96
+ // -----------------------------------------------------------------------
97
+ // Provider ID parity
98
+ // -----------------------------------------------------------------------
99
+
100
+ test("client catalog provider IDs match daemon catalog provider IDs", () => {
101
+ const daemonIds = listCatalogProviderIds();
102
+ const clientCatalog = loadClientCatalog();
103
+ const clientIds = clientCatalog.providers.map((p) => p.id);
104
+
105
+ // Every daemon provider ID must appear in the client catalog
106
+ const missingInClient = daemonIds.filter((id) => !clientIds.includes(id));
107
+ if (missingInClient.length > 0) {
108
+ const message = [
109
+ "Daemon catalog has provider IDs not present in meta/tts-provider-catalog.json.",
110
+ "",
111
+ "Missing in client catalog:",
112
+ ...missingInClient.map((id) => ` - ${id}`),
113
+ "",
114
+ "Add entries for these providers to meta/tts-provider-catalog.json.",
115
+ ].join("\n");
116
+ expect(missingInClient, message).toEqual([]);
117
+ }
118
+
119
+ // Every client catalog provider ID must appear in the daemon catalog
120
+ const missingInDaemon = clientIds.filter(
121
+ (id) => !daemonIds.includes(id as never),
122
+ );
123
+ if (missingInDaemon.length > 0) {
124
+ const message = [
125
+ "Client catalog (meta/tts-provider-catalog.json) has provider IDs not present in daemon catalog.",
126
+ "",
127
+ "Missing in daemon catalog:",
128
+ ...missingInDaemon.map((id) => ` - ${id}`),
129
+ "",
130
+ "Add entries for these providers to assistant/src/tts/provider-catalog.ts.",
131
+ ].join("\n");
132
+ expect(missingInDaemon, message).toEqual([]);
133
+ }
134
+ });
135
+
136
+ test("daemon and client catalog list providers in the same order", () => {
137
+ const daemonIds = listCatalogProviderIds();
138
+ const clientCatalog = loadClientCatalog();
139
+ const clientIds = clientCatalog.providers.map((p) => p.id);
140
+
141
+ expect(clientIds).toEqual([...daemonIds]);
142
+ });
143
+
144
+ // -----------------------------------------------------------------------
145
+ // Credential metadata parity
146
+ // -----------------------------------------------------------------------
147
+
148
+ test("each client catalog entry credentialMode matches its daemon counterpart", () => {
149
+ const clientCatalog = loadClientCatalog();
150
+ const violations: string[] = [];
151
+
152
+ for (const clientEntry of clientCatalog.providers) {
153
+ try {
154
+ const daemonEntry = getCatalogProvider(clientEntry.id as TtsProviderId);
155
+ const expected = deriveCredentialMetadata(daemonEntry);
156
+ if (clientEntry.credentialMode !== expected.credentialMode) {
157
+ violations.push(
158
+ `Provider "${clientEntry.id}": client credentialMode="${clientEntry.credentialMode}" ` +
159
+ `!= expected "${expected.credentialMode}" (derived from daemon credentialStoreKey)`,
160
+ );
161
+ }
162
+ } catch {
163
+ // Unknown ID — covered by the provider ID parity test above.
164
+ }
165
+ }
166
+
167
+ if (violations.length > 0) {
168
+ const message = [
169
+ "Credential mode mismatch between daemon and client TTS catalogs.",
170
+ "",
171
+ "Violations:",
172
+ ...violations.map((v) => ` - ${v}`),
173
+ "",
174
+ "Update meta/tts-provider-catalog.json or assistant/src/tts/provider-catalog.ts to match.",
175
+ ].join("\n");
176
+ expect(violations, message).toEqual([]);
177
+ }
178
+ });
179
+
180
+ test("api-key providers: client apiKeyProviderName matches daemon credentialStoreKey", () => {
181
+ const clientCatalog = loadClientCatalog();
182
+ const violations: string[] = [];
183
+
184
+ for (const clientEntry of clientCatalog.providers) {
185
+ if (clientEntry.credentialMode !== "api-key") continue;
186
+
187
+ try {
188
+ const daemonEntry = getCatalogProvider(clientEntry.id as TtsProviderId);
189
+ const expected = deriveCredentialMetadata(daemonEntry);
190
+ if (clientEntry.apiKeyProviderName !== expected.apiKeyProviderName) {
191
+ violations.push(
192
+ `Provider "${clientEntry.id}": client apiKeyProviderName="${clientEntry.apiKeyProviderName}" ` +
193
+ `!= expected "${expected.apiKeyProviderName}" (derived from daemon credentialStoreKey)`,
194
+ );
195
+ }
196
+ } catch {
197
+ // Unknown ID — covered by the provider ID parity test above.
198
+ }
199
+ }
200
+
201
+ if (violations.length > 0) {
202
+ const message = [
203
+ "apiKeyProviderName mismatch between daemon and client TTS catalogs.",
204
+ "",
205
+ "Violations:",
206
+ ...violations.map((v) => ` - ${v}`),
207
+ "",
208
+ "Update meta/tts-provider-catalog.json or assistant/src/tts/provider-catalog.ts to match.",
209
+ ].join("\n");
210
+ expect(violations, message).toEqual([]);
211
+ }
212
+ });
213
+
214
+ test("credential providers: client credentialNamespace matches daemon credentialStoreKey namespace", () => {
215
+ const clientCatalog = loadClientCatalog();
216
+ const violations: string[] = [];
217
+
218
+ for (const clientEntry of clientCatalog.providers) {
219
+ if (clientEntry.credentialMode !== "credential") continue;
220
+
221
+ try {
222
+ const daemonEntry = getCatalogProvider(clientEntry.id as TtsProviderId);
223
+ const expected = deriveCredentialMetadata(daemonEntry);
224
+ if (clientEntry.credentialNamespace !== expected.credentialNamespace) {
225
+ violations.push(
226
+ `Provider "${clientEntry.id}": client credentialNamespace="${clientEntry.credentialNamespace}" ` +
227
+ `!= expected "${expected.credentialNamespace}" (derived from daemon credentialStoreKey)`,
228
+ );
229
+ }
230
+ } catch {
231
+ // Unknown ID — covered by the provider ID parity test above.
232
+ }
233
+ }
234
+
235
+ if (violations.length > 0) {
236
+ const message = [
237
+ "credentialNamespace mismatch between daemon and client TTS catalogs.",
238
+ "",
239
+ "Violations:",
240
+ ...violations.map((v) => ` - ${v}`),
241
+ "",
242
+ "Update meta/tts-provider-catalog.json or assistant/src/tts/provider-catalog.ts to match.",
243
+ ].join("\n");
244
+ expect(violations, message).toEqual([]);
245
+ }
246
+ });
247
+
248
+ // -----------------------------------------------------------------------
249
+ // Display name parity
250
+ // -----------------------------------------------------------------------
251
+
252
+ test("each client catalog entry displayName matches its daemon counterpart", () => {
253
+ const clientCatalog = loadClientCatalog();
254
+ const violations: string[] = [];
255
+
256
+ for (const clientEntry of clientCatalog.providers) {
257
+ try {
258
+ const daemonEntry = getCatalogProvider(clientEntry.id as TtsProviderId);
259
+ if (clientEntry.displayName !== daemonEntry.displayName) {
260
+ violations.push(
261
+ `Provider "${clientEntry.id}": client displayName="${clientEntry.displayName}" ` +
262
+ `!= daemon displayName="${daemonEntry.displayName}"`,
263
+ );
264
+ }
265
+ } catch {
266
+ // Unknown ID — covered by the provider ID parity test above.
267
+ }
268
+ }
269
+
270
+ if (violations.length > 0) {
271
+ const message = [
272
+ "Display name mismatch between daemon and client TTS catalogs.",
273
+ "",
274
+ "Violations:",
275
+ ...violations.map((v) => ` - ${v}`),
276
+ "",
277
+ "Update meta/tts-provider-catalog.json or assistant/src/tts/provider-catalog.ts to match.",
278
+ ].join("\n");
279
+ expect(violations, message).toEqual([]);
280
+ }
281
+ });
282
+
283
+ // -----------------------------------------------------------------------
284
+ // Structural sanity
285
+ // -----------------------------------------------------------------------
286
+
287
+ test("client catalog JSON has a version field", () => {
288
+ const clientCatalog = loadClientCatalog();
289
+ expect(typeof clientCatalog.version).toBe("number");
290
+ expect(clientCatalog.version).toBeGreaterThanOrEqual(1);
291
+ });
292
+
293
+ test("client catalog has at least one provider", () => {
294
+ const clientCatalog = loadClientCatalog();
295
+ expect(clientCatalog.providers.length).toBeGreaterThan(0);
296
+ });
297
+
298
+ test("every client catalog entry has required fields", () => {
299
+ const clientCatalog = loadClientCatalog();
300
+ const violations: string[] = [];
301
+
302
+ for (const entry of clientCatalog.providers) {
303
+ if (!entry.id || typeof entry.id !== "string") {
304
+ violations.push(`Entry missing or invalid 'id'`);
305
+ }
306
+ if (!entry.displayName || typeof entry.displayName !== "string") {
307
+ violations.push(`${entry.id}: missing or invalid 'displayName'`);
308
+ }
309
+ if (!entry.setupMode || typeof entry.setupMode !== "string") {
310
+ violations.push(`${entry.id}: missing or invalid 'setupMode'`);
311
+ }
312
+ if (
313
+ !entry.credentialMode ||
314
+ !["api-key", "credential"].includes(entry.credentialMode)
315
+ ) {
316
+ violations.push(
317
+ `${entry.id}: missing or invalid 'credentialMode' (expected "api-key" or "credential")`,
318
+ );
319
+ }
320
+ if (entry.credentialMode === "api-key" && !entry.apiKeyProviderName) {
321
+ violations.push(
322
+ `${entry.id}: credentialMode is "api-key" but apiKeyProviderName is missing`,
323
+ );
324
+ }
325
+ if (entry.credentialMode === "credential" && !entry.credentialNamespace) {
326
+ violations.push(
327
+ `${entry.id}: credentialMode is "credential" but credentialNamespace is missing`,
328
+ );
329
+ }
330
+ if (!entry.credentialsGuide || !entry.credentialsGuide.url) {
331
+ violations.push(`${entry.id}: missing or invalid 'credentialsGuide'`);
332
+ }
333
+ }
334
+
335
+ if (violations.length > 0) {
336
+ const message = [
337
+ "Client catalog entries have missing or invalid required fields.",
338
+ "",
339
+ "Violations:",
340
+ ...violations.map((v) => ` - ${v}`),
341
+ ].join("\n");
342
+ expect(violations, message).toEqual([]);
343
+ }
344
+ });
345
+ });