@vellumai/assistant 0.10.9 → 0.10.10-dev.202607170046.8482d40

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 (496) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +8 -0
  3. package/docs/activation-funnel-telemetry.md +13 -7
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +26 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +16 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +26 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +16 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  14. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +26 -0
  16. package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  17. package/node_modules/@vellumai/service-contracts/src/ingress.ts +16 -0
  18. package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  19. package/openapi.yaml +678 -107
  20. package/package.json +1 -1
  21. package/scripts/generate-openapi.ts +8 -0
  22. package/src/__tests__/activation-early-marking.test.ts +6 -5
  23. package/src/__tests__/agent-loop-override-profile.test.ts +22 -25
  24. package/src/__tests__/agent-wake-override-profile.test.ts +21 -48
  25. package/src/__tests__/app-builder-tool-scripts.test.ts +0 -1
  26. package/src/__tests__/app-bundler.test.ts +4 -12
  27. package/src/__tests__/app-executors.test.ts +2 -49
  28. package/src/__tests__/app-routes-csp.test.ts +114 -146
  29. package/src/__tests__/auth-fallback-events-store.test.ts +8 -1
  30. package/src/__tests__/build-persisted-content.test.ts +96 -0
  31. package/src/__tests__/bundle-scanner.test.ts +27 -1
  32. package/src/__tests__/call-controller.test.ts +291 -0
  33. package/src/__tests__/call-site-routing-connection-auto-resolve.test.ts +165 -0
  34. package/src/__tests__/chat-credential-redaction.test.ts +1395 -0
  35. package/src/__tests__/chat-reveal-guard-priming.test.ts +791 -0
  36. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  37. package/src/__tests__/compactor-image-manifest-trust.test.ts +50 -0
  38. package/src/__tests__/config-loader-backfill.test.ts +9 -4
  39. package/src/__tests__/config-schema-cmd.test.ts +10 -11
  40. package/src/__tests__/config-schema.test.ts +190 -257
  41. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +208 -0
  42. package/src/__tests__/conversation-agent-loop-overflow.test.ts +5 -19
  43. package/src/__tests__/conversation-agent-loop.test.ts +22 -9
  44. package/src/__tests__/conversation-error.test.ts +31 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +110 -0
  46. package/src/__tests__/conversation-process-callsite.test.ts +12 -19
  47. package/src/__tests__/conversation-routes-slash-commands.test.ts +7 -21
  48. package/src/__tests__/conversation-summarize-route.test.ts +36 -44
  49. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  50. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +7 -7
  51. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +0 -1
  52. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -1
  53. package/src/__tests__/conversation-usage.test.ts +4 -12
  54. package/src/__tests__/credential-prompt-route.test.ts +137 -0
  55. package/src/__tests__/credential-routes.test.ts +355 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +5 -0
  57. package/src/__tests__/db-migration-rollback.test.ts +22 -0
  58. package/src/__tests__/empty-response-hook.test.ts +186 -1
  59. package/src/__tests__/external-plugin-loader.test.ts +11 -5
  60. package/src/__tests__/heartbeat-service.test.ts +0 -28
  61. package/src/__tests__/host-shell-tool.test.ts +2 -0
  62. package/src/__tests__/inactive-tool-error-messages.test.ts +2 -2
  63. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +52 -8
  64. package/src/__tests__/internal-telemetry-routes.test.ts +23 -4
  65. package/src/__tests__/invite-routes-http.test.ts +12 -16
  66. package/src/__tests__/list-all-apps.test.ts +0 -4
  67. package/src/__tests__/llm-context-resolution.test.ts +32 -56
  68. package/src/__tests__/llm-request-log-turn-query.test.ts +109 -0
  69. package/src/__tests__/llm-resolver-override-or-default.test.ts +3 -52
  70. package/src/__tests__/llm-resolver.test.ts +342 -602
  71. package/src/__tests__/llm-schema.test.ts +79 -37
  72. package/src/__tests__/max-tokens-continue-hook.test.ts +19 -0
  73. package/src/__tests__/media-stream-output.test.ts +259 -3
  74. package/src/__tests__/media-stream-server-integration.test.ts +22 -1
  75. package/src/__tests__/media-stream-stt-session.test.ts +47 -0
  76. package/src/__tests__/memory-jobs-worker-cleanup-cadence.test.ts +33 -0
  77. package/src/__tests__/memory-recall-log-store.test.ts +47 -13
  78. package/src/__tests__/mock-gateway-ipc.ts +46 -1
  79. package/src/__tests__/mtime-cache.test.ts +61 -0
  80. package/src/__tests__/navigate-settings-tab.test.ts +2 -0
  81. package/src/__tests__/normalize-onboarding.test.ts +33 -0
  82. package/src/__tests__/onboarding-persona-write.test.ts +26 -0
  83. package/src/__tests__/plugin-api-resolve-credential.test.ts +140 -0
  84. package/src/__tests__/plugin-app-serve-routes.test.ts +166 -14
  85. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +2 -0
  86. package/src/__tests__/post-turn-tool-result-truncation.test.ts +38 -0
  87. package/src/__tests__/provider-commit-message-generator.test.ts +27 -20
  88. package/src/__tests__/provider-connections-backfill.test.ts +138 -0
  89. package/src/__tests__/provider-platform-proxy-integration.test.ts +10 -31
  90. package/src/__tests__/provider-registry-ollama.test.ts +8 -18
  91. package/src/__tests__/provider-send-message-override-profile.test.ts +23 -23
  92. package/src/__tests__/provider-usage-tracking.test.ts +9 -19
  93. package/src/__tests__/prune-old-conversations-job.test.ts +12 -0
  94. package/src/__tests__/published-app-updater.test.ts +22 -16
  95. package/src/__tests__/registry.test.ts +5 -16
  96. package/src/__tests__/retry-openrouter-only-normalization.test.ts +22 -23
  97. package/src/__tests__/retry-thinking-adaptive-only.test.ts +43 -48
  98. package/src/__tests__/retry-thinking-tool-choice.test.ts +57 -66
  99. package/src/__tests__/retry-verbosity-normalization.test.ts +24 -23
  100. package/src/__tests__/reveal-success-registry.test.ts +123 -0
  101. package/src/__tests__/run-conversation-turn-persistence.test.ts +130 -0
  102. package/src/__tests__/secret-fixtures.ts +9 -0
  103. package/src/__tests__/secret-routes-acp-guard.test.ts +77 -0
  104. package/src/__tests__/server-history-render.test.ts +48 -0
  105. package/src/__tests__/skills.test.ts +9 -4
  106. package/src/__tests__/slack-share-routes.test.ts +0 -1
  107. package/src/__tests__/stt-stream-session.test.ts +6 -5
  108. package/src/__tests__/subagent-call-site-routing.test.ts +69 -95
  109. package/src/__tests__/subagent-disposal.test.ts +2 -0
  110. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  111. package/src/__tests__/subagent-fork-spawn.test.ts +2 -0
  112. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  113. package/src/__tests__/subagent-role-registry.test.ts +37 -0
  114. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  115. package/src/__tests__/subagent-terminal-message.test.ts +50 -0
  116. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -5
  117. package/src/__tests__/surface-completion-nudge-hook.test.ts +19 -0
  118. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  119. package/src/__tests__/telemetry-routes.test.ts +99 -19
  120. package/src/__tests__/tool-audit.test.ts +34 -4
  121. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  122. package/src/__tests__/tool-profiler.test.ts +72 -1
  123. package/src/__tests__/tool-result-spool.test.ts +49 -4
  124. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -1
  125. package/src/__tests__/ui-channel-variants.test.ts +108 -0
  126. package/src/__tests__/ui-shape-teaching.test.ts +255 -0
  127. package/src/__tests__/usage-attribution.test.ts +18 -41
  128. package/src/__tests__/user-plugin-loader.test.ts +4 -4
  129. package/src/__tests__/voice-config-update.test.ts +46 -0
  130. package/src/__tests__/voice-session-bridge.test.ts +216 -84
  131. package/src/__tests__/weak-open-model.test.ts +8 -1
  132. package/src/__tests__/workspace-migration-131-drop-web-fetch-mode.test.ts +120 -0
  133. package/src/acp/__tests__/acp-claude-oauth.test.ts +206 -0
  134. package/src/acp/__tests__/acp-connect-card-state.test.ts +23 -0
  135. package/src/acp/__tests__/acp-credentials.test.ts +87 -0
  136. package/src/acp/__tests__/prepare-agent-env.test.ts +161 -1
  137. package/src/acp/acp-claude-oauth.ts +144 -0
  138. package/src/acp/acp-connect-card-state.ts +39 -0
  139. package/src/acp/acp-credentials.ts +67 -0
  140. package/src/acp/prepare-agent-env.ts +103 -6
  141. package/src/agent/loop.ts +8 -3
  142. package/src/api/events/open-conversation.test.ts +64 -0
  143. package/src/api/events/open-conversation.ts +33 -0
  144. package/src/api/events/tool-result.ts +8 -0
  145. package/src/api/index.ts +6 -0
  146. package/src/api/responses/conversation-message.ts +14 -0
  147. package/src/apps/app-store.ts +25 -35
  148. package/src/bundler/app-bundler.ts +34 -48
  149. package/src/bundler/app-compiler.ts +39 -4
  150. package/src/bundler/bundle-scanner.ts +13 -0
  151. package/src/bundler/manifest.ts +1 -1
  152. package/src/calls/__tests__/voice-session-bridge.test.ts +47 -0
  153. package/src/calls/call-constants.ts +5 -0
  154. package/src/calls/call-controller.ts +100 -32
  155. package/src/calls/call-transport.ts +9 -0
  156. package/src/calls/media-stream-output.ts +107 -4
  157. package/src/calls/media-stream-server.ts +29 -9
  158. package/src/calls/media-stream-stt-session.ts +7 -1
  159. package/src/calls/media-turn-detector.ts +11 -1
  160. package/src/calls/voice-session-bridge.ts +84 -63
  161. package/src/cli/commands/__tests__/inference-providers.test.ts +270 -33
  162. package/src/cli/commands/__tests__/inference-send.test.ts +50 -0
  163. package/src/cli/commands/__tests__/notifications.test.ts +24 -3
  164. package/src/cli/commands/config.help.ts +6 -6
  165. package/src/cli/commands/credentials.help.ts +13 -0
  166. package/src/cli/commands/credentials.ts +23 -1
  167. package/src/cli/commands/email.help.ts +7 -0
  168. package/src/cli/commands/email.ts +35 -1
  169. package/src/cli/commands/inference-providers.ts +168 -107
  170. package/src/cli/commands/inference.help.ts +118 -46
  171. package/src/cli/commands/inference.ts +6 -0
  172. package/src/cli/commands/memory/index.help.ts +23 -0
  173. package/src/cli/commands/memory/nodes.ts +146 -0
  174. package/src/cli/commands/notifications.help.ts +10 -10
  175. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +40 -0
  176. package/src/cli/commands/oauth/connect-surface-guidance.ts +54 -0
  177. package/src/cli/commands/oauth/connect.test.ts +126 -0
  178. package/src/cli/commands/oauth/connect.ts +29 -6
  179. package/src/cli/commands/oauth/index.help.ts +7 -1
  180. package/src/cli/commands/oauth/status.test.ts +69 -3
  181. package/src/cli/commands/oauth/status.ts +50 -12
  182. package/src/cli/commands/plugins.help.ts +13 -2
  183. package/src/cli/commands/plugins.ts +61 -7
  184. package/src/cli/commands/telemetry.help.ts +13 -0
  185. package/src/cli/commands/telemetry.ts +45 -5
  186. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +8 -2
  187. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +213 -9
  188. package/src/cli/lib/bundled-marketplace.json +93 -36
  189. package/src/cli/lib/inspect-plugin.ts +5 -1
  190. package/src/cli/lib/install-from-github.ts +68 -4
  191. package/src/cli/lib/plugin-catalog-local.ts +6 -2
  192. package/src/cli/lib/plugin-fingerprint.ts +3 -3
  193. package/src/cli/lib/upgrade-plugin.ts +236 -35
  194. package/src/config/__tests__/default-profile-catalog.test.ts +8 -12
  195. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +137 -0
  196. package/src/config/__tests__/profile-materialization.test.ts +1 -88
  197. package/src/config/bundled-skills/AGENTS.md +3 -30
  198. package/src/config/bundled-skills/acp/SKILL.md +11 -3
  199. package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
  200. package/src/config/bundled-skills/app-builder/TOOLS.json +23 -0
  201. package/src/config/bundled-skills/app-builder/tools/app-open.ts +32 -0
  202. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
  203. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +7 -7
  204. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  205. package/src/config/bundled-skills/schedule/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/settings/TOOLS.json +3 -1
  207. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +2 -0
  208. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +18 -10
  209. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  210. package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
  211. package/src/config/call-site-defaults.ts +3 -3
  212. package/src/config/feature-flag-registry.json +24 -39
  213. package/src/config/llm-resolver.ts +88 -475
  214. package/src/config/profile-materialization.ts +11 -11
  215. package/src/config/schema.ts +93 -127
  216. package/src/config/schemas/__tests__/live-voice.test.ts +24 -6
  217. package/src/config/schemas/__tests__/stt.test.ts +31 -3
  218. package/src/config/schemas/live-voice.ts +10 -4
  219. package/src/config/schemas/llm.ts +52 -15
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrospective.ts +8 -0
  222. package/src/config/schemas/memory-v2.ts +2 -2
  223. package/src/config/schemas/memory-v3.ts +1 -1
  224. package/src/config/schemas/services.ts +6 -3
  225. package/src/config/schemas/stt.ts +38 -21
  226. package/src/config/schemas/tts.ts +13 -18
  227. package/src/config/skills.ts +31 -21
  228. package/src/context/compactor.ts +44 -7
  229. package/src/context/post-turn-tool-result-truncation.ts +4 -2
  230. package/src/context/tool-result-spool.ts +26 -5
  231. package/src/conversations/__tests__/message-consolidation.test.ts +48 -0
  232. package/src/conversations/message-consolidation.ts +22 -2
  233. package/src/credential-execution/prompted-credential.ts +18 -0
  234. package/src/daemon/__tests__/conversation-tool-setup.test.ts +5 -12
  235. package/src/daemon/app-source-watcher.ts +17 -23
  236. package/src/daemon/chat-credential-redaction.ts +1365 -0
  237. package/src/daemon/conversation-agent-loop-handlers.ts +640 -34
  238. package/src/daemon/conversation-agent-loop.ts +71 -31
  239. package/src/daemon/conversation-error.ts +36 -14
  240. package/src/daemon/conversation-process.ts +22 -0
  241. package/src/daemon/conversation-store.ts +35 -0
  242. package/src/daemon/conversation-surfaces.ts +25 -25
  243. package/src/daemon/conversation-tool-setup.ts +33 -7
  244. package/src/daemon/conversation.ts +55 -5
  245. package/src/daemon/handlers/shared.ts +40 -2
  246. package/src/daemon/lifecycle.ts +4 -4
  247. package/src/daemon/message-types/conversations.ts +6 -17
  248. package/src/daemon/providers-setup.ts +8 -0
  249. package/src/daemon/tool-setup-types.ts +7 -1
  250. package/src/daemon/wake-conversation-ops.ts +10 -1
  251. package/src/hooks/types.ts +9 -0
  252. package/src/ipc/__tests__/email-ipc.test.ts +90 -0
  253. package/src/ipc/gateway-client.test.ts +59 -0
  254. package/src/ipc/gateway-client.ts +70 -27
  255. package/src/live-voice/__tests__/live-voice-events.test.ts +14 -2
  256. package/src/live-voice/__tests__/live-voice-integration.test.ts +116 -2
  257. package/src/live-voice/__tests__/live-voice-vad.test.ts +804 -13
  258. package/src/live-voice/__tests__/protocol.test.ts +122 -0
  259. package/src/live-voice/live-voice-session.ts +443 -40
  260. package/src/live-voice/protocol.ts +143 -1
  261. package/src/monitoring/__tests__/plugin-source-watch.test.ts +3 -1
  262. package/src/monitoring/plugin-source-watch.ts +3 -62
  263. package/src/notifications/README.md +1 -1
  264. package/src/permissions/checker.ts +8 -4
  265. package/src/persistence/__tests__/db-init-migrations-ok.test.ts +26 -0
  266. package/src/persistence/conversation-crud.ts +104 -2
  267. package/src/persistence/db-init.ts +14 -3
  268. package/src/persistence/job-handlers/cleanup.ts +15 -7
  269. package/src/persistence/llm-request-log-store.ts +88 -52
  270. package/src/persistence/migrations/298-move-memory-jobs-to-memory-db.ts +7 -31
  271. package/src/persistence/migrations/305-drop-contact-acl-columns.ts +3 -2
  272. package/src/persistence/migrations/326-move-injection-events-to-memory-db.ts +8 -34
  273. package/src/persistence/migrations/336-move-memory-v2-activation-logs-to-memory-db.ts +90 -0
  274. package/src/persistence/migrations/337-move-memory-recall-logs-to-memory-db.ts +114 -0
  275. package/src/persistence/migrations/338-move-memory-v3-selections-to-memory-db.ts +84 -0
  276. package/src/persistence/migrations/339-move-activation-sessions-to-memory-db.ts +52 -0
  277. package/src/persistence/migrations/__tests__/run-migrations.test.ts +155 -0
  278. package/src/persistence/migrations/helpers/relocation.ts +44 -1
  279. package/src/persistence/migrations/run-migrations.ts +25 -1
  280. package/src/persistence/schema/infrastructure.ts +9 -0
  281. package/src/persistence/schema/memory-core.ts +3 -0
  282. package/src/persistence/schema/memory-injection.ts +2 -0
  283. package/src/persistence/steps.ts +36 -0
  284. package/src/platform/client.test.ts +1 -44
  285. package/src/platform/client.ts +10 -20
  286. package/src/platform/consent-cache.test.ts +89 -23
  287. package/src/platform/consent-cache.ts +71 -33
  288. package/src/plugin-api/constants.ts +12 -0
  289. package/src/plugin-api/conversation-turn.ts +37 -14
  290. package/src/plugin-api/index.ts +11 -1
  291. package/src/plugin-api/resolve-credential.ts +75 -0
  292. package/src/plugin-api/vision-support.test.ts +8 -19
  293. package/src/plugin-api/vision-support.ts +26 -17
  294. package/src/plugins/collect-source-versions.ts +77 -0
  295. package/src/plugins/defaults/compaction/compact.ts +6 -0
  296. package/src/plugins/defaults/compaction/window-manager.ts +9 -0
  297. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +18 -24
  298. package/src/plugins/defaults/empty-response/hooks/user-prompt-submit.ts +38 -0
  299. package/src/plugins/defaults/empty-response/refusal-quarantine.ts +99 -0
  300. package/src/plugins/defaults/image-fallback/__tests__/caption-cache-persistence.test.ts +7 -3
  301. package/src/plugins/defaults/index.ts +8 -1
  302. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +4 -1
  303. package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +48 -6
  304. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +28 -22
  305. package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +148 -0
  306. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +80 -8
  307. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +202 -0
  308. package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +64 -25
  309. package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +24 -11
  310. package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +70 -6
  311. package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +278 -0
  312. package/src/plugins/defaults/memory/activation-session-store.ts +27 -20
  313. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -9
  314. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -8
  315. package/src/plugins/defaults/memory/graph/retriever.test.ts +6 -6
  316. package/src/plugins/defaults/memory/graph/store.ts +114 -0
  317. package/src/plugins/defaults/memory/graph/tool-handlers.ts +36 -1
  318. package/src/plugins/defaults/memory/graph/tools.ts +47 -13
  319. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +16 -35
  320. package/src/plugins/defaults/memory/jobs-worker.ts +10 -1
  321. package/src/plugins/defaults/memory/memory-db.ts +3 -2
  322. package/src/plugins/defaults/memory/memory-recall-log-store.ts +129 -66
  323. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +8 -0
  324. package/src/plugins/defaults/memory/memory-retrospective-job.ts +11 -122
  325. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +216 -0
  326. package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +84 -46
  327. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +42 -32
  328. package/src/plugins/defaults/memory/path-containment.ts +21 -0
  329. package/src/plugins/defaults/memory/prompt-override.ts +51 -13
  330. package/src/plugins/defaults/memory/tools.test.ts +34 -0
  331. package/src/plugins/defaults/memory/tools.ts +12 -2
  332. package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +19 -15
  333. package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +24 -19
  334. package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +19 -15
  335. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +22 -2
  336. package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +5 -3
  337. package/src/plugins/defaults/memory/v2/harness/oracle.ts +59 -41
  338. package/src/plugins/defaults/memory/v2/harness/replay-input.ts +29 -25
  339. package/src/plugins/defaults/memory/v2/migration.ts +46 -16
  340. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +4 -0
  341. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +17 -15
  342. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +24 -22
  343. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +9 -5
  344. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +277 -10
  345. package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +57 -5
  346. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +9 -7
  347. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +38 -7
  348. package/src/plugins/defaults/memory/v3/hot-set.test.ts +36 -18
  349. package/src/plugins/defaults/memory/v3/hot-set.ts +12 -14
  350. package/src/plugins/defaults/memory/v3/learned-edges.test.ts +47 -27
  351. package/src/plugins/defaults/memory/v3/learned-edges.ts +12 -14
  352. package/src/plugins/defaults/memory/v3/orchestrate.ts +139 -22
  353. package/src/plugins/defaults/memory/v3/prune.test.ts +10 -3
  354. package/src/plugins/defaults/memory/v3/prune.ts +17 -11
  355. package/src/plugins/defaults/memory/v3/selection-log-store.ts +23 -11
  356. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +70 -53
  357. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +9 -6
  358. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +8 -6
  359. package/src/plugins/external-plugin-loader.ts +15 -15
  360. package/src/plugins/mtime-cache.ts +47 -0
  361. package/src/plugins/pipeline.ts +9 -1
  362. package/src/plugins/plugin-execution-context.ts +44 -0
  363. package/src/plugins/plugin-tree-walk.ts +31 -24
  364. package/src/plugins/source-fingerprint.ts +3 -4
  365. package/src/prompts/normalize-onboarding.ts +12 -0
  366. package/src/prompts/persona-resolver.ts +8 -0
  367. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +1 -1
  368. package/src/providers/__tests__/dispatch-connection-routing.test.ts +6 -9
  369. package/src/providers/__tests__/registry-native-web-search.test.ts +4 -10
  370. package/src/providers/__tests__/retry-callsite.test.ts +235 -215
  371. package/src/providers/__tests__/satellite-connection-routing.test.ts +8 -16
  372. package/src/providers/anthropic/client.ts +1 -0
  373. package/src/providers/atlascloud/client.ts +10 -49
  374. package/src/providers/baseten/client.ts +43 -0
  375. package/src/providers/call-site-routing.ts +26 -13
  376. package/src/providers/connection-resolution.ts +9 -11
  377. package/src/providers/fetch-provider-catalog.ts +4 -2
  378. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +29 -4
  379. package/src/providers/inference/__tests__/connection-availability-keyless.test.ts +78 -0
  380. package/src/providers/inference/adapter-factory.ts +27 -6
  381. package/src/providers/inference/auth.ts +29 -1
  382. package/src/providers/inference/backfill.ts +61 -10
  383. package/src/providers/inference/connection-availability.ts +3 -2
  384. package/src/providers/inference/resolve-auth.ts +9 -1
  385. package/src/providers/model-catalog.ts +49 -0
  386. package/src/providers/openai/__tests__/api-error-normalization.test.ts +24 -2
  387. package/src/providers/openai/__tests__/chat-completions-provider-endpoint.test.ts +159 -0
  388. package/src/providers/openai/api-key-validation.ts +70 -0
  389. package/src/providers/openai/chat-completions-provider.ts +1 -0
  390. package/src/providers/openai/responses-provider.ts +1 -0
  391. package/src/providers/retry.ts +2 -0
  392. package/src/providers/types.ts +18 -4
  393. package/src/providers/vellum-model-routing.test.ts +26 -0
  394. package/src/providers/vellum-model-routing.ts +30 -0
  395. package/src/providers/voice-error-copy.ts +47 -0
  396. package/src/providers/weak-open-model.ts +14 -11
  397. package/src/runtime/agent-wake.ts +40 -24
  398. package/src/runtime/for-chat-mint-registry.ts +118 -0
  399. package/src/runtime/reveal-nonce.ts +49 -0
  400. package/src/runtime/reveal-success-registry.ts +306 -0
  401. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +408 -0
  402. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +88 -3
  403. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +161 -30
  404. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +97 -0
  405. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +7 -0
  406. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +15 -0
  407. package/src/runtime/routes/acp-claude-auth-routes.ts +389 -0
  408. package/src/runtime/routes/app-management-routes.ts +152 -45
  409. package/src/runtime/routes/app-routes.ts +16 -77
  410. package/src/runtime/routes/canned-message-complete.ts +16 -16
  411. package/src/runtime/routes/conversation-management-routes.ts +17 -14
  412. package/src/runtime/routes/conversation-query-routes.ts +54 -6
  413. package/src/runtime/routes/conversation-routes.ts +48 -2
  414. package/src/runtime/routes/credential-prompt-routes.ts +45 -0
  415. package/src/runtime/routes/credential-routes.ts +134 -3
  416. package/src/runtime/routes/email-routes.ts +17 -1
  417. package/src/runtime/routes/errors.ts +2 -2
  418. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +33 -5
  419. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +6 -5
  420. package/src/runtime/routes/index.ts +2 -0
  421. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -28
  422. package/src/runtime/routes/inference-send-routes.ts +17 -1
  423. package/src/runtime/routes/internal-telemetry-routes.ts +7 -6
  424. package/src/runtime/routes/plugins-routes.ts +37 -6
  425. package/src/runtime/routes/publish-routes.ts +15 -18
  426. package/src/runtime/routes/schedule-worker-routes.ts +9 -0
  427. package/src/runtime/routes/secret-routes.ts +29 -0
  428. package/src/runtime/routes/telemetry-routes.ts +149 -45
  429. package/src/schedule/__tests__/schedule-timezone.test.ts +101 -0
  430. package/src/schedule/__tests__/worker-watchdog.test.ts +209 -0
  431. package/src/schedule/schedule-store.ts +12 -1
  432. package/src/schedule/schedule-timezone.ts +63 -0
  433. package/src/schedule/scheduler.ts +100 -6
  434. package/src/schedule/worker-control.ts +19 -0
  435. package/src/security/__tests__/oauth2-exchange-body.test.ts +123 -0
  436. package/src/security/auth-fallback-events-store.ts +7 -6
  437. package/src/security/oauth2.ts +37 -9
  438. package/src/security/secret-scanner.ts +26 -1
  439. package/src/services/published-app-updater.ts +6 -11
  440. package/src/stt/stt-stream-session.ts +36 -16
  441. package/src/subagent/manager.ts +34 -8
  442. package/src/telemetry/AGENTS.md +30 -1
  443. package/src/telemetry/__tests__/config-setting-snapshot.test.ts +18 -0
  444. package/src/telemetry/__tests__/outbox-test-harness.ts +5 -3
  445. package/src/telemetry/config-setting-snapshot.ts +44 -10
  446. package/src/telemetry/telemetry-event-sources.test.ts +124 -30
  447. package/src/telemetry/telemetry-event-sources.ts +137 -83
  448. package/src/telemetry/telemetry-events-outbox.test.ts +46 -1
  449. package/src/telemetry/telemetry-events-outbox.ts +60 -9
  450. package/src/telemetry/telemetry-wire-source.json +1 -1
  451. package/src/telemetry/telemetry-wire-validation.ts +39 -2
  452. package/src/telemetry/telemetry-wire.generated.ts +8 -0
  453. package/src/telemetry/tool-audit.ts +15 -9
  454. package/src/telemetry/tool-executed-events-store.test.ts +1 -1
  455. package/src/telemetry/turn-events-store.ts +20 -0
  456. package/src/telemetry/types.ts +45 -12
  457. package/src/telemetry/usage-telemetry-reporter.test.ts +294 -25
  458. package/src/telemetry/usage-telemetry-reporter.ts +117 -18
  459. package/src/telemetry/watchdog-direct-emit.test.ts +11 -3
  460. package/src/telemetry/watchdog-direct-emit.ts +11 -6
  461. package/src/tools/acp/spawn.test.ts +17 -0
  462. package/src/tools/acp/spawn.ts +31 -2
  463. package/src/tools/apps/executors.ts +11 -36
  464. package/src/tools/executor.ts +14 -1
  465. package/src/tools/host-terminal/host-shell.ts +6 -2
  466. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +1 -1
  467. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +25 -0
  468. package/src/tools/network/web-fetch.ts +6 -2
  469. package/src/tools/skills/sandbox-runner.ts +5 -2
  470. package/src/tools/subagent/spawn.ts +7 -11
  471. package/src/tools/terminal/shell.ts +5 -2
  472. package/src/tools/tool-manifest.ts +0 -2
  473. package/src/tools/tool-profiler.ts +37 -6
  474. package/src/tools/types.ts +8 -0
  475. package/src/tools/ui-surface/channel-variants.ts +101 -0
  476. package/src/tools/ui-surface/definitions.ts +23 -67
  477. package/src/tools/ui-surface/surface-shape-docs.ts +229 -0
  478. package/src/tts/__tests__/provider-adapters.test.ts +18 -0
  479. package/src/tts/provider-catalog.ts +2 -4
  480. package/src/tts/providers/deepgram-provider.ts +10 -1
  481. package/src/types/onboarding-context.ts +8 -0
  482. package/src/usage/attribution.ts +18 -112
  483. package/src/{config/bundled-skills/messaging/tools/gmail-mime-helpers.ts → util/mime-type.ts} +4 -1
  484. package/src/util/provider-error-patterns.ts +7 -1
  485. package/src/util/worker-process.ts +105 -0
  486. package/src/watcher/__tests__/telemetry.test.ts +17 -4
  487. package/src/watcher/telemetry.ts +5 -5
  488. package/src/workspace/migrations/131-drop-web-fetch-mode.ts +61 -0
  489. package/src/workspace/migrations/registry.ts +2 -0
  490. package/src/workspace/provider-commit-message-generator.ts +6 -4
  491. package/src/__tests__/app-open-proxy.test.ts +0 -67
  492. package/src/onboarding/onboarding-research-events-store.test.ts +0 -230
  493. package/src/onboarding/onboarding-research-events-store.ts +0 -104
  494. package/src/runtime/routes/assets/vellum-design-system.css +0 -2236
  495. package/src/tools/apps/definitions.ts +0 -73
  496. package/src/tools/apps/open-proxy.ts +0 -43
