@vellumai/assistant 0.11.6 → 0.11.7-staging.1

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 (447) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -179,6 +179,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
179
179
  "providers/inference/credential-slot-repair.ts", // boot repair: copies the shared openai-compatible slot value into per-connection slots (get/set of provider API keys only; values never logged or returned)
180
180
  "runtime/routes/inference-provider-connection-routes.ts", // connection delete removes its dedicated per-connection key slot (deleteSecureKeyAsync only; no reads)
181
181
  "daemon/handlers/config-slack-channel.ts", // Slack channel config credential management
182
+ "daemon/handlers/config-discord-channel.ts", // Discord channel bot token management
182
183
  "providers/platform-proxy/context.ts", // managed proxy API key lookup for provider initialization
183
184
  "platform/client.ts", // platform client credential store fallback for standalone CLI auth
184
185
  "mcp/mcp-header-store.ts", // MCP static auth header persistence (credential store CRUD + legacy migration)
@@ -203,8 +204,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
203
204
  "providers/registry.ts", // provider registry API key lookup for initialization
204
205
  "providers/inference/resolve-auth.ts", // provider_connection auth resolver (api_key path reads vault, mirrors registry.ts)
205
206
  "providers/inference/codex-token-refresh.ts", // Codex OAuth token refresh (reads/writes access_token, refresh_token, expires_at)
206
- "cli/commands/inference-providers.ts", // ChatGPT subscription OAuth token storage
207
- "runtime/routes/chatgpt-subscription-auth-routes.ts", // ChatGPT subscription daemon OAuth flow (stores tokens in CES)
207
+ "providers/inference/chatgpt-subscription-credentials.ts", // ChatGPT subscription sign-in token storage, shared by the daemon routes and the CLI (setSecureKeyAsync only; no reads)
208
208
  "providers/provider-availability.ts", // provider availability API key check
209
209
  "media/image-credentials.ts", // shared image-gen credential resolver (provider API key lookup)
210
210
  "persistence/embeddings/embedding-backend.ts", // embedding backend API key lookup
@@ -231,7 +231,6 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
231
231
  "runtime/routes/avatar-routes.ts", // avatar generate route reads platform_base_url from credential store
232
232
  "cli/commands/keys.ts", // CLI provider key management
233
233
  "cli/commands/oauth/connect.ts", // CLI OAuth connect stored-secret verification
234
- "runtime/routes/chatgpt-subscription-auth-routes.ts", // ChatGPT subscription OAuth token storage
235
234
  "runtime/routes/identity-routes.ts", // health/readyz endpoint checks CES connectivity via getCesClient
236
235
  "tools/executor.ts", // CES approval bridge resolves the CES RPC client via getCesClient
237
236
  "tools/network/web-fetch.ts", // Firecrawl /scrape BYOK fetch provider API key lookup (firecrawl provider key)
@@ -470,24 +469,24 @@ describe("Invariant 4: credentials only used for allowed purpose", () => {
470
469
  });
471
470
 
472
471
  // PR 18 — vault policy fields with strict defaults
