@vellumai/assistant 0.11.6 → 0.11.7-staging.2

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 (478) 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/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Connect and disconnect the Discord bot: the assistant's own identity in a
3
+ * server, not a grant to act as the person who authorized it. The user-scoped
4
+ * `discord` OAuth provider is a different surface and is untouched here.
5
+ *
6
+ * Storing the credential IS the connection. The gateway's Discord client is
7
+ * credential-gated (`gateway/src/index.ts`, "Discord Gateway lifecycle"): the
8
+ * watcher's next tick starts it when the token appears and tears it down when
9
+ * it goes, so there is no webhook to register and no restart to arrange.
10
+ */
11
+
12
+ import { CHANNEL_BOT_PROVIDER } from "@vellumai/service-contracts/channels";
13
+ import { z } from "zod";
14
+
15
+ import {
16
+ getConfig,
17
+ invalidateConfigCache,
18
+ loadRawConfig,
19
+ saveRawConfig,
20
+ setNestedValue,
21
+ } from "../../config/loader.js";
22
+ import { DISCORD_API_BASE_URL } from "../../messaging/providers/discord/api.js";
23
+ import {
24
+ ensureManualTokenConnection,
25
+ removeManualTokenConnection,
26
+ } from "../../oauth/manual-token-connection.js";
27
+ import { credentialKey } from "../../security/credential-key.js";
28
+ import {
29
+ deleteSecureKeyAsync,
30
+ getSecureKeyAsync,
31
+ setSecureKeyAsync,
32
+ } from "../../security/secure-keys.js";
33
+ import {
34
+ deleteCredentialMetadata,
35
+ upsertCredentialMetadata,
36
+ } from "../../tools/credentials/metadata-store.js";
37
+ import { ChannelConfigResultBaseSchema } from "./channel-config-result.js";
38
+ import { log } from "./shared.js";
39
+
40
+ /**
41
+ * From the shared vocabulary rather than a literal: `manual-token-providers`
42
+ * declares this bot's credential fields under the same constant, and a second
43
+ * spelling would be free to disagree with it.
44
+ */
45
+ const DISCORD_PROVIDER = CHANNEL_BOT_PROVIDER.discord;
46
+
47
+ /**
48
+ * The fields read back from Discord when a token validates. Parsed rather than
49
+ * cast: this runs at a network boundary and what it keeps is written into
50
+ * config, so a shape Discord did not send should fail here rather than persist
51
+ * as an undefined the reader silently tolerates.
52
+ */
53
+ const DiscordBotIdentitySchema = z.object({
54
+ id: z.string().optional(),
55
+ username: z.string().optional(),
56
+ });
57
+
58
+ /**
59
+ * The application read, including its own install settings. The portal's
60
+ * Installation page writes Default Install Settings to
61
+ * `integration_types_config["0"].oauth2_install_params` (`0` is
62
+ * GUILD_INSTALL); top-level `install_params` is the older in-app
63
+ * authorization link, still served for apps that configured it.
64
+ */
65
+ const DiscordInstallParamsSchema = z.object({
66
+ scopes: z.array(z.string()).optional(),
67
+ });
68
+ const DiscordApplicationSchema = z.object({
69
+ id: z.string().optional(),
70
+ install_params: DiscordInstallParamsSchema.optional(),
71
+ integration_types_config: z
72
+ .record(
73
+ z.string(),
74
+ z.object({
75
+ oauth2_install_params: DiscordInstallParamsSchema.optional(),
76
+ }),
77
+ )
78
+ .optional(),
79
+ });
80
+
81
+ /**
82
+ * The permission integer a parameterized invite link requests: View Channels,
83
+ * Send Messages, Send Messages in Threads, Embed Links, Attach Files, Read
84
+ * Message History, Add Reactions, Use External Emojis, Use Slash Commands.
85
+ * The setup skill's invite script derives the same integer from named bits;
86
+ * skills are import-isolated from this package, so the value is stated here
87
+ * and pinned equal by the connect test.
88
+ */
89
+ const DISCORD_INVITE_PERMISSIONS = "277025770560";
90
+
91
+ /**
92
+ * The install link for the application, total over every app state.
93
+ *
94
+ * An app whose own install settings grant the `bot` scope gets a link
95
+ * carrying only the client id, so the grant is whatever the owner configured
96
+ * and an edit on the portal's Installation page takes effect. Any other app,
97
+ * including the fresh one the wizard's create step just made, gets the
98
+ * parameterized link: a client-id-only link installs nothing without
99
+ * settings, and explicit scope and permission parameters are honored
100
+ * regardless of them.
101
+ */
102
+ function buildDiscordInviteUrl(
103
+ app: z.infer<typeof DiscordApplicationSchema>,
104
+ ): string | undefined {
105
+ if (!app.id) {
106
+ return undefined;
107
+ }
108
+ const url = new URL("https://discord.com/oauth2/authorize");
109
+ url.searchParams.set("client_id", app.id);
110
+ const settingsScopes =
111
+ app.integration_types_config?.["0"]?.oauth2_install_params?.scopes ??
112
+ app.install_params?.scopes ??
113
+ [];
114
+ if (!settingsScopes.includes("bot")) {
115
+ url.searchParams.set("permissions", DISCORD_INVITE_PERMISSIONS);
116
+ url.searchParams.set("scope", "bot");
117
+ }
118
+ return url.toString();
119
+ }
120
+
121
+ export const DiscordChannelConfigResultSchema =
122
+ ChannelConfigResultBaseSchema.extend({
123
+ /** The bot's own account, read back from Discord when the token validates. */
124
+ botUserId: z.string().optional(),
125
+ botUsername: z.string().optional(),
126
+ /**
127
+ * The application the bot belongs to. Read from Discord rather than
128
+ * assumed equal to the bot user id: they coincide today, and anything
129
+ * built on that assumption breaks silently if it ever stops being true.
130
+ */
131
+ applicationId: z.string().optional(),
132
+ /**
133
+ * The install link for the bot's application, computed daemon-side from
134
+ * the application's own install settings so no client re-derives the
135
+ * grant rules. Absent until a token has validated.
136
+ */
137
+ inviteUrl: z.string().optional(),
138
+ });
139
+
140
+ export type DiscordChannelConfigResult = z.infer<
141
+ typeof DiscordChannelConfigResultSchema
142
+ >;
143
+
144
+ /**
145
+ * A failed operation still reports the current state. For Discord "connected"
146
+ * and "has a token" are one question, since the gateway's client exists while
147
+ * the credential does.
148
+ */
149
+ async function failure(error: string): Promise<DiscordChannelConfigResult> {
150
+ const hasToken = await hasStoredBotToken();
151
+ return { success: false, hasBotToken: hasToken, connected: hasToken, error };
152
+ }
153
+
154
+ async function hasStoredBotToken(): Promise<boolean> {
155
+ return Boolean(
156
+ await getSecureKeyAsync(credentialKey(DISCORD_PROVIDER, "bot_token")),
157
+ );
158
+ }
159
+
160
+ /** Current state, without touching anything. */
161
+ export async function getDiscordChannelConfig(): Promise<DiscordChannelConfigResult> {
162
+ const { discord } = getConfig();
163
+ const hasToken = await hasStoredBotToken();
164
+ return {
165
+ success: true,
166
+ hasBotToken: hasToken,
167
+ connected: hasToken,
168
+ ...(discord.botUserId ? { botUserId: discord.botUserId } : {}),
169
+ ...(discord.botUsername ? { botUsername: discord.botUsername } : {}),
170
+ ...(discord.applicationId ? { applicationId: discord.applicationId } : {}),
171
+ ...(discord.inviteUrl ? { inviteUrl: discord.inviteUrl } : {}),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Validate a bot token with Discord and store it.
177
+ *
178
+ * Recording the bot's own account is what lets a connected panel name it
179
+ * rather than say "connected": the gateway learns that identity only at
180
+ * READY, which no caller here can wait for.
181
+ */
182
+ export async function setDiscordChannelConfig(
183
+ botToken?: string,
184
+ ): Promise<DiscordChannelConfigResult> {
185
+ if (!botToken?.trim()) {
186
+ return failure("botToken is required");
187
+ }
188
+ const token = botToken.trim();
189
+
190
+ let botUserId: string | undefined;
191
+ let botUsername: string | undefined;
192
+ let applicationId: string | undefined;
193
+ let inviteUrl: string | undefined;
194
+ try {
195
+ // Not `callDiscordApi`: that resolves the stored credential, and this
196
+ // token is not stored yet. Validating first is what keeps an invalid one
197
+ // out of secure storage, where it would start a connection that cannot
198
+ // authenticate.
199
+ const headers = { Authorization: `Bot ${token}` };
200
+ const signal = () => AbortSignal.timeout(10_000);
201
+ // Both answer from the token alone, so they go together. The application
202
+ // read is optional and swallows its own failure: it enriches the result
203
+ // and must never be the reason a valid token is rejected.
204
+ const [res, appRes] = await Promise.all([
205
+ fetch(`${DISCORD_API_BASE_URL}/users/@me`, {
206
+ headers,
207
+ signal: signal(),
208
+ }),
209
+ fetch(`${DISCORD_API_BASE_URL}/oauth2/applications/@me`, {
210
+ headers,
211
+ signal: signal(),
212
+ }).catch(() => null),
213
+ ]);
214
+ if (!res.ok) {
215
+ throw new Error(`Discord returned ${res.status}`);
216
+ }
217
+ const me = DiscordBotIdentitySchema.parse(await res.json());
218
+ botUserId = me.id;
219
+ botUsername = me.username;
220
+
221
+ if (appRes?.ok) {
222
+ const app = DiscordApplicationSchema.safeParse(await appRes.json());
223
+ if (app.success) {
224
+ applicationId = app.data.id;
225
+ inviteUrl = buildDiscordInviteUrl(app.data);
226
+ }
227
+ }
228
+ } catch (err) {
229
+ // The body is not echoed: this path runs with a secret in hand, and the
230
+ // caller only needs to know the token was rejected.
231
+ log.warn({ err }, "Discord bot token validation failed");
232
+ return failure(
233
+ "Discord rejected this bot token. Check it was copied from the Bot tab of your application, and reset it there if it was shown before.",
234
+ );
235
+ }
236
+
237
+ const stored = await setSecureKeyAsync(
238
+ credentialKey(DISCORD_PROVIDER, "bot_token"),
239
+ token,
240
+ );
241
+ if (!stored) {
242
+ return failure("Failed to store the bot token in secure storage");
243
+ }
244
+ upsertCredentialMetadata(DISCORD_PROVIDER, "bot_token", {});
245
+
246
+ const raw = loadRawConfig();
247
+ setNestedValue(raw, "discord.botUserId", botUserId ?? "");
248
+ setNestedValue(raw, "discord.botUsername", botUsername ?? "");
249
+ setNestedValue(raw, "discord.applicationId", applicationId ?? "");
250
+ setNestedValue(raw, "discord.inviteUrl", inviteUrl ?? "");
251
+ saveRawConfig(raw);
252
+ invalidateConfigCache();
253
+
254
+ await ensureManualTokenConnection(
255
+ DISCORD_PROVIDER,
256
+ botUsername ? `@${botUsername}` : undefined,
257
+ );
258
+
259
+ log.info({ botUserId, botUsername }, "Discord bot token stored");
260
+ return {
261
+ success: true,
262
+ hasBotToken: true,
263
+ connected: true,
264
+ ...(botUserId ? { botUserId } : {}),
265
+ ...(botUsername ? { botUsername } : {}),
266
+ ...(applicationId ? { applicationId } : {}),
267
+ ...(inviteUrl ? { inviteUrl } : {}),
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Clear the stored credential, which disconnects the bot.
273
+ *
274
+ * A legacy `discord.allowedChannelIds` entry, where one exists, is left
275
+ * alone on purpose: the gateway still enforces it as persisted operator
276
+ * intent, so reconnecting the same bot must not silently widen its room
277
+ * scope by clearing it here.
278
+ */
279
+ export async function clearDiscordChannelConfig(): Promise<DiscordChannelConfigResult> {
280
+ const deleted = await deleteSecureKeyAsync(
281
+ credentialKey(DISCORD_PROVIDER, "bot_token"),
282
+ );
283
+ if (deleted === "error") {
284
+ // The token is still stored, so the gateway's client stays connected: a
285
+ // success here would report a disconnect that did not happen.
286
+ return failure("Failed to delete the bot token from secure storage");
287
+ }
288
+ deleteCredentialMetadata(DISCORD_PROVIDER, "bot_token");
289
+ await removeManualTokenConnection(DISCORD_PROVIDER);
290
+
291
+ const raw = loadRawConfig();
292
+ setNestedValue(raw, "discord.botUserId", "");
293
+ setNestedValue(raw, "discord.botUsername", "");
294
+ setNestedValue(raw, "discord.applicationId", "");
295
+ setNestedValue(raw, "discord.inviteUrl", "");
296
+ saveRawConfig(raw);
297
+ invalidateConfigCache();
298
+
299
+ log.info("Discord bot token cleared");
300
+ return { success: true, hasBotToken: false, connected: false };
301
+ }
@@ -5,6 +5,7 @@ import {
5
5
  normalizePublicBaseUrl,
6
6
  parseRecordedAssistantId,
7
7
  parseTunnelRecord,
8
+ selectRestartTunnelRecord,
8
9
  type TunnelRecord,
9
10
  } from "@vellumai/service-contracts/ingress";
10
11
 
@@ -60,6 +61,14 @@ export function loadPairingTunnelRecord(): TunnelRecord | null {
60
61
  return parseTunnelRecord(readIngressSection()[INGRESS_PAIRING_TUNNEL_KEY]);
61
62
  }
62
63
 
64
+ /**
65
+ * Read the tunnel a reader should offer to restart: the pairing-only record
66
+ * when one exists, else the last that ran.
67
+ */
68
+ export function loadRestartTunnelRecord(): TunnelRecord | null {
69
+ return selectRestartTunnelRecord(readIngressSection());
70
+ }
71
+
63
72
  /**
64
73
  * Read the assistant id the last tunnel fronted, or null when absent. The
65
74
  * daemon cannot derive it: it scopes itself as `self` internally.
@@ -24,6 +24,7 @@ import {
24
24
  getCredentialMetadata,
25
25
  upsertCredentialMetadata,
26
26
  } from "../../tools/credentials/metadata-store.js";
27
+ import { ChannelConfigResultBaseSchema } from "./channel-config-result.js";
27
28
  import { log as _log } from "./shared.js";
28
29
 
29
30
  // -- Result type --
@@ -31,21 +32,18 @@ import { log as _log } from "./shared.js";
31
32
  export const SlackThreadMode = z.enum(["mention_only", "mention_then_thread"]);
32
33
  export type SlackThreadMode = z.infer<typeof SlackThreadMode>;
33
34
 
34
- export const SlackChannelConfigResultSchema = z.object({
35
- success: z.boolean(),
36
- hasBotToken: z.boolean(),
37
- hasAppToken: z.boolean(),
38
- hasUserToken: z.boolean(),
39
- connected: z.boolean(),
40
- teamId: z.string().optional(),
41
- teamName: z.string().optional(),
42
- teamUrl: z.string().optional(),
43
- botUserId: z.string().optional(),
44
- botUsername: z.string().optional(),
45
- threadMode: SlackThreadMode,
46
- error: z.string().optional(),
47
- warning: z.string().optional(),
48
- });
35
+ export const SlackChannelConfigResultSchema =
36
+ ChannelConfigResultBaseSchema.extend({
37
+ hasAppToken: z.boolean(),
38
+ hasUserToken: z.boolean(),
39
+ teamId: z.string().optional(),
40
+ teamName: z.string().optional(),
41
+ teamUrl: z.string().optional(),
42
+ botUserId: z.string().optional(),
43
+ botUsername: z.string().optional(),
44
+ threadMode: SlackThreadMode,
45
+ warning: z.string().optional(),
46
+ });
49
47
 
50
48
  export type SlackChannelConfigResult = z.infer<
51
49
  typeof SlackChannelConfigResultSchema
@@ -28,6 +28,7 @@ import {
28
28
  deleteCredentialMetadata,
29
29
  upsertCredentialMetadata,
30
30
  } from "../../tools/credentials/metadata-store.js";
31
+ import { ChannelConfigResultBaseSchema } from "./channel-config-result.js";
31
32
  import { log } from "./shared.js";
32
33
 
33
34
  const TELEGRAM_BOT_TOKEN_IN_URL_PATTERN =
@@ -61,15 +62,11 @@ function summarizeTelegramError(err: unknown): string {
61
62
 
62
63
  // -- Transport-agnostic result type (omits the `type` discriminant) --
63
64
 
64
- export const TelegramConfigResultSchema = z.object({
65
- success: z.boolean(),
66
- hasBotToken: z.boolean(),
65
+ export const TelegramConfigResultSchema = ChannelConfigResultBaseSchema.extend({
67
66
  botId: z.string().optional(),
68
67
  botUsername: z.string().optional(),
69
- connected: z.boolean(),
70
68
  hasWebhookSecret: z.boolean(),
71
69
  lastError: z.string().optional(),
72
- error: z.string().optional(),
73
70
  commandsRegistered: z.array(z.string()).optional(),
74
71
  warning: z.string().optional(),
75
72
  });
@@ -43,8 +43,6 @@ export function handleConfirmationResponse(msg: ConfirmationResponse): void {
43
43
  if (conversation.hasPendingConfirmation(msg.requestId)) {
44
44
  touchConversation(conversationId);
45
45
  conversation.handleConfirmationResponse(msg.requestId, decision, {
46
- selectedPattern: msg.selectedPattern,
47
- selectedScope: msg.selectedScope,
48
46
  emissionContext: { source: "button" },
49
47
  });
50
48
  return;
@@ -67,6 +67,11 @@ import {
67
67
  deleteManagedSkill,
68
68
  validateManagedSkillId,
69
69
  } from "../../skills/managed-store.js";
70
+ import {
71
+ filterSkillsByPlatform,
72
+ isSkillCompatibleWithPlatform,
73
+ skillPlatformUnavailableMessage,
74
+ } from "../../skills/platform-compatibility.js";
70
75
  import type { SkillFileProvider } from "../../skills/skill-file-provider.js";
71
76
  import { createSkillsShProvider } from "../../skills/skillssh-files.js";
72
77
  import type { SkillAuditData } from "../../skills/skillssh-registry.js";
@@ -475,7 +480,7 @@ async function listSkillsWithCatalog(): Promise<SlimSkillResponse[]> {
475
480
 
476
481
  // All entries from the Vellum platform API are first-party.
477
482
  // Create SlimSkillResponses for catalog skills not already installed.
478
- const available: SlimSkillResponse[] = catalogSkills
483
+ const available: SlimSkillResponse[] = filterSkillsByPlatform(catalogSkills)
479
484
  .filter((cs) => !installedIds.has(cs.id))
480
485
  .map((cs) => catalogSkillToSlim(cs));
481
486
 
@@ -1227,11 +1232,17 @@ export async function installSkill(spec: {
1227
1232
  // Bundled skills are already available — no install needed
1228
1233
  const catalog = loadSkillCatalog();
1229
1234
 
1230
- // Feature flag gate: reject install if the skill's flag is disabled
1235
+ // Reject install if the skill is unsupported on this host or its flag is disabled
1231
1236
  const config = getConfig();
1232
- const flaggedSkill = catalog.find((s) => s.id === spec.slug);
1233
- if (flaggedSkill) {
1234
- const flagKey = skillFlagKey(flaggedSkill);
1237
+ const catalogSkill = catalog.find((s) => s.id === spec.slug);
1238
+ if (catalogSkill) {
1239
+ if (!isSkillCompatibleWithPlatform(catalogSkill)) {
1240
+ return {
1241
+ success: false,
1242
+ error: skillPlatformUnavailableMessage(spec.slug, catalogSkill),
1243
+ };
1244
+ }
1245
+ const flagKey = skillFlagKey(catalogSkill);
1235
1246
  if (flagKey && !isAssistantFeatureFlagEnabled(flagKey, config)) {
1236
1247
  return {
1237
1248
  success: false,
@@ -1277,6 +1288,12 @@ export async function installSkill(spec: {
1277
1288
  const vellumCatalog = await getCatalog();
1278
1289
  const catalogEntry = vellumCatalog.find((s) => s.id === spec.slug);
1279
1290
  if (catalogEntry) {
1291
+ if (!isSkillCompatibleWithPlatform(catalogEntry)) {
1292
+ return {
1293
+ success: false,
1294
+ error: skillPlatformUnavailableMessage(spec.slug, catalogEntry),
1295
+ };
1296
+ }
1280
1297
  // Default `overwrite` to true at the handler boundary to preserve
1281
1298
  // pre-existing HTTP API behaviour. CLI callers always pass an
1282
1299
  // explicit boolean (`opts.overwrite ?? false`) so the CLI surface
@@ -1476,10 +1493,11 @@ export async function searchSkills(
1476
1493
  // hard-coded as catalog/available.
1477
1494
  const catalog = loadSkillCatalog();
1478
1495
  const config = getConfig();
1479
- const resolved = resolveSkillStates(catalog, config);
1496
+ const compatibleCatalog = filterSkillsByPlatform(catalog);
1497
+ const resolved = resolveSkillStates(compatibleCatalog, config);
1480
1498
  const resolvedById = new Map(resolved.map((r) => [r.summary.id, r]));
1481
1499
 
1482
- const catalogMatches = filterByQuery(catalog, query, [
1500
+ const catalogMatches = filterByQuery(compatibleCatalog, query, [
1483
1501
  (s) => s.id,
1484
1502
  (s) => s.displayName,
1485
1503
  (s) => s.description,
@@ -32,6 +32,7 @@ import { maybeEnqueueLexicalBackfillOnUpgrade } from "../persistence/job-handler
32
32
  import { clearLifecycleQuiesce } from "../persistence/lifecycle-quiesce.js";
33
33
  import { isPlatformClientConfigured } from "../platform/client.js";
34
34
  import { startConsentRefresh } from "../platform/consent-cache.js";
35
+ import { syncAvatarToPlatform } from "../platform/sync-avatar.js";
35
36
  import { syncWorkspaceIdentityToPlatform } from "../platform/sync-identity.js";
36
37
  import { ensurePromptFiles } from "../prompts/system-prompt.js";
37
38
  import { runProviderConnectionsBackfill } from "../providers/inference/backfill.js";
@@ -695,9 +696,10 @@ export async function runDaemon(): Promise<void> {
695
696
 
696
697
  // Initialize providers before Qdrant so HTTP routes can begin accepting
697
698
  // requests while Qdrant initializes, then best-effort sync the workspace
698
- // identity name to the platform record.
699
+ // identity name and avatar to the platform record.
699
700
  await initializeProviders(config);
700
701
  syncWorkspaceIdentityToPlatform();
702
+ syncAvatarToPlatform();
701
703
 
702
704
  // Start the idle/LRU/memory-pressure sweep over the in-memory conversation
703
705
  // pool.
@@ -33,8 +33,6 @@ export interface ConfirmationResponse {
33
33
  type: "confirmation_response";
34
34
  requestId: string;
35
35
  decision: "allow" | "deny";
36
- selectedPattern?: string;
37
- selectedScope?: string;
38
36
  }
39
37
 
40
38
  export interface SecretResponse {
@@ -3,7 +3,7 @@
3
3
  * tool-setup ↔ doordash-steps and tool-setup ↔ tool-side-effects cycles.
4
4
  */
5
5
 
6
- import type { InterfaceId } from "../channels/types.js";
6
+ import type { ClientOs, InterfaceId } from "../channels/types.js";
7
7
 
8
8
  /**
9
9
  * How a subagent/wake tool allowlist is enforced.
@@ -51,8 +51,8 @@ export interface SubagentToolStats {
51
51
  * transport interface, no channel capabilities), which drops client-gated
52
52
  * tools (`host_*`, `ui_*`, `ask_question`, `request_system_permission`) from
53
53
  * the wire definitions and breaks the cache prefix anyway. When this pin is
54
- * set on the conversation, `isToolActiveForContext` reads `hasNoClient` and
55
- * `transportInterface` exclusively from the pin and treats channel
54
+ * set on the conversation, `isToolActiveForContext` reads `hasNoClient`,
55
+ * `transportInterface`, and `clientOs` exclusively from the pin and treats channel
56
56
  * capabilities as unset — an absent optional field pins the value to
57
57
  * `undefined`; there is no fall-through to the live conversation state.
58
58
  * (Interactive-interface turns never set channel capabilities, so unset IS
@@ -70,6 +70,8 @@ export interface WakeToolContextPin {
70
70
  hasNoClient: boolean;
71
71
  /** The interface the source's live turns ran on (e.g. `"macos"`). */
72
72
  transportInterface?: InterfaceId;
73
+ /** The client OS the source's live turns reported. */
74
+ clientOs?: ClientOs;
73
75
  /**
74
76
  * Origin tag stamped onto `ToolContext.requestOrigin` for the duration of
75
77
  * the wake (e.g. `"memory_retrospective"`). Wakes bypass `runAgentLoopImpl`,
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ sameTrustIdentity,
5
+ type TrustContext,
6
+ } from "./trust-context-types.js";
7
+
8
+ function contactTrust(
9
+ overrides: Partial<TrustContext> = {},
10
+ ): TrustContext {
11
+ return {
12
+ trustClass: "trusted_contact",
13
+ sourceChannel: "slack",
14
+ requesterExternalUserId: "U12345678",
15
+ requesterContactId: "contact-abc",
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ describe("sameTrustIdentity", () => {
21
+ test("two identical contact contexts compare equal", () => {
22
+ expect(sameTrustIdentity(contactTrust(), contactTrust())).toBe(true);
23
+ });
24
+
25
+ test("a different contact id is a different actor", () => {
26
+ expect(
27
+ sameTrustIdentity(
28
+ contactTrust({ requesterContactId: "contact-abc" }),
29
+ contactTrust({ requesterContactId: "contact-xyz" }),
30
+ ),
31
+ ).toBe(false);
32
+ });
33
+ });
@@ -75,7 +75,6 @@ mock.module("../../util/platform.js", () => ({
75
75
  getSandboxWorkingDir: () => join(workspaceDir ?? fallbackDir, "sandbox/work"),
76
76
  getSoundsDir: () => join(workspaceDir ?? fallbackDir, "sounds"),
77
77
  getAvatarDir: () => join(workspaceDir ?? fallbackDir, "avatar"),
78
- AVATAR_IMAGE_FILENAME: "avatar-image.png",
79
78
  getAvatarImagePath: () =>
80
79
  join(workspaceDir ?? fallbackDir, "avatar/avatar-image.png"),
81
80
 
@@ -277,6 +277,13 @@ const FLUX_ON = {
277
277
  eotTimeoutMs: 500,
278
278
  } as const satisfies Partial<LiveVoiceFluxConfig>;
279
279
 
280
+ // Turn-end is on by schema default, so a session that must run the local
281
+ // silence boundary has to say so. Naming it keeps those tests about the
282
+ // boundary they exercise rather than about a default they no longer own.
283
+ const FLUX_OFF = {
284
+ turnEnd: { enabled: false },
285
+ } as const satisfies Partial<LiveVoiceFluxConfig>;
286
+
280
287
  describe("LiveVoiceSession Flux end-of-turn", () => {
281
288
  test("commits the turn on turn-end without an endpoint-decision leg", async () => {
282
289
  const { frames, session, transcribers, turnCalls } = createHarness({
@@ -331,6 +338,7 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
331
338
 
332
339
  test("ignores turn-end when the flag is off and keeps the hold path", async () => {
333
340
  const { frames, session, transcribers, turnCalls } = createHarness({
341
+ fluxConfig: FLUX_OFF,
334
342
  startVoiceTurn: autoCompletingTurn(),
335
343
  });
336
344
 
@@ -420,8 +428,9 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
420
428
 
421
429
  test("records the commit latency on the front-door path too, with no hold", async () => {
422
430
  const { frames, session, transcribers, turnCalls } = createHarness({
423
- // No flux config: the latch is down and the local silence boundary owns
431
+ // Turn-end off: the latch is down and the local silence boundary owns
424
432
  // the commit, which is arm A of the measurement run.
433
+ fluxConfig: FLUX_OFF,
425
434
  silenceThresholdMs: 40,
426
435
  emitMetrics: true,
427
436
  startVoiceTurn: autoCompletingTurn(),
@@ -823,7 +832,10 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
823
832
  describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
824
833
  beforeEach(() => {
825
834
  setConfig("services", {
826
- stt: { provider: "deepgram-flux", providers: {} },
835
+ stt: {
836
+ provider: "deepgram",
837
+ providers: { deepgram: { model: "flux" } },
838
+ },
827
839
  });
828
840
  });
829
841
 
@@ -912,11 +924,53 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
912
924
  await session.close("client_end");
913
925
  });
914
926
 
927
+ test("seeds the latch from the live-voice role, not the global provider", async () => {
928
+ // The configuration roles exist for: live voice on flux while the global
929
+ // stays on a family that owns no turn boundary. Seeding from the global
930
+ // leaves the latch false, and the opening utterance closes its silence
931
+ // boundary on the caller's side before the dial can correct it.
932
+ setConfig("services", {
933
+ stt: {
934
+ provider: "deepgram",
935
+ providers: { deepgram: {} },
936
+ roles: { liveVoice: { provider: "deepgram", model: "flux" } },
937
+ },
938
+ });
939
+ const gate = createDialGate();
940
+ const { frames, session, transcribers, turnCalls } = createHarness({
941
+ fluxConfig: FLUX_ON,
942
+ silenceThresholdMs: 40,
943
+ resolveGate: gate.promise,
944
+ startVoiceTurn: autoCompletingTurn(),
945
+ });
946
+
947
+ await session.start();
948
+ await session.handleBinaryAudio(LOUD_CHUNK);
949
+ await sleep(PAST_SILENCE_BOUNDARY_MS);
950
+
951
+ // Deferred under the seeded latch: Flux owns this boundary.
952
+ expect(countFrames(frames, "utterance_end")).toBe(0);
953
+ expect(turnCalls).toHaveLength(0);
954
+
955
+ gate.open();
956
+ await waitFor(() => transcribers.length > 0);
957
+ transcribers[0]?.endOfTurn("what is the weather");
958
+
959
+ await waitFor(
960
+ () => turnCalls.length === 1,
961
+ "The opening utterance never committed on the Flux end-of-turn",
962
+ );
963
+ expect(turnCalls[0]?.content).toBe("what is the weather");
964
+ expect(countFrames(frames, "utterance_end")).toBe(1);
965
+
966
+ await session.close("client_end");
967
+ });
968
+
915
969
  test("never seeds the latch when the flag is off", async () => {
916
970
  const gate = createDialGate();
917
971
  const { frames, session, transcribers, turnCalls } = createHarness({
918
- // No fluxConfig: `turnEnd.enabled` keeps its schema default of false
919
- // while config still names deepgram-flux as the provider.
972
+ // Turn-end explicitly off while config still selects the flux family.
973
+ fluxConfig: FLUX_OFF,
920
974
  silenceThresholdMs: 40,
921
975
  resolveGate: gate.promise,
922
976
  startVoiceTurn: autoCompletingTurn(),