package/ARCHITECTURE.md CHANGED
@@ -1944,7 +1944,7 @@ Connected channels are resolved at signal emission time: vellum is always includ
1944
1944
 
1945
1945
  **Audit trail (SQLite):** `notification_events` → `notification_decisions` (with `conversationActions` in validation results) → `notification_deliveries` (with `conversation_id`, `message_id`, `conversation_strategy`, `conversation_action`, `conversation_target_id`, `conversation_fallback_used`)
1946
1946
 
1947
- **Configuration:** `llm.callSites.notificationDecision` (decision engine) and `llm.callSites.preferenceExtraction` (preference extractor) in `config.json`. Both fall back to `llm.default` when unset.
1947
+ **Configuration:** `llm.callSites.notificationDecision` (decision engine) and `llm.callSites.preferenceExtraction` (preference extractor) in `config.json`. Both fall back to their shipped call-site defaults when unset.
1948
1948
 
1949
1949
  ---
1950
1950
 
package/Dockerfile CHANGED
@@ -129,6 +129,14 @@ COPY packages/twilio-client /app/packages/twilio-client
129
129
 
130
130
  COPY assistant /app/assistant
131
131
 
132
+ # Materialize the git-ignored offline plugin manifest from its canonical source.
133
+ # `assistant/src/cli/lib/plugin-catalog-local.ts` statically imports this JSON
134
+ # (loaded even when platform features are enabled), so it must exist in the image
135
+ # — the build installs with --ignore-scripts, so the postinstall sync that
136
+ # regenerates it in dev never runs here. Mirrors how CI materializes the
137
+ # feature-flag registry before building.
138
+ COPY plugins/marketplace.json /app/assistant/src/cli/lib/bundled-marketplace.json
139
+
132
140
  # Install assistant CLI launcher backed by the bundled assistant package