473
- test("credential without explicit policy gets strict defaults (deny all)", () => {
472
+ test("credential without explicit policy gets strict defaults (deny all)", async () => {
474
473
  // A credential stored without allowed_tools defaults to empty array,
475
474
  // which the broker's isToolAllowed check fails closed on.
476
475
  upsertCredentialMetadata("test-svc", "pass", {});
477
476
 
478
- const result = broker.authorize({
477
+ const result = await broker.browserFill({
479
478
  service: "test-svc",
480
479
  field: "pass",
481
480
  toolName: "browser_fill_credential",
481
+ fill: async () => {},
482
482
  });
483
483
 
484
- expect(result.authorized).toBe(false);
485
- expect(!result.authorized && result.reason).toContain(
486
- "No tools are currently allowed",
487
- );
484
+ expect(result.success).toBe(false);
485
+ expect(result.reason).toContain("No tools are currently allowed");
488
486
  });
489
487
  });
490
488
 
489
+
491
490
  // ---------------------------------------------------------------------------
492
491
  // Invariant 6 — oauth2ClientSecret never in plaintext metadata
493
492
  // ---------------------------------------------------------------------------
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Managed backup profiles and the `fallbackProfile` pointers of the vellum
3
+ * column (`default-profile-catalog.ts`).
4
+ *
5
+ * The invariants under test:
6
+ * - Every default profile's managed (vellum) implementation points at a
7
+ * backup profile that exists in the code catalog.
8
+ * - Cross-provider rule: a backup pins its model at a DIFFERENT managed
9
+ * upstream than its primary, so an outage at the primary's provider can
10
+ * be served by the backup. Holds for the experiment arms too.
11
+ * - Probe coverage: every backup model is a catalog member of its upstream
12
+ * provider, which is what feeds the platform's model liveness probe.
13
+ * - Scoping: backups exist for the managed column only. A BYOK or chatgpt
14
+ * default provider materializes no backup profiles, and its primaries
15
+ * carry no `fallbackProfile` pointers.
16
+ * - The effective catalog parses under `LLMSchema` (exercising the
17
+ * `fallbackProfile` superRefine against the real entries), and backups
18
+ * are delete-protected like the other managed defaults.
19
+ */
20
+
21
+ import { afterEach, describe, expect, test } from "bun:test";
22
+
23
+ import { BALANCED_MODEL_EXPERIMENT_FLAG_KEY } from "../config/balanced-model-experiment.js";
24
+ import {
25
+ CODE_DEFAULT_PROFILE_ENTRIES,
26
+ getEffectiveProfile,
27
+ getEffectiveProfiles,
28
+ getEffectiveProfilesForProvider,
29
+ INVARIANT_PROFILE_NAMES,
30
+ MANAGED_PROFILE_NAMES,
31
+ MANAGED_PROFILE_TEMPLATES,
32
+ PROFILE_IMPLS,
33
+ resolveDefaultProfileForProvider,
34
+ } from "../config/default-profile-catalog.js";
35
+ import {
36
+ BACKUP_PROFILE_KEYS,
37
+ DEFAULT_PROFILE_KEYS,
38
+ DEFAULT_PROFILE_PROVIDERS,
39
+ FALLBACK_PROFILE_BY_KEY,
40
+ } from "../config/default-profile-names.js";
41
+ import { clearCachedOverrides } from "../config/feature-flag-cache.js";
42
+ import {
43
+ type DefaultProviderConfig,
44
+ LLMSchema,
45
+ type ProfileEntry,
46
+ } from "../config/schemas/llm.js";
47
+ import { isModelInCatalog } from "../providers/model-catalog.js";
48
+ import { getManagedUpstream } from "../providers/vellum-model-routing.js";
49
+ import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
50
+
51
+ const vellum: DefaultProviderConfig = { provider: "vellum" };
52
+
53
+ afterEach(() => {
54
+ clearCachedOverrides();
55
+ });
56
+
57
+ describe("fallbackProfile pointers on the vellum column", () => {
58
+ test("every default profile's vellum impl points at an existing backup", () => {
59
+ for (const key of DEFAULT_PROFILE_KEYS) {
60
+ const impl = PROFILE_IMPLS[key].vellum;
61
+ expect(impl.fallbackProfile).toBe(FALLBACK_PROFILE_BY_KEY[key]);
62
+ const backup = CODE_DEFAULT_PROFILE_ENTRIES[FALLBACK_PROFILE_BY_KEY[key]];
63
+ expect(backup).toBeDefined();
64
+ expect(backup.provider).toBe("vellum");
65
+ expect(backup.source).toBe("managed");
66
+ // Single hop: a backup never declares a fallback of its own.
67
+ expect(backup.fallbackProfile).toBeUndefined();
68
+ }
69
+ });
70
+
71
+ test("no BYOK or chatgpt column impl carries fallbackProfile", () => {
72
+ for (const key of DEFAULT_PROFILE_KEYS) {
73
+ for (const provider of DEFAULT_PROFILE_PROVIDERS) {
74
+ if (provider === "vellum") {
75
+ continue;
76
+ }
77
+ expect(PROFILE_IMPLS[key][provider].fallbackProfile).toBeUndefined();
78
+ }
79
+ // A default-capable provider without a named matrix column
80
+ // materializes from the shared BYOK templates: no pointer either.
81
+ const entry = resolveDefaultProfileForProvider(undefined, key, {
82
+ provider: "together",
83
+ });
84
+ expect(entry?.fallbackProfile).toBeUndefined();
85
+ }
86
+ });
87
+ });
88
+
89
+ describe("cross-provider rule", () => {
90
+ test("every backup dispatches through a different managed upstream than its primary", () => {
91
+ for (const key of DEFAULT_PROFILE_KEYS) {
92
+ const primary = CODE_DEFAULT_PROFILE_ENTRIES[key];
93
+ const backup = CODE_DEFAULT_PROFILE_ENTRIES[FALLBACK_PROFILE_BY_KEY[key]];
94
+ const primaryUpstream = getManagedUpstream(primary.model as string);
95
+ const backupUpstream = getManagedUpstream(backup.model as string);
96
+ expect(primaryUpstream).not.toBeNull();
97
+ expect(backupUpstream).not.toBeNull();
98
+ expect(backupUpstream).not.toBe(primaryUpstream);
99
+ }
100
+ });
101
+
102
+ test("the balanced experiment arms keep the cross-provider split", () => {
103
+ const backup =
104
+ CODE_DEFAULT_PROFILE_ENTRIES[FALLBACK_PROFILE_BY_KEY.balanced];
105
+ const backupUpstream = getManagedUpstream(backup.model as string);
106
+ for (const arm of ["terra", "glm-5p2"]) {
107
+ setOverridesForTesting({ [BALANCED_MODEL_EXPERIMENT_FLAG_KEY]: arm });
108
+ const armed = resolveDefaultProfileForProvider(
109
+ undefined,
110
+ "balanced",
111
+ vellum,
112
+ );
113
+ expect(armed?.fallbackProfile).toBe(FALLBACK_PROFILE_BY_KEY.balanced);
114
+ const armedUpstream = getManagedUpstream(armed?.model as string);
115
+ expect(armedUpstream).not.toBeNull();
116
+ expect(armedUpstream).not.toBe(backupUpstream);
117
+ }
118
+ });
119
+
120
+ test("every backup model is a catalog member of its upstream (liveness-probe coverage)", () => {
121
+ for (const key of BACKUP_PROFILE_KEYS) {
122
+ const model = CODE_DEFAULT_PROFILE_ENTRIES[key].model as string;
123
+ const upstream = getManagedUpstream(model);
124
+ expect(upstream).not.toBeNull();
125
+ expect(isModelInCatalog(upstream as string, model)).toBe(true);
126
+ }
127
+ });
128
+ });
129
+
130
+ describe("managed-column-only scoping", () => {
131
+ test("the managed column materializes the backups after the primaries", () => {
132
+ const effective = getEffectiveProfilesForProvider(undefined, vellum);
133
+ for (const key of BACKUP_PROFILE_KEYS) {
134
+ expect(effective[key]).toBeDefined();
135
+ expect(effective[key]?.provider).toBe("vellum");
136
+ expect(effective[key]?.source).toBe("managed");
137
+ }
138
+ // The seeder inserts missing managed keys in MANAGED_PROFILE_TEMPLATES
139
+ // order, which is what places backups after the primaries in
140
+ // `profileOrder` presentation.
141
+ expect(Object.keys(MANAGED_PROFILE_TEMPLATES)).toEqual([
142
+ ...DEFAULT_PROFILE_KEYS,
143
+ ...BACKUP_PROFILE_KEYS,
144
+ ]);
145
+ });
146
+
147
+ test("a null defaultProvider (pre-defaultProvider install) keeps its backups", () => {
148
+ const effective = getEffectiveProfiles(undefined);
149
+ for (const key of BACKUP_PROFILE_KEYS) {
150
+ expect(effective[key]).toBeDefined();
151
+ expect(
152
+ resolveDefaultProfileForProvider(undefined, key, null),
153
+ ).toBeDefined();
154
+ }
155
+ });
156
+
157
+ test("a BYOK default provider materializes no backups and no pointers", () => {
158
+ for (const provider of ["anthropic", "openai", "gemini"] as const) {
159
+ const effective = getEffectiveProfilesForProvider(undefined, {
160
+ provider,
161
+ });
162
+ for (const key of BACKUP_PROFILE_KEYS) {
163
+ expect(effective[key]).toBeUndefined();
164
+ }
165
+ for (const key of DEFAULT_PROFILE_KEYS) {
166
+ expect(effective[key]?.fallbackProfile).toBeUndefined();
167
+ }
168
+ }
169
+ });
170
+
171
+ test("the chatgpt column materializes no backups and no pointers", () => {
172
+ const effective = getEffectiveProfilesForProvider(undefined, {
173
+ provider: "chatgpt",
174
+ });
175
+ for (const key of BACKUP_PROFILE_KEYS) {
176
+ expect(effective[key]).toBeUndefined();
177
+ }
178
+ for (const key of DEFAULT_PROFILE_KEYS) {
179
+ expect(effective[key]?.fallbackProfile).toBeUndefined();
180
+ }
181
+ });
182
+
183
+ test("a persisted managed stub does not resurrect a backup off the managed column", () => {
184
+ // A `config get` -> `config set` round-trip on a managed install can
185
+ // leave a thin managed stub for a backup in `llm.profiles`
186
+ // (`normalizeManagedProfileWrites`). The stub is the workspace's slot for
187
+ // a code-owned body, not a profile, so once the column has no body behind
188
+ // it the name must stop being listed rather than resolve to a
189
+ // providerless entry the pickers would offer and `LLMSchema` would
190
+ // reject.
191
+ const workspace: Record<string, ProfileEntry> = {
192
+ "balanced-backup": { source: "managed" },
193
+ };
194
+ for (const provider of ["anthropic", "chatgpt"] as const) {
195
+ const effective = getEffectiveProfilesForProvider(workspace, {
196
+ provider,
197
+ });
198
+ expect(effective["balanced-backup"]).toBeUndefined();
199
+ expect(
200
+ resolveDefaultProfileForProvider(workspace, "balanced-backup", {
201
+ provider,
202
+ }),
203
+ ).toBeUndefined();
204
+ }
205
+ // The managed column still has a body for the stub to stand for.
206
+ expect(
207
+ getEffectiveProfilesForProvider(workspace, vellum)["balanced-backup"]
208
+ ?.model,
209
+ ).toBe(CODE_DEFAULT_PROFILE_ENTRIES["balanced-backup"]?.model);
210
+ });
211
+
212
+ test("a user-owned entry under a backup name stays listed off the managed column", () => {
213
+ // Nothing code-owned resolves there, so the entry is simply a profile of
214
+ // that name and keeps its own body.
215
+ const workspace: Record<string, ProfileEntry> = {
216
+ "balanced-backup": {
217
+ source: "user",
218
+ provider: "anthropic",
219
+ model: "claude-opus-4-7",
220
+ },
221
+ };
222
+ const effective = getEffectiveProfilesForProvider(workspace, {
223
+ provider: "anthropic",
224
+ });
225
+ expect(effective["balanced-backup"]?.model).toBe("claude-opus-4-7");
226
+ });
227
+ });
228
+
229
+ describe("effective-profile machinery", () => {
230
+ test("the full effective catalog parses under LLMSchema", () => {
231
+ // Exercises the fallbackProfile superRefine (target exists, no self
232
+ // reference, no mix target, single hop) against the real entries.
233
+ for (const profiles of [
234
+ getEffectiveProfiles(undefined),
235
+ getEffectiveProfilesForProvider(undefined, vellum),
236
+ ]) {
237
+ const parsed = LLMSchema.parse({
238
+ profiles,
239
+ activeProfile: "balanced",
240
+ });
241
+ for (const key of DEFAULT_PROFILE_KEYS) {
242
+ expect(parsed.profiles[key]?.fallbackProfile).toBe(
243
+ FALLBACK_PROFILE_BY_KEY[key],
244
+ );
245
+ }
246
+ }
247
+ });
248
+
249
+ test("a workspace entry never overlays a backup, whatever its source", () => {
250
+ // Backups are code-owned (`CODE_OWNED_PROFILE_NAMES`), so unlike the
251
+ // primaries they take no workspace overlay at all: not the usual
252
+ // label/status/topP whitelist from a managed stub, and not a user-owned
253
+ // shadow. Disabling a backup through a hand-edited config would strand
254
+ // the primary's fallback exactly when it is needed.
255
+ for (const source of ["managed", "user"] as const) {
256
+ const workspace: Record<string, ProfileEntry> = {
257
+ "balanced-backup": {
258
+ source,
259
+ label: "My Backup",
260
+ status: "disabled",
261
+ topP: 0.7,
262
+ // Stale content drift on disk must lose to the code default body.
263
+ provider: "anthropic",
264
+ model: "claude-sonnet-4-6",
265
+ maxTokens: 1,
266
+ },
267
+ };
268
+ const entry = getEffectiveProfile(workspace, "balanced-backup");
269
+ const body = CODE_DEFAULT_PROFILE_ENTRIES["balanced-backup"];
270
+ expect(entry?.label).toBe(body.label);
271
+ expect(entry?.status).toBeUndefined();
272
+ expect(entry?.topP).toBe(body.topP);
273
+ expect(entry?.model).toBe(body.model);
274
+ expect(entry?.maxTokens).toBe(body.maxTokens);
275
+ expect(String(entry?.provider)).toBe("vellum");
276
+ }
277
+ });
278
+
279
+ test("backup profiles are delete-protected like the other managed defaults", () => {
280
+ // Membership in these sets is what wires the route-level deletion
281
+ // rejection (`rejectManagedProfileDeletion`, `handleCreateProfile`) and
282
+ // the commitConfigWrite invariant guard
283
+ // (`assertInvariantProfilesPreserved`).
284
+ for (const key of BACKUP_PROFILE_KEYS) {
285
+ expect(MANAGED_PROFILE_NAMES.has(key)).toBe(true);
286
+ expect(INVARIANT_PROFILE_NAMES.has(key)).toBe(true);
287
+ }
288
+ });
289
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The Discord callback seam, end to end through the join rather than either
3
+ * half of it.
4
+ *
5
+ * Discord's send, render and DM delivery each have their own unit coverage,
6
+ * and the gateway's inbound has schema, admission and normalizer coverage.
7
+ * Nothing exercised the point where they meet: a reply addressed to a Discord
8
+ * callback resolving to the Discord transport and being delivered with the
9
+ * chat id it was given. That join is where a channel goes silently deaf while
10
+ * every component around it still passes, which is the shape the Slack
11
+ * plan-mode bug had.
12
+ */
13
+
14
+ import { describe, expect, mock, test } from "bun:test";
15
+
16
+ const sent: Array<{ channelId: string; text: string }> = [];
17
+
18
+ // Spread the real module rather than listing the exports this test happens to
19
+ // need. `mock.module` replaces the module wholesale, so a partial stub breaks
20
+ // the transport's import of any export it omits, and would break again the
21
+ // next time `send.ts` grows one.
22
+ const actualSend = await import("../messaging/providers/discord/send.js");
23
+
24
+ mock.module("../messaging/providers/discord/send.js", () => ({
25
+ ...actualSend,
26
+ sendDiscordReply: async (target: { channelId: string }, text: string) => {
27
+ sent.push({ channelId: target.channelId, text });
28
+ return { messageId: "1401234567890123456" };
29
+ },
30
+ }));
31
+
32
+ const { channelForCallback } =
33
+ await import("../messaging/providers/callback-routing.js");
34
+ const { deliverDirect, getTransportForCallback, isDirectDelivery } =
35
+ await import("../messaging/providers/index.js");
36
+
37
+ type ChannelReplyPayload = Parameters<typeof deliverDirect>[1];
38
+
39
+ const DISCORD_CALLBACK = "https://gateway.example/deliver/discord";
40
+
41
+ describe("a Discord reply reaches the Discord transport", () => {
42
+ test("the callback resolves to discord, absolute and relative", () => {
43
+ expect(channelForCallback(DISCORD_CALLBACK)).toBe("discord");
44
+ // Off-channel guardian flows emit a base-less callback.
45
+ expect(channelForCallback("/deliver/discord")).toBe("discord");
46
+ expect(channelForCallback("/deliver/discord?x=1")).toBe("discord");
47
+ });
48
+
49
+ test("the resolved transport is the one that can send", () => {
50
+ expect(isDirectDelivery(DISCORD_CALLBACK)).toBe(true);
51
+ const transport = getTransportForCallback(DISCORD_CALLBACK);
52
+ expect(transport).toBeDefined();
53
+ expect(typeof transport?.deliver).toBe("function");
54
+ });
55
+
56
+ test("a reply is delivered to the chat id it was addressed to", async () => {
57
+ sent.length = 0;
58
+ const payload: ChannelReplyPayload = {
59
+ chatId: "1409876543210987654",
60
+ text: "hello from the assistant",
61
+ };
62
+ await deliverDirect(DISCORD_CALLBACK, payload);
63
+
64
+ expect(sent).toHaveLength(1);
65
+ // `POST /channels/{id}/messages` addresses a channel and nothing more, so
66
+ // the chat id is the channel the reply lands in.
67
+ expect(sent[0]?.channelId).toBe("1409876543210987654");
68
+ expect(sent[0]?.text).toContain("hello from the assistant");
69
+ });
70
+
71
+ test("a threaded reply lands in the thread, which is its own channel", async () => {
72
+ sent.length = 0;
73
+ const payload: ChannelReplyPayload = {
74
+ chatId: "1409876543210987654",
75
+ text: "in the thread",
76
+ };
77
+ await deliverDirect(
78
+ `${DISCORD_CALLBACK}?threadId=1405555555555555555`,
79
+ payload,
80
+ );
81
+
82
+ expect(sent[0]?.channelId).toBe("1405555555555555555");
83
+ });
84
+
85
+ test("Discord declines streaming rather than faking it", () => {
86
+ // No primitive exists, so the transport omits the method and the caller
87
+ // sends the finished reply. An implementation here would be a repeated
88
+ // edit, which is the approach the platforms discourage.
89
+ expect(getTransportForCallback(DISCORD_CALLBACK)?.streamReply).toBe(
90
+ undefined,
91
+ );
92
+ });
93
+ });
@@ -0,0 +1,237 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ afterAll,
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ mock,
10
+ spyOn,
11
+ test,
12
+ } from "bun:test";
13
+
14
+ const originalVellumDev = process.env.VELLUM_DEV;
15
+ process.env.VELLUM_DEV = "1";
16
+
17
+ mock.module("../util/logger.js", () => ({
18
+ getLogger: () => ({
19
+ info: () => {},
20
+ warn: () => {},
21
+ error: () => {},
22
+ debug: () => {},
23
+ trace: () => {},
24
+ fatal: () => {},
25
+ child: () => ({
26
+ info: () => {},
27
+ warn: () => {},
28
+ error: () => {},
29
+ debug: () => {},
30
+ }),
31
+ }),
32
+ }));
33
+
34
+ let oauthConnectionStore: Record<string, { id: string; status: string }> = {};
35
+
36
+ mock.module("../oauth/manual-token-connection.js", () => ({
37
+ ensureManualTokenConnection: async (provider: string) => {
38
+ oauthConnectionStore[provider] = {
39
+ id: `conn-${provider}`,
40
+ status: "active",
41
+ };
42
+ },
43
+ removeManualTokenConnection: (provider: string) => {
44
+ delete oauthConnectionStore[provider];
45
+ },
46
+ syncManualTokenConnection: async () => {},
47
+ }));
48
+
49
+ const originalFetch = globalThis.fetch;
50
+
51
+ import {
52
+ clearDiscordChannelConfig,
53
+ getDiscordChannelConfig,
54
+ setDiscordChannelConfig,
55
+ } from "../daemon/handlers/config-discord-channel.js";
56
+ import { credentialKey } from "../security/credential-key.js";
57
+ import * as secureKeys from "../security/secure-keys.js";
58
+ import { _resetBackend, getSecureKeyAsync } from "../security/secure-keys.js";
59
+ import { _setMetadataPath } from "../tools/credentials/metadata-store.js";
60
+ import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
61
+ import { setConfig } from "./helpers/set-config.js";
62
+
63
+ const BOT_TOKEN_KEY = credentialKey("discord_channel", "bot_token");
64
+
65
+ /**
66
+ * The permission integer the parameterized invite link requests. The setup
67
+ * skill's invite script derives the same value from named bits; skills are
68
+ * import-isolated from this package, so equality is pinned here instead.
69
+ */
70
+ const INVITE_PERMISSIONS = "277025770560";
71
+
72
+ interface FakeDiscord {
73
+ meStatus?: number;
74
+ application?: unknown;
75
+ }
76
+
77
+ function installDiscordApi(fake: FakeDiscord) {
78
+ globalThis.fetch = (async (input: URL | RequestInfo) => {
79
+ const url = String(input);
80
+ if (url.endsWith("/users/@me")) {
81
+ return new Response(
82
+ JSON.stringify({ id: "900000000000000001", username: "test-bot" }),
83
+ { status: fake.meStatus ?? 200 },
84
+ );
85
+ }
86
+ if (url.endsWith("/oauth2/applications/@me")) {
87
+ return new Response(JSON.stringify(fake.application ?? {}), {
88
+ status: 200,
89
+ });
90
+ }
91
+ throw new Error(`Unexpected fetch: ${url}`);
92
+ }) as typeof fetch;
93
+ }
94
+
95
+ afterAll(() => {
96
+ globalThis.fetch = originalFetch;
97
+ _setMetadataPath(null);
98
+ setStorePathForTesting(null);
99
+ _resetBackend();
100
+ if (originalVellumDev === undefined) {
101
+ delete process.env.VELLUM_DEV;
102
+ } else {
103
+ process.env.VELLUM_DEV = originalVellumDev;
104
+ }
105
+ });
106
+
107
+ const testDir = process.env.VELLUM_WORKSPACE_DIR!;
108
+ const secureStorePath = join(testDir, "discord-config-keys.enc");
109
+ const metadataPath = join(testDir, "discord-config-metadata.json");
110
+
111
+ beforeEach(() => {
112
+ oauthConnectionStore = {};
113
+ setConfig("discord", {});
114
+ rmSync(secureStorePath, { force: true });
115
+ rmSync(metadataPath, { force: true });
116
+ setStorePathForTesting(secureStorePath);
117
+ _resetBackend();
118
+ _setMetadataPath(metadataPath);
119
+ });
120
+
121
+ afterEach(() => {
122
+ globalThis.fetch = originalFetch;
123
+ });
124
+
125
+ describe("connecting a Discord bot", () => {
126
+ test("a valid token is stored and the identity read back", async () => {
127
+ installDiscordApi({
128
+ application: { id: "700000000000000001" },
129
+ });
130
+
131
+ const result = await setDiscordChannelConfig("bot-valid-token");
132
+
133
+ expect(result.success).toBe(true);
134
+ expect(result.connected).toBe(true);
135
+ expect(result.botUserId).toBe("900000000000000001");
136
+ expect(result.botUsername).toBe("test-bot");
137
+ expect(result.applicationId).toBe("700000000000000001");
138
+ expect(await getSecureKeyAsync(BOT_TOKEN_KEY)).toBe("bot-valid-token");
139
+
140
+ const read = await getDiscordChannelConfig();
141
+ expect(read.hasBotToken).toBe(true);
142
+ expect(read.botUsername).toBe("test-bot");
143
+ });
144
+
145
+ test("an app without install settings gets a parameterized invite link", async () => {
146
+ // The wizard's create step makes exactly this app: a client-id-only link
147
+ // would install nothing, so the link has to say what to grant.
148
+ installDiscordApi({ application: { id: "700000000000000001" } });
149
+
150
+ const result = await setDiscordChannelConfig("bot-valid-token");
151
+
152
+ const url = new URL(result.inviteUrl!);
153
+ expect(url.searchParams.get("client_id")).toBe("700000000000000001");
154
+ expect(url.searchParams.get("scope")).toBe("bot");
155
+ expect(url.searchParams.get("permissions")).toBe(INVITE_PERMISSIONS);
156
+ });
157
+
158
+ test("an app whose install settings grant bot gets a client-id-only link", async () => {
159
+ // The owner's Default Install Settings own the grant; parameters in the
160
+ // URL would silently override what they configured.
161
+ installDiscordApi({
162
+ application: {
163
+ id: "700000000000000001",
164
+ integration_types_config: {
165
+ "0": { oauth2_install_params: { scopes: ["bot"] } },
166
+ },
167
+ },
168
+ });
169
+
170
+ const result = await setDiscordChannelConfig("bot-valid-token");
171
+
172
+ const url = new URL(result.inviteUrl!);
173
+ expect(url.searchParams.get("client_id")).toBe("700000000000000001");
174
+ expect(url.searchParams.get("scope")).toBeNull();
175
+ expect(url.searchParams.get("permissions")).toBeNull();
176
+
177
+ // And the link survives a reload through the stored config.
178
+ const read = await getDiscordChannelConfig();
179
+ expect(read.inviteUrl).toBe(result.inviteUrl);
180
+ });
181
+
182
+ test("a rejected token stores nothing", async () => {
183
+ installDiscordApi({ meStatus: 401 });
184
+
185
+ const result = await setDiscordChannelConfig("bot-bad-token");
186
+
187
+ expect(result.success).toBe(false);
188
+ expect(result.hasBotToken).toBe(false);
189
+ expect(result.error).toContain("rejected");
190
+ expect(await getSecureKeyAsync(BOT_TOKEN_KEY)).toBeFalsy();
191
+ });
192
+ });
193
+
194
+ describe("disconnecting a Discord bot", () => {
195
+ test("clearing removes the token and the stored identity", async () => {
196
+ installDiscordApi({ application: { id: "700000000000000001" } });
197
+ await setDiscordChannelConfig("bot-valid-token");
198
+
199
+ const result = await clearDiscordChannelConfig();
200
+
201
+ expect(result.success).toBe(true);
202
+ expect(result.hasBotToken).toBe(false);
203
+ expect(result.connected).toBe(false);
204
+ expect(await getSecureKeyAsync(BOT_TOKEN_KEY)).toBeFalsy();
205
+ const read = await getDiscordChannelConfig();
206
+ expect(read.botUsername).toBeUndefined();
207
+ expect(read.inviteUrl).toBeUndefined();
208
+ });
209
+
210
+ test("a failed credential delete does not report a disconnect", async () => {
211
+ installDiscordApi({ application: { id: "700000000000000001" } });
212
+ await setDiscordChannelConfig("bot-valid-token");
213
+
214
+ const deleteSpy = spyOn(
215
+ secureKeys,
216
+ "deleteSecureKeyAsync",
217
+ ).mockResolvedValue("error");
218
+ try {
219
+ const result = await clearDiscordChannelConfig();
220
+ // The token is still stored, so the gateway's client stays connected: a
221
+ // success here would be a disconnect that did not happen.
222
+ expect(result.success).toBe(false);
223
+ expect(result.hasBotToken).toBe(true);
224
+ expect(result.connected).toBe(true);
225
+ } finally {
226
+ deleteSpy.mockRestore();
227
+ }
228
+ expect(await getSecureKeyAsync(BOT_TOKEN_KEY)).toBe("bot-valid-token");
229
+ });
230
+
231
+ test("clearing when nothing is stored is already disconnected", async () => {
232
+ const result = await clearDiscordChannelConfig();
233
+
234
+ expect(result.success).toBe(true);
235
+ expect(result.hasBotToken).toBe(false);
236
+ });
237
+ });