@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
@@ -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(
@@ -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
+ });