133
141
  RUN printf '#!/usr/bin/env sh\nexec bun run /app/assistant/src/index.ts "$@"\n' > /usr/local/bin/assistant && \
134
142
  chmod +x /usr/local/bin/assistant
@@ -36,7 +36,11 @@ Flow, end to end:
36
36
  `assistant/src/onboarding/onboarding-events-store.ts`:
37
37
  - emission is **deterministic, tied to a `ui_show` surface** — there is no
38
38
  model-facing tool. The model tags the surface it is already rendering for a
39
- rail move with an optional `activation_moment` parameter on `ui_show`; the
39
+ rail move with an optional `activation_moment` parameter on `ui_show` (the
40
+ param is present in ui_show's schema only for activation-rail
41
+ conversations — `injectActivationMomentParam` in
42
+ `tools/ui-surface/channel-variants.ts`, applied at tool resolution when
43
+ `isActivationSession` is true); the
40
44
  daemon captures that tag on the surface's server-side state and records the
41
45
  milestone (gated on `isActivationSession`). **Timing is per-moment**
42
46
  (`ACTIVATION_MOMENT_EMIT_AT` in `activation-funnel.ts`): most moments record
@@ -53,8 +57,9 @@ Flow, end to end:
53
57
  - emission is best-effort (wrapped in try/catch) and never blocks or alters
54
58
  the surface-action flow;
55
59
  - `recordActivationEvent` respects the platform `share_analytics` consent gate
56
- via `getCachedShareAnalytics()` (returns `null` / no row when the platform
57
- user has not consented; default-off when there is no platform session).
60
+ via `getRawShareAnalytics()`: only a confirmed opt-out (`false`) drops the
61
+ event; an `"unknown"` state (cold cache, no platform session) records and
62
+ lets the flush/ingest gates enforce consent before anything ships.
58
63
  2. **Reporter flushes** every ~5 min: `usage-telemetry-reporter.ts`
59
64
  (`REPORT_INTERVAL_MS = 5 * 60 * 1000`, with a one-time
60
65
  `INITIAL_FLUSH_DELAY_MS = 30_000` after startup) POSTs queued onboarding
@@ -180,10 +185,11 @@ the conversation in `activation_sessions` on first build of the system prompt.
180
185
  Two distinct concerns: whether record-time rows reach SQLite (consent gate), and
181
186
  whether those rows reach BigQuery (flush gate, dev-disabled).
182
187
 
183
- 1. `recordActivationEvent` no-ops when the platform `share_analytics` consent is
184
- off (it reads `getCachedShareAnalytics()`), and the consent is **default-off
185
- when there is no platform session**. So the dev session must be signed in to
186
- the platform with `share_analytics` consent enabled, or no rows are written to
188
+ 1. `recordActivationEvent` no-ops only on a confirmed `share_analytics` opt-out
189
+ (it reads `getRawShareAnalytics()`); an `"unknown"` state records, but the
190
+ flush defers until consent resolves. So for rows to actually SHIP, the dev
191
+ session must be signed in to
192
+ the platform with `share_analytics` consent enabled, or no rows reach
187
193
  SQLite. The consent cache is refreshed by `startConsentRefresh()` in
188
194
  `assistant/src/daemon/lifecycle.ts`, which runs **regardless of dev mode** — so
189
195
  a dev session signed in with `share_analytics` consent enabled writes
@@ -15,6 +15,7 @@
15
15
  "./handles": "./src/handles.ts",
16
16
  "./rpc": "./src/rpc.ts",
17
17
  "./attachment-naming": "./src/attachment-naming.ts",
18
+ "./redacted-credential": "./src/redacted-credential.ts",
18
19
  "./error": "./src/error.ts"
19
20
  },
20
21
  "scripts": {
@@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test";
3
3
  import {
4
4
  normalizeHttpPublicBaseUrl,
5
5
  normalizePublicBaseUrl,
6
+ velayHostForPlatformHost,
6
7
  } from "../ingress.js";
7
8
  import {
8
9
  buildTwilioMediaStreamUrl,
@@ -11,6 +12,31 @@ import {
11
12
  resolveTwilioPublicBaseUrl,
12
13
  } from "../twilio-ingress.js";
13
14
 
15
+ describe("velayHostForPlatformHost", () => {
16
+ test("maps the prod platform host to prod velay", () => {
17
+ expect(velayHostForPlatformHost("platform.vellum.ai")).toBe(
18
+ "velay.vellum.ai",
19
+ );
20
+ });
21
+
22
+ test("maps env-prefixed platform hosts to their env velay", () => {
23
+ expect(velayHostForPlatformHost("staging-platform.vellum.ai")).toBe(
24
+ "velay-staging.vellum.ai",
25
+ );
26
+ expect(velayHostForPlatformHost("dev-platform.vellum.ai")).toBe(
27
+ "velay-dev.vellum.ai",
28
+ );
29
+ });
30
+
31
+ test("returns null for hosts outside the deployment convention", () => {
32
+ expect(velayHostForPlatformHost("localhost")).toBeNull();
33
+ expect(velayHostForPlatformHost("platform.example.com")).toBeNull();
34
+ expect(
35
+ velayHostForPlatformHost("staging-platform.vellum.ai.evil.com"),
36
+ ).toBeNull();
37
+ });
38
+ });
39
+
14
40
  describe("normalizePublicBaseUrl", () => {
15
41
  test("trims whitespace and trailing slashes", () => {
16
42
  expect(normalizePublicBaseUrl(" https://example.test/path/// ")).toBe(
@@ -0,0 +1,200 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildRedactedSentinel,
5
+ createRedactedSentinelRegex,
6
+ isRevealableSentinel,
7
+ neutralizeRedactedSentinels,
8
+ parseRedactedSentinel,
9
+ REDACTED_SENTINEL_CLOSE,
10
+ REDACTED_SENTINEL_OPEN,
11
+ } from "../redacted-credential";
12
+
13
+ describe("buildRedactedSentinel", () => {
14
+ test("plain shape carries only the type", () => {
15
+ expect(buildRedactedSentinel({ type: "OpenAI Project Key" })).toBe(
16
+ `${REDACTED_SENTINEL_OPEN}redacted:OpenAI Project Key${REDACTED_SENTINEL_CLOSE}`,
17
+ );
18
+ });
19
+
20
+ test("enriched shape carries service and field", () => {
21
+ expect(
22
+ buildRedactedSentinel({
23
+ type: "Anthropic API Key",
24
+ service: "anthropic",
25
+ field: "api_key",
26
+ }),
27
+ ).toBe(
28
+ `${REDACTED_SENTINEL_OPEN}redacted:Anthropic API Key:anthropic:api_key${REDACTED_SENTINEL_CLOSE}`,
29
+ );
30
+ });
31
+
32
+ test("colon-qualified segments are percent-encoded, not rejected", () => {
33
+ // Real vaults contain colon-qualified service names (migration
34
+ // 018-rekey-compound-credential-keys produces `integration:google`).
35
+ // The builder must keep those revealable by encoding the delimiter.
36
+ expect(
37
+ buildRedactedSentinel({
38
+ type: "Generic Secret",
39
+ service: "integration:google",
40
+ field: "access_token",
41
+ }),
42
+ ).toBe(
43
+ `${REDACTED_SENTINEL_OPEN}redacted:Generic Secret:integration%3Agoogle:access_token${REDACTED_SENTINEL_CLOSE}`,
44
+ );
45
+ });
46
+
47
+ test("plain identifier segments stay literal (unencoded)", () => {
48
+ expect(
49
+ buildRedactedSentinel({
50
+ type: "Anthropic API Key",
51
+ service: "anthropic",
52
+ field: "api-key.v2_test",
53
+ }),
54
+ ).toBe(
55
+ `${REDACTED_SENTINEL_OPEN}redacted:Anthropic API Key:anthropic:api-key.v2_test${REDACTED_SENTINEL_CLOSE}`,
56
+ );
57
+ });
58
+
59
+ test("empty service or field degrades to the plain shape", () => {
60
+ expect(
61
+ buildRedactedSentinel({
62
+ type: "Generic Secret",
63
+ service: "",
64
+ field: "x",
65
+ }),
66
+ ).toBe(
67
+ `${REDACTED_SENTINEL_OPEN}redacted:Generic Secret${REDACTED_SENTINEL_CLOSE}`,
68
+ );
69
+ expect(
70
+ buildRedactedSentinel({
71
+ type: "Generic Secret",
72
+ service: "x",
73
+ field: "",
74
+ }),
75
+ ).toBe(
76
+ `${REDACTED_SENTINEL_OPEN}redacted:Generic Secret${REDACTED_SENTINEL_CLOSE}`,
77
+ );
78
+ });
79
+
80
+ test("missing field alone degrades to the plain shape", () => {
81
+ expect(
82
+ buildRedactedSentinel({ type: "Generic Secret", service: "anthropic" }),
83
+ ).toBe(
84
+ `${REDACTED_SENTINEL_OPEN}redacted:Generic Secret${REDACTED_SENTINEL_CLOSE}`,
85
+ );
86
+ });
87
+
88
+ test("throws on a type label that would corrupt the format", () => {
89
+ expect(() => buildRedactedSentinel({ type: "bad:type" })).toThrow();
90
+ expect(() =>
91
+ buildRedactedSentinel({ type: `bad${REDACTED_SENTINEL_CLOSE}type` }),
92
+ ).toThrow();
93
+ });
94
+ });
95
+
96
+ describe("parseRedactedSentinel", () => {
97
+ test("round-trips the plain shape", () => {
98
+ const s = buildRedactedSentinel({ type: "GitHub Token" });
99
+ expect(parseRedactedSentinel(s)).toEqual({ type: "GitHub Token" });
100
+ });
101
+
102
+ test("round-trips the enriched shape", () => {
103
+ const s = buildRedactedSentinel({
104
+ type: "GitHub Token",
105
+ service: "github-app",
106
+ field: "pem",
107
+ });
108
+ const parsed = parseRedactedSentinel(s);
109
+ expect(parsed).toEqual({
110
+ type: "GitHub Token",
111
+ service: "github-app",
112
+ field: "pem",
113
+ });
114
+ expect(parsed && isRevealableSentinel(parsed)).toBe(true);
115
+ });
116
+
117
+ test("round-trips encoded segments back to the original identifiers", () => {
118
+ const original = {
119
+ type: "OAuth Access Token",
120
+ service: "integration:google",
121
+ field: "access token/v2 (staging)",
122
+ };
123
+ const parsed = parseRedactedSentinel(buildRedactedSentinel(original));
124
+ expect(parsed).toEqual(original);
125
+ expect(parsed && isRevealableSentinel(parsed)).toBe(true);
126
+ });
127
+
128
+ test("malformed percent-escape degrades to the plain shape", () => {
129
+ // Daemon-encoded segments always decode; a hand-forged sentinel with a
130
+ // broken escape must not surface bogus vault coordinates.
131
+ const forged = `${REDACTED_SENTINEL_OPEN}redacted:Generic Secret:bad%zzsvc:field${REDACTED_SENTINEL_CLOSE}`;
132
+ const parsed = parseRedactedSentinel(forged);
133
+ expect(parsed).toEqual({ type: "Generic Secret" });
134
+ expect(parsed && isRevealableSentinel(parsed)).toBe(false);
135
+ });
136
+
137
+ test("rejects non-sentinel and partial inputs", () => {
138
+ expect(parseRedactedSentinel("nope")).toBeUndefined();
139
+ expect(
140
+ parseRedactedSentinel(`${REDACTED_SENTINEL_OPEN}redacted:Type`),
141
+ ).toBeUndefined();
142
+ expect(
143
+ parseRedactedSentinel(
144
+ `prefix ${buildRedactedSentinel({ type: "T" })} suffix`,
145
+ ),
146
+ ).toBeUndefined();
147
+ });
148
+
149
+ test("plain shape is not revealable", () => {
150
+ const parsed = parseRedactedSentinel(
151
+ buildRedactedSentinel({ type: "AWS Access Key" }),
152
+ );
153
+ expect(parsed && isRevealableSentinel(parsed)).toBe(false);
154
+ });
155
+ });
156
+
157
+ describe("neutralizeRedactedSentinels", () => {
158
+ test("a forged sentinel no longer matches the consumer regex", () => {
159
+ const forged = buildRedactedSentinel({
160
+ type: "GitHub Token",
161
+ service: "github-app",
162
+ field: "pem",
163
+ });
164
+ const neutralized = neutralizeRedactedSentinels(`quote: ${forged}`);
165
+ expect([...neutralized.matchAll(createRedactedSentinelRegex())]).toEqual(
166
+ [],
167
+ );
168
+ // Visually identical: only a zero-width word joiner was inserted.
169
+ expect(neutralized.replaceAll("\u2060", "")).toBe(`quote: ${forged}`);
170
+ });
171
+
172
+ test("is idempotent", () => {
173
+ const once = neutralizeRedactedSentinels(
174
+ buildRedactedSentinel({ type: "A" }),
175
+ );
176
+ expect(neutralizeRedactedSentinels(once)).toBe(once);
177
+ });
178
+
179
+ test("leaves ordinary text untouched", () => {
180
+ expect(neutralizeRedactedSentinels("plain text")).toBe("plain text");
181
+ expect(
182
+ neutralizeRedactedSentinels(`${REDACTED_SENTINEL_OPEN}not a sentinel`),
183
+ ).toBe(`${REDACTED_SENTINEL_OPEN}not a sentinel`);
184
+ });
185
+ });
186
+
187
+ describe("createRedactedSentinelRegex", () => {
188
+ test("finds multiple sentinels of both shapes in one text", () => {
189
+ const text = `a ${buildRedactedSentinel({ type: "A Key" })} b ${buildRedactedSentinel(
190
+ { type: "B Key", service: "svc", field: "f" },
191
+ )} c`;
192
+ const matches = [...text.matchAll(createRedactedSentinelRegex())];
193
+ expect(matches).toHaveLength(2);
194
+ expect(matches[0][1]).toBe("A Key");
195
+ expect(matches[0][2]).toBeUndefined();
196
+ expect(matches[1][1]).toBe("B Key");
197
+ expect(matches[1][2]).toBe("svc");
198
+ expect(matches[1][3]).toBe("f");
199
+ });
200
+ });
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Map a Vellum platform host onto its environment's velay host, following the
3
+ * deployment naming convention (`platform.vellum.ai` → `velay.vellum.ai`,
4
+ * `{env}-platform.vellum.ai` → `velay-{env}.vellum.ai`). Returns null for
5
+ * hosts outside that convention (localhost, custom domains). Shared by the
6
+ * gateway speech relay and the web live-voice client so the two ends of the
7
+ * managed-speech transport can't drift.
8
+ */
9
+ export function velayHostForPlatformHost(host: string): string | null {
10
+ if (host === "platform.vellum.ai") {
11
+ return "velay.vellum.ai";
12
+ }
13
+ const match = /^([a-z0-9-]+)-platform\.vellum\.ai$/.exec(host);
14
+ return match ? `velay-${match[1]}.vellum.ai` : null;
15
+ }
16
+
1
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
2
18
  if (typeof value !== "string") return undefined;
3
19
  const normalized = value.trim().replace(/\/+$/, "");
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Redacted-credential sentinel — the shared wire format for chat-persisted
3
+ * secret redactions.
4
+ *
5
+ * When the daemon persists a chat message containing a detected secret (and
6
+ * the `chat-credential-reveal` feature flag is on), the secret is replaced
7
+ * with a plain-text sentinel instead of the legacy `<redacted type="…" />`
8
+ * HTML marker. The sentinel is plain text on purpose: the chat markdown
9
+ * pipeline has no rehype-raw, so an HTML marker can never become an element —
10
+ * a text sentinel survives markdown parsing as an ordinary text node that a
11
+ * rehype plugin can match and upgrade into an interactive chip.
12
+ *
13
+ * Two shapes:
14
+ * `〔redacted:TYPE〕` — secret detected, vault identity unknown
15
+ * `〔redacted:TYPE:SERVICE:FIELD〕` — secret byte-matched to a stored credential
16
+ *
17
+ * The enriched shape is only emitted when the daemon has *proven* the match
18
+ * (exact plaintext comparison against a credential fetched for this turn) —
19
+ * never inferred. A wrong SERVICE:FIELD here would let the client reveal the
20
+ * wrong secret, so producers must treat enrichment as opt-in per proven match.
21
+ *
22
+ * The corner brackets (U+3014/U+3015) were chosen because they are not
23
+ * markdown syntax characters, never appear in the secret formats we detect,
24
+ * and are visually distinct if a client without chip support renders the raw
25
+ * sentinel.
26
+ *
27
+ * This module is shared by the daemon (producer) and clients/web (consumer)
28
+ * so the format can never drift between the two. It is intentionally
29
+ * dependency-free and data-only.
30
+ */
31
+
32
+ export const REDACTED_SENTINEL_OPEN = "\u3014"; // 〔
33
+ export const REDACTED_SENTINEL_CLOSE = "\u3015"; // 〕
34
+ export const REDACTED_SENTINEL_TAG = "redacted";
35
+
36
+ /**
37
+ * Version stamped as a `_redactionVersion` rider on daemon-persisted text
38
+ * blocks whose raw text passed through {@link neutralizeRedactedSentinels}
39
+ * before persistence. The daemon's history read boundary
40
+ * (`renderHistoryContent`) neutralizes text blocks WITHOUT this rider —
41
+ * rows persisted before the forgery guard existed — so a forged sentinel in
42
+ * old history can never reach a chip-enabled client surface. Internal to the
43
+ * daemon: the wire projection rebuilds blocks and never ships the rider.
44
+ */
45
+ export const SENTINEL_REDACTION_VERSION = 2;
46
+
47
+ /**
48
+ * Service/field segments are percent-encoded into the sentinel so arbitrary
49
+ * credential identifiers survive the colon-delimited format. The credential
50
+ * routes accept service/field as arbitrary strings — colon-qualified names
51
+ * exist in real vaults (migration `018-rekey-compound-credential-keys`
52
+ * produces `service = "integration:google"`), so a delimiter-collision
53
+ * charset restriction would silently downgrade proven reveals for those
54
+ * credentials to the non-revealable shape. Encoding keeps the common
55
+ * kebab/snake identifiers fully legible while making every other string
56
+ * representable.
57
+ *
58
+ * The encoded form contains only `[A-Za-z0-9_.%-]` — `encodeURIComponent`
59
+ * plus the characters it leaves bare that fall outside that set — so the
60
+ * sentinel regex stays unambiguous around its `:` delimiters and `〕` close.
61
+ */
62
+ function encodeSentinelSegment(segment: string): string {
63
+ return encodeURIComponent(segment).replace(
64
+ /[!'()*~]/g,
65
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Inverse of {@link encodeSentinelSegment}. Returns undefined for malformed
71
+ * percent-escapes — daemon-encoded segments always decode, and forged
72
+ * sentinels are neutralized at persist time, so this path is defensive.
73
+ */
74
+ function decodeSentinelSegment(segment: string): string | undefined {
75
+ try {
76
+ return decodeURIComponent(segment);
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Charset for the TYPE label. Pattern labels are our own
84
+ * (`secret-patterns.ts` / scanner-only patterns) and contain only words,
85
+ * spaces, and hyphens — but validate anyway so a future label containing a
86
+ * colon or bracket cannot silently corrupt the format.
87
+ */
88
+ const TYPE_RE = /^[^:\u3014\u3015]+$/;
89
+
90
+ export interface RedactedCredentialSentinel {
91
+ /** Human-readable secret type label, e.g. "Anthropic API Key". */
92
+ type: string;
93
+ /** Vault service namespace — present only when the match was proven. */
94
+ service?: string;
95
+ /** Vault field name — present only when the match was proven. */
96
+ field?: string;
97
+ }
98
+
99
+ /** True when the sentinel carries a proven vault identity. */
100
+ export function isRevealableSentinel(
101
+ s: RedactedCredentialSentinel,
102
+ ): s is RedactedCredentialSentinel & { service: string; field: string } {
103
+ return s.service !== undefined && s.field !== undefined;
104
+ }
105
+
106
+ /**
107
+ * Build a sentinel string. Service/field are percent-encoded (see
108
+ * {@link encodeSentinelSegment}), so any non-empty identifier the credential
109
+ * routes accept — including colon-qualified names like `integration:google`
110
+ * — round-trips through the enriched shape. Empty segments fall back to the
111
+ * plain (non-revealable) shape; the function throws only if the type label
112
+ * itself is unusable (which would mean a broken pattern table).
113
+ */
114
+ export function buildRedactedSentinel(
115
+ sentinel: RedactedCredentialSentinel,
116
+ ): string {
117
+ const { type, service, field } = sentinel;
118
+ if (!TYPE_RE.test(type)) {
119
+ throw new Error(`Invalid redacted-sentinel type label: ${type}`);
120
+ }
121
+ const base = `${REDACTED_SENTINEL_OPEN}${REDACTED_SENTINEL_TAG}:${type}`;
122
+ if (
123
+ service !== undefined &&
124
+ field !== undefined &&
125
+ service !== "" &&
126
+ field !== ""
127
+ ) {
128
+ const encService = encodeSentinelSegment(service);
129
+ const encField = encodeSentinelSegment(field);
130
+ return `${base}:${encService}:${encField}${REDACTED_SENTINEL_CLOSE}`;
131
+ }
132
+ return `${base}${REDACTED_SENTINEL_CLOSE}`;
133
+ }
134
+
135
+ /**
136
+ * Global-flag regex for locating sentinels inside a larger text. Callers must
137
+ * not share a single instance across concurrent iterations (stateful
138
+ * `lastIndex`); use {@link createRedactedSentinelRegex}.
139
+ */
140
+ export function createRedactedSentinelRegex(): RegExp {
141
+ // 〔redacted:TYPE〕 or 〔redacted:TYPE:SERVICE:FIELD〕
142
+ // TYPE = anything except colon/brackets; SERVICE/FIELD = the
143
+ // percent-encoded segment charset (see encodeSentinelSegment). Capture
144
+ // groups carry the ENCODED segments — decode via decodeRedactedSentinelMatch.
145
+ return new RegExp(
146
+ `${REDACTED_SENTINEL_OPEN}${REDACTED_SENTINEL_TAG}:([^:\u3014\u3015]+?)(?::([A-Za-z0-9_.%-]+):([A-Za-z0-9_.%-]+))?${REDACTED_SENTINEL_CLOSE}`,
147
+ "g",
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Global-flag regex for locating NEUTRALIZED sentinels — the word-joiner
153
+ * form {@link neutralizeRedactedSentinels} produces — inside a larger text.
154
+ * Renderers use this to show a defused redaction marker as an inert badge
155
+ * instead of leaking bare sentinel glyphs into the transcript. The capture
156
+ * groups mirror {@link createRedactedSentinelRegex}, but consumers must NOT
157
+ * trust them as vault coordinates: a neutralized span is by definition text
158
+ * the daemon refused to vouch for. Same statefulness caveat as the genuine
159
+ * regex — create a fresh instance per iteration.
160
+ */
161
+ export function createNeutralizedSentinelRegex(): RegExp {
162
+ return new RegExp(
163
+ `${REDACTED_SENTINEL_OPEN}\\u2060${REDACTED_SENTINEL_TAG}:([^:\\u3014\\u3015]+?)(?::([A-Za-z0-9_.%-]+):([A-Za-z0-9_.%-]+))?${REDACTED_SENTINEL_CLOSE}`,
164
+ "g",
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Map a {@link createRedactedSentinelRegex} match to a decoded sentinel.
170
+ * Shared by {@link parseRedactedSentinel} and the web rehype plugin so the
171
+ * segment decoding never drifts between consumers. A malformed
172
+ * percent-escape (unreachable for daemon-minted sentinels; forged ones are
173
+ * neutralized at persist) degrades to the plain non-revealable shape rather
174
+ * than surfacing bogus vault coordinates.
175
+ */
176
+ export function decodeRedactedSentinelMatch(
177
+ m: RegExpExecArray,
178
+ ): RedactedCredentialSentinel {
179
+ const [, type, encService, encField] = m;
180
+ if (encService !== undefined && encField !== undefined) {
181
+ const service = decodeSentinelSegment(encService);
182
+ const field = decodeSentinelSegment(encField);
183
+ if (service !== undefined && field !== undefined) {
184
+ return { type, service, field };
185
+ }
186
+ }
187
+ return { type };
188
+ }
189
+
190
+ /**
191
+ * Neutralize sentinel-shaped strings that were NOT produced by the redactor.
192
+ *
193
+ * The sentinel is plain text, so any content source (model output quoting a
194
+ * transcript, fetched web content, a pasted message) could otherwise forge
195
+ * one and manufacture a reveal chip for an arbitrary stored credential.
196
+ * Producers must call this on raw text *before* inserting their own
197
+ * sentinels, so the only sentinels that survive persistence are the ones the
198
+ * redactor itself emitted from a detected secret.
199
+ *
200
+ * Neutralization inserts a word joiner (U+2060, zero-width, non-breaking)
201
+ * after the opening bracket — visually identical for a human reading a
202
+ * quoted sentinel, but the consumer regex no longer matches. Idempotent:
203
+ * after one pass the trigger prefix no longer occurs, so re-running is a
204
+ * no-op and never corrupts text further.
205
+ */
206
+ export function neutralizeRedactedSentinels(text: string): string {
207
+ const trigger = `${REDACTED_SENTINEL_OPEN}${REDACTED_SENTINEL_TAG}:`;
208
+ if (!text.includes(trigger)) return text;
209
+ return text.replaceAll(
210
+ trigger,
211
+ `${REDACTED_SENTINEL_OPEN}\u2060${REDACTED_SENTINEL_TAG}:`,
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Parse a single exact sentinel string (the whole input must be one
217
+ * sentinel). Returns undefined when the input is not a valid sentinel.
218
+ */
219
+ export function parseRedactedSentinel(
220
+ text: string,
221
+ ): RedactedCredentialSentinel | undefined {
222
+ const re = createRedactedSentinelRegex();
223
+ const m = re.exec(text);
224
+ if (!m || m[0] !== text) return undefined;
225
+ return decodeRedactedSentinelMatch(m);
226
+ }
@@ -15,6 +15,7 @@
15
15
  "./handles": "./src/handles.ts",
16
16
  "./rpc": "./src/rpc.ts",
17
17
  "./attachment-naming": "./src/attachment-naming.ts",
18
+ "./redacted-credential": "./src/redacted-credential.ts",
18
19
  "./error": "./src/error.ts"
19
20
  },
20
21
  "scripts": {
@@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test";
3
3
  import {
4
4
  normalizeHttpPublicBaseUrl,
5
5
  normalizePublicBaseUrl,
6
+ velayHostForPlatformHost,
6
7
  } from "../ingress.js";
7
8
  import {
8
9
  buildTwilioMediaStreamUrl,
@@ -11,6 +12,31 @@ import {
11
12
  resolveTwilioPublicBaseUrl,
12
13
  } from "../twilio-ingress.js";
13
14
 
15
+ describe("velayHostForPlatformHost", () => {
16
+ test("maps the prod platform host to prod velay", () => {
17
+ expect(velayHostForPlatformHost("platform.vellum.ai")).toBe(
18
+ "velay.vellum.ai",
19
+ );
20
+ });
21
+
22
+ test("maps env-prefixed platform hosts to their env velay", () => {
23
+ expect(velayHostForPlatformHost("staging-platform.vellum.ai")).toBe(
24
+ "velay-staging.vellum.ai",
25
+ );
26
+ expect(velayHostForPlatformHost("dev-platform.vellum.ai")).toBe(
27
+ "velay-dev.vellum.ai",
28
+ );
29
+ });
30
+
31
+ test("returns null for hosts outside the deployment convention", () => {
32
+ expect(velayHostForPlatformHost("localhost")).toBeNull();
33
+ expect(velayHostForPlatformHost("platform.example.com")).toBeNull();
34
+ expect(
35
+ velayHostForPlatformHost("staging-platform.vellum.ai.evil.com"),
36
+ ).toBeNull();
37
+ });
38
+ });
39
+
14
40
  describe("normalizePublicBaseUrl", () => {
15
41
  test("trims whitespace and trailing slashes", () => {
16
42
  expect(normalizePublicBaseUrl(" https://example.test/path/// ")).toBe(