@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.6",
3
+ "version": "0.11.7-staging.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,6 +43,7 @@
43
43
  "@qdrant/js-client-rest": "1.17.0",
44
44
  "@resvg/resvg-js": "2.6.2",
45
45
  "@slack/types": "2.22.0",
46
+ "@vellumai/avatar-manifest": "file:../packages/avatar-manifest",
46
47
  "@vellumai/ces-client": "file:../packages/ces-client",
47
48
  "@vellumai/credential-storage": "file:../packages/credential-storage",
48
49
  "@vellumai/egress-proxy": "file:../packages/egress-proxy",
@@ -72,6 +73,7 @@
72
73
  "remark-parse": "11.0.0",
73
74
  "rrule": "2.8.1",
74
75
  "semver": "7.8.0",
76
+ "sharp": "0.34.5",
75
77
  "stemmer": "2.0.1",
76
78
  "tar-stream": "3.1.7",
77
79
  "tldts": "7.0.25",
@@ -85,6 +87,7 @@
85
87
  "node": ">=20.12.0"
86
88
  },
87
89
  "bundledDependencies": [
90
+ "@vellumai/avatar-manifest",
88
91
  "@vellumai/ces-client",
89
92
  "@vellumai/credential-storage",
90
93
  "@vellumai/service-contracts",
@@ -86,6 +86,9 @@ function projectModel(model: CatalogModel): Record<string, unknown> {
86
86
  if (model.pricing !== undefined) {
87
87
  projected.pricing = model.pricing;
88
88
  }
89
+ if (model.featureFlag !== undefined) {
90
+ projected.featureFlag = model.featureFlag;
91
+ }
89
92
  return projected;
90
93
  }
91
94
 
@@ -115,6 +118,9 @@ function projectProvider(entry: ProviderCatalogEntry): Record<string, unknown> {
115
118
  if (entry.supportsPlatformAuth !== undefined) {
116
119
  projected.supportsPlatformAuth = entry.supportsPlatformAuth;
117
120
  }
121
+ if (entry.featureFlag !== undefined) {
122
+ projected.featureFlag = entry.featureFlag;
123
+ }
118
124
  projected.defaultModel = entry.defaultModel;
119
125
  projected.models = entry.models.map(projectModel);
120
126
  // NOTE: `apiKeyUrl` intentionally omitted — clients use
@@ -0,0 +1,199 @@
1
+ /**
2
+ * The inbound handoff of the approval-card message id, driven through the
3
+ * real handler.
4
+ *
5
+ * The companion suite (approval-card-edit.test.ts) pins the interception
6
+ * seam but injects `approvalMessageId` directly, so it stays green if the
7
+ * inbound handler stops reading `sourceMetadata.messageId` off the wire.
8
+ * This suite closes that gap: a Telegram button decision enters through
9
+ * `handleChannelInbound` exactly as the gateway forwards it, and the edit
10
+ * must address the message id the wire carried.
11
+ */
12
+ import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
13
+
14
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
15
+
16
+ const _conversationMocks = new Map<string, unknown>();
17
+ mock.module("../daemon/conversation-registry.js", () => ({
18
+ findConversation: (id: string) => _conversationMocks.get(id),
19
+ }));
20
+
21
+ mock.module("../runtime/local-actor-identity.js", () => ({
22
+ findLocalGuardianPrincipalId: async () => "vellum-principal-1",
23
+ }));
24
+
25
+ import type { TrustVerdict } from "@vellumai/gateway-client";
26
+
27
+ import type { Conversation } from "../daemon/conversation.js";
28
+ import * as providers from "../messaging/providers/index.js";
29
+ import { getDb } from "../persistence/db-connection.js";
30
+ import { initializeDb } from "../persistence/db-init.js";
31
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
32
+ import {
33
+ handleChannelInbound,
34
+ setAdapterProcessMessage,
35
+ } from "./helpers/channel-test-adapter.js";
36
+
37
+ await initializeDb();
38
+
39
+ const CHAT_ID = "chat-123";
40
+ const GUARDIAN = "user-1";
41
+ const BUTTON_MESSAGE_ID = "4242";
42
+ const CALLBACK_URL = "https://gateway.test/deliver/telegram";
43
+
44
+ function resetTables(): void {
45
+ const db = getDb();
46
+ db.run("DELETE FROM channel_inbound_events");
47
+ db.run("DELETE FROM conversation_keys");
48
+ db.run("DELETE FROM messages");
49
+ db.run("DELETE FROM conversations");
50
+ db.run("DELETE FROM external_conversation_bindings");
51
+ db.run("DELETE FROM contact_channels");
52
+ db.run("DELETE FROM contacts");
53
+ }
54
+
55
+ function guardianVerdict(): TrustVerdict {
56
+ return {
57
+ trustClass: "guardian",
58
+ canonicalSenderId: GUARDIAN,
59
+ contactId: "contact-1",
60
+ channelId: "channel-1",
61
+ type: "telegram",
62
+ address: GUARDIAN,
63
+ externalChatId: CHAT_ID,
64
+ status: "active",
65
+ policy: "allow",
66
+ guardianExternalUserId: GUARDIAN,
67
+ guardianDeliveryChatId: CHAT_ID,
68
+ guardianPrincipalId: "vellum-principal-1",
69
+ } satisfies TrustVerdict;
70
+ }
71
+
72
+ function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
73
+ return new Request("http://localhost/channels/inbound", {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ "X-Gateway-Origin": "test-token",
78
+ },
79
+ body: JSON.stringify({
80
+ sourceChannel: "telegram",
81
+ interface: "telegram",
82
+ conversationExternalId: CHAT_ID,
83
+ externalMessageId: `msg-${Date.now()}-${Math.random()
84
+ .toString(36)
85
+ .slice(2, 8)}`,
86
+ content: "hello",
87
+ actorExternalId: GUARDIAN,
88
+ replyCallbackUrl: CALLBACK_URL,
89
+ sourceMetadata: { trustVerdict: guardianVerdict() },
90
+ ...overrides,
91
+ }),
92
+ });
93
+ }
94
+
95
+ function conversationIdFromInboundEvents(): string {
96
+ const rows = getDb()
97
+ .$client.prepare("SELECT conversation_id FROM channel_inbound_events")
98
+ .all() as Array<{ conversation_id: string }>;
99
+ expect(rows.length).toBeGreaterThan(0);
100
+ return rows[0].conversation_id;
101
+ }
102
+
103
+ function registerPendingInteraction(
104
+ requestId: string,
105
+ conversationId: string,
106
+ ): void {
107
+ const _mockSession = {
108
+ handleConfirmationResponse: mock(() => {}),
109
+ ensureActorScopedHistory: async () => {},
110
+ } as unknown as Conversation;
111
+ _conversationMocks.set(conversationId, _mockSession);
112
+
113
+ pendingInteractions.register(requestId, {
114
+ conversationId,
115
+ kind: "confirmation",
116
+ confirmationDetails: {
117
+ toolName: "execute_shell",
118
+ input: { command: "ls" },
119
+ riskLevel: "high",
120
+ allowlistOptions: [
121
+ { label: "test", description: "test", pattern: "test" },
122
+ ],
123
+ scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
124
+ },
125
+ });
126
+ }
127
+
128
+ describe("approval-card edit: inbound message-id handoff", () => {
129
+ let editSpy: ReturnType<typeof spyOn>;
130
+
131
+ beforeEach(() => {
132
+ resetTables();
133
+ pendingInteractions.clear();
134
+ _conversationMocks.clear();
135
+ setAdapterProcessMessage(undefined);
136
+ editSpy = spyOn(providers, "editChannelMessage").mockResolvedValue({
137
+ ok: true,
138
+ });
139
+ });
140
+
141
+ test("a Telegram button press edits the message id the wire carried", async () => {
142
+ // First inbound materializes the conversation the pending approval
143
+ // will hang off, the way a real approval prompt's conversation exists
144
+ // before its buttons are pressed.
145
+ const first = await handleChannelInbound(makeInboundRequest());
146
+ expect(((await first.json()) as { accepted?: boolean }).accepted).toBe(
147
+ true,
148
+ );
149
+ const conversationId = conversationIdFromInboundEvents();
150
+ registerPendingInteraction("req-handoff-1", conversationId);
151
+
152
+ // The gateway forwards a button press with the keyboard message's id on
153
+ // sourceMetadata.messageId; nothing in this request names the id any
154
+ // other way, so the edit below proves the handler read it off the wire.
155
+ const res = await handleChannelInbound(
156
+ makeInboundRequest({
157
+ content: "",
158
+ callbackData: "apr:req-handoff-1:approve_once",
159
+ sourceMetadata: {
160
+ trustVerdict: guardianVerdict(),
161
+ messageId: BUTTON_MESSAGE_ID,
162
+ },
163
+ }),
164
+ );
165
+ expect(res.status).toBe(200);
166
+
167
+ expect(editSpy).toHaveBeenCalledTimes(1);
168
+ const [callbackUrl, payload] = editSpy.mock.calls[0];
169
+ expect(callbackUrl).toBe(CALLBACK_URL);
170
+ expect(payload.chatId).toBe(CHAT_ID);
171
+ expect(payload.messageId).toBe(BUTTON_MESSAGE_ID);
172
+ expect(payload.text).toContain("Approved");
173
+
174
+ editSpy.mockRestore();
175
+ });
176
+
177
+ test("a button press whose wire carries no message id edits nothing", async () => {
178
+ // The inverse pin: the edit is driven by the wire's id and only by it.
179
+ const first = await handleChannelInbound(makeInboundRequest());
180
+ expect(((await first.json()) as { accepted?: boolean }).accepted).toBe(
181
+ true,
182
+ );
183
+ const conversationId = conversationIdFromInboundEvents();
184
+ registerPendingInteraction("req-handoff-2", conversationId);
185
+
186
+ const res = await handleChannelInbound(
187
+ makeInboundRequest({
188
+ content: "",
189
+ callbackData: "apr:req-handoff-2:approve_once",
190
+ sourceMetadata: { trustVerdict: guardianVerdict() },
191
+ }),
192
+ );
193
+ expect(res.status).toBe(200);
194
+
195
+ expect(editSpy).not.toHaveBeenCalled();
196
+
197
+ editSpy.mockRestore();
198
+ });
199
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Approval-card editing after a button decision, across channels.
3
+ *
4
+ * The card edit is routed by the reply callback URL through the channel edit
5
+ * capability, so the same interception path serves every channel whose
6
+ * transport implements `edit`. These tests drive the Telegram shape end to
7
+ * end at this seam: the gateway forwards the id of the message holding the
8
+ * inline keyboard, and a decision or a stale press must edit exactly that
9
+ * message.
10
+ */
11
+ import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
12
+
13
+ const _conversationMocks = new Map<string, unknown>();
14
+ mock.module("../daemon/conversation-registry.js", () => ({
15
+ findConversation: (id: string) => _conversationMocks.get(id),
16
+ }));
17
+
18
+ let _anchorPrincipalId: string | undefined;
19
+ mock.module("../runtime/local-actor-identity.js", () => ({
20
+ findLocalGuardianPrincipalId: async () => _anchorPrincipalId,
21
+ }));
22
+
23
+ import type { Conversation } from "../daemon/conversation.js";
24
+ import * as providers from "../messaging/providers/index.js";
25
+ import { initializeDb } from "../persistence/db-init.js";
26
+ import * as gatewayClient from "../runtime/gateway-client.js";
27
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
28
+ import { handleApprovalInterception } from "../runtime/routes/guardian-approval-interception.js";
29
+
30
+ await initializeDb();
31
+
32
+ const ASSISTANT_ID = "self";
33
+ const CONVERSATION_ID = "conv-card-edit-1";
34
+ const REQUESTER_CHAT = "5550100";
35
+ const TELEGRAM_CALLBACK_URL = "https://gateway.test/deliver/telegram?chat=1";
36
+ const BUTTON_MESSAGE_ID = "4242";
37
+
38
+ function registerPendingInteraction(requestId: string): void {
39
+ const _mockSession = {
40
+ handleConfirmationResponse: mock(() => {}),
41
+ ensureActorScopedHistory: async () => {},
42
+ } as unknown as Conversation;
43
+ _conversationMocks.set(CONVERSATION_ID, _mockSession);
44
+
45
+ pendingInteractions.register(requestId, {
46
+ conversationId: CONVERSATION_ID,
47
+ kind: "confirmation",
48
+ confirmationDetails: {
49
+ toolName: "execute_shell",
50
+ input: { command: "ls" },
51
+ riskLevel: "high",
52
+ allowlistOptions: [
53
+ { label: "test", description: "test", pattern: "test" },
54
+ ],
55
+ scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
56
+ },
57
+ });
58
+ }
59
+
60
+ function guardianParams(callbackData: string) {
61
+ return {
62
+ conversationId: CONVERSATION_ID,
63
+ callbackData,
64
+ content: "",
65
+ conversationExternalId: REQUESTER_CHAT,
66
+ sourceChannel: "telegram" as const,
67
+ actorExternalId: "guardian-user-1",
68
+ replyCallbackUrl: TELEGRAM_CALLBACK_URL,
69
+ trustCtx: {
70
+ sourceChannel: "telegram" as const,
71
+ trustClass: "guardian" as const,
72
+ requesterExternalUserId: "guardian-user-1",
73
+ guardianExternalUserId: "guardian-user-1",
74
+ guardianPrincipalId: "guardian-principal-1",
75
+ },
76
+ assistantId: ASSISTANT_ID,
77
+ approvalMessageId: BUTTON_MESSAGE_ID,
78
+ };
79
+ }
80
+
81
+ describe("approval card edit after a button decision", () => {
82
+ let editSpy: ReturnType<typeof spyOn>;
83
+ let deliverSpy: ReturnType<typeof spyOn>;
84
+
85
+ beforeEach(() => {
86
+ pendingInteractions.clear();
87
+ _anchorPrincipalId = "guardian-principal-1";
88
+ editSpy = spyOn(providers, "editChannelMessage").mockResolvedValue({
89
+ ok: true,
90
+ });
91
+ deliverSpy = spyOn(gatewayClient, "deliverChannelReply").mockResolvedValue({
92
+ ok: true,
93
+ });
94
+ });
95
+
96
+ test("a Telegram approve button edits exactly the card message", async () => {
97
+ registerPendingInteraction("req-card-edit-approve");
98
+
99
+ const result = await handleApprovalInterception(
100
+ guardianParams("apr:req-card-edit-approve:approve_once"),
101
+ );
102
+
103
+ expect(result).toEqual({ handled: true, type: "decision_applied" });
104
+ expect(editSpy).toHaveBeenCalledTimes(1);
105
+ const [callbackUrl, payload] = editSpy.mock.calls[0];
106
+ expect(callbackUrl).toBe(TELEGRAM_CALLBACK_URL);
107
+ expect(payload.chatId).toBe(REQUESTER_CHAT);
108
+ expect(payload.messageId).toBe(BUTTON_MESSAGE_ID);
109
+ expect(payload.text).toContain("Approved");
110
+
111
+ editSpy.mockRestore();
112
+ deliverSpy.mockRestore();
113
+ });
114
+
115
+ test("a Telegram reject button edits the card to Denied", async () => {
116
+ registerPendingInteraction("req-card-edit-reject");
117
+
118
+ const result = await handleApprovalInterception(
119
+ guardianParams("apr:req-card-edit-reject:reject"),
120
+ );
121
+
122
+ expect(result).toEqual({ handled: true, type: "decision_applied" });
123
+ expect(editSpy).toHaveBeenCalledTimes(1);
124
+ const [, payload] = editSpy.mock.calls[0];
125
+ expect(payload.messageId).toBe(BUTTON_MESSAGE_ID);
126
+ expect(payload.text).toContain("Denied");
127
+
128
+ editSpy.mockRestore();
129
+ deliverSpy.mockRestore();
130
+ });
131
+
132
+ test("a stale Telegram button press marks the card resolved", async () => {
133
+ // Pending interaction exists, but the button belongs to a different,
134
+ // already-settled request: the card must lose its buttons rather than
135
+ // apply to the wrong interaction.
136
+ registerPendingInteraction("req-card-edit-live");
137
+
138
+ const result = await handleApprovalInterception(
139
+ guardianParams("apr:req-card-edit-settled:approve_once"),
140
+ );
141
+
142
+ expect(result).toEqual({ handled: true, type: "stale_ignored" });
143
+ expect(editSpy).toHaveBeenCalledTimes(1);
144
+ const [callbackUrl, payload] = editSpy.mock.calls[0];
145
+ expect(callbackUrl).toBe(TELEGRAM_CALLBACK_URL);
146
+ expect(payload.messageId).toBe(BUTTON_MESSAGE_ID);
147
+ expect(payload.text).toBe("This approval request has been resolved.");
148
+ expect(payload.emphasis).toBe("muted");
149
+
150
+ editSpy.mockRestore();
151
+ deliverSpy.mockRestore();
152
+ });
153
+ });
@@ -4,7 +4,7 @@
4
4
  * register and assistant-outbound paths keep content verbatim.
5
5
  *
6
6
  * The converter module is mocked (keyed on the declared mime type) so these
7
- * tests run identically with and without macOS sips; real conversion is
7
+ * tests run independently of the installed image encoder; real conversion is
8
8
  * covered in image-conversion.test.ts. mock.module is process-global — keep
9
9
  * these cases in this file.
10
10
  */
@@ -8,6 +8,10 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
8
8
 
9
9
  import type { SkillSummary } from "../config/skills.js";
10
10
  import type { SkillInstallMeta } from "../skills/install-meta.js";
11
+ import {
12
+ SKILL_PLATFORM_VALUES,
13
+ skillPlatformForNodePlatform,
14
+ } from "../skills/platform-compatibility.js";
11
15
  import { setConfig } from "./helpers/set-config.js";
12
16
 
13
17
  function makeSummary(overrides: Partial<SkillSummary> = {}): SkillSummary {
@@ -120,6 +124,26 @@ describe("listInstalledSkills", () => {
120
124
  ]);
121
125
  });
122
126
 
127
+ test("reports host-incompatible skills as unavailable", async () => {
128
+ const currentPlatform = skillPlatformForNodePlatform(process.platform)!;
129
+ const incompatiblePlatform = SKILL_PLATFORM_VALUES.find(
130
+ (platform) => platform !== currentPlatform,
131
+ )!;
132
+ catalogFixture = [
133
+ makeSummary({ id: "compatible", platforms: [currentPlatform] }),
134
+ makeSummary({ id: "incompatible", platforms: [incompatiblePlatform] }),
135
+ ];
136
+
137
+ const { listInstalledSkills } =
138
+ await import("../skills/available-skills.js");
139
+ const entries = await listInstalledSkills();
140
+
141
+ expect(entries.map((entry) => [entry.id, entry.state])).toEqual([
142
+ ["compatible", "enabled"],
143
+ ["incompatible", "unavailable"],
144
+ ]);
145
+ });
146
+
123
147
  test("reports bundled skills excluded by allowBundled as unavailable", async () => {
124
148
  catalogFixture = [
125
149
  makeSummary({ id: "allowed-bundled" }),
@@ -250,6 +274,36 @@ describe("listCatalogSkills", () => {
250
274
  expect(await listCatalogSkills()).toEqual([]);
251
275
  });
252
276
 
277
+ test("reports host-incompatible catalog skills as unavailable", async () => {
278
+ const currentPlatform = skillPlatformForNodePlatform(process.platform)!;
279
+ const incompatiblePlatform = SKILL_PLATFORM_VALUES.find(
280
+ (platform) => platform !== currentPlatform,
281
+ )!;
282
+ remoteFixture = [
283
+ {
284
+ id: "incompatible",
285
+ name: "incompatible",
286
+ description: "Only works elsewhere",
287
+ platforms: [incompatiblePlatform],
288
+ metadata: { vellum: { platforms: [incompatiblePlatform] } },
289
+ },
290
+ ];
291
+
292
+ const { listCatalogSkills } = await import("../skills/available-skills.js");
293
+ expect(await listCatalogSkills()).toEqual([
294
+ {
295
+ id: "incompatible",
296
+ displayName: "incompatible",
297
+ description: "Only works elsewhere",
298
+ activationHints: undefined,
299
+ avoidWhen: undefined,
300
+ platforms: [incompatiblePlatform],
301
+ installed: false,
302
+ state: "unavailable",
303
+ },
304
+ ]);
305
+ });
306
+
253
307
  test("adds no error handling of its own — unexpected catalog-read errors propagate", async () => {
254
308
  remoteError = new Error("catalog read exploded");
255
309
  const { listCatalogSkills } = await import("../skills/available-skills.js");
@@ -1,4 +1,8 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../platform/sync-avatar.js", () => ({
4
+ syncAvatarToPlatform: () => {},
5
+ }));
2
6
 
3
7
  import type { AssistantEventEnvelope } from "../api/index.js";
4
8
  import { SYNC_TAGS } from "../daemon/message-types/sync.js";
@@ -52,6 +52,7 @@ describe("fetchCatalog normalization", () => {
52
52
  description: "Shop on Amazon",
53
53
  icon: "🛒",
54
54
  category: "commerce",
55
+ platforms: ["windows"],
55
56
  updated_at: "2026-04-19T19:26:17Z",
56
57
  },
57
58
  ],
@@ -67,6 +68,8 @@ describe("fetchCatalog normalization", () => {
67
68
  expect(skill.icon).toBe("🛒");
68
69
  expect(skill.metadata?.icon).toBe("🛒");
69
70
  expect(skill.updatedAt).toBe("2026-04-19T19:26:17Z");
71
+ expect(skill.platforms).toEqual(["windows"]);
72
+ expect(skill.metadata?.vellum?.platforms).toEqual(["windows"]);
70
73
  });
71
74
 
72
75
  test("leaves category undefined when the API omits it", async () => {
@@ -115,7 +118,11 @@ describe("readLocalCatalog normalization", () => {
115
118
  description: "Task management",
116
119
  metadata: {
117
120
  icon: "✅",
118
- vellum: { "display-name": "Tasks", category: "productivity" },
121
+ vellum: {
122
+ "display-name": "Tasks",
123
+ category: "productivity",
124
+ platforms: ["macos", "linux"],
125
+ },
119
126
  },
120
127
  },
121
128
  ],
@@ -127,6 +134,7 @@ describe("readLocalCatalog normalization", () => {
127
134
  expect(catalog).toHaveLength(1);
128
135
  expect(catalog[0].metadata?.vellum?.category).toBe("productivity");
129
136
  expect(catalog[0].metadata?.vellum?.["display-name"]).toBe("Tasks");
137
+ expect(catalog[0].platforms).toEqual(["macos", "linux"]);
130
138
  } finally {
131
139
  rmSync(dir, { recursive: true, force: true });
132
140
  }
@@ -38,9 +38,7 @@ describe("channel policy registry", () => {
38
38
 
39
39
  test("getDeliverableChannels returns exactly the channels with deliveryEnabled: true", () => {
40
40
  const deliverable = getDeliverableChannels();
41
- const expected = CHANNEL_IDS.filter(
42
- (id) => getChannelPolicy(id).notification.deliveryEnabled,
43
- );
41
+ const expected = CHANNEL_IDS.filter(isNotificationDeliverable);
44
42
 
45
43
  expect(deliverable).toHaveLength(expected.length);
46
44
  for (const id of expected) {
@@ -44,18 +44,6 @@ type SocketHealth = {
44
44
  let askedChannels: string[];
45
45
  let socketHealth: SocketHealth | Error;
46
46
 
47
- /** How many channels the gateway reports on Discord's allow-list. */
48
- let admittedChannelCount: number | Error;
49
-
50
- mock.module("../channels/gateway-discord-admission.js", () => ({
51
- readDiscordAdmission: async () => {
52
- if (admittedChannelCount instanceof Error) {
53
- throw admittedChannelCount;
54
- }
55
- return { admittedChannelCount };
56
- },
57
- }));
58
-
59
47
  mock.module("../channels/gateway-channel-socket-health.js", () => ({
60
48
  readChannelSocketHealth: async (channel: string) => {
61
49
  askedChannels.push(channel);
@@ -72,7 +60,6 @@ beforeEach(() => {
72
60
  socketHealth = { channel: "discord", status: "connected" };
73
61
  // The default install state: fail-closed and empty, admitting nothing until
74
62
  // someone lists channel ids. Tests that need admission say so.
75
- admittedChannelCount = 0;
76
63
  });
77
64
 
78
65
  async function runDiscordProbe() {
@@ -95,28 +82,30 @@ describe("discord readiness", () => {
95
82
  expect(askedChannels).toEqual(["discord"]);
96
83
  });
97
84
 
98
- test("a fresh install has neither a token nor an allow-list", async () => {
85
+ test("a fresh install has no token", async () => {
99
86
  const [snapshot] = await runDiscordProbe();
100
87
 
101
88
  expect(findCheck(snapshot, "bot_token").passed).toBe(false);
102
- expect(findCheck(snapshot, "guild_admission").passed).toBe(false);
103
89
  // Nothing configured at all, which is a different report from a channel
104
90
  // half set up: no step has been taken rather than one left undone.
105
91
  expect(snapshot.setupStatus).toBe("not_configured");
106
92
  });
107
93
 
108
- test("an allow-list without a token is half set up", async () => {
109
- admittedChannelCount = 3;
94
+ test("Discord has one setup step, so it is never half configured", async () => {
95
+ // A live socket without a token still reads as not configured rather than
96
+ // incomplete. `incomplete` needs one configuration check passing while
97
+ // another fails, and the token is now the only one: with the room
98
+ // allow-list gone there is no second step to be part-way through.
99
+ socketHealth = { channel: "discord", status: "connected" };
110
100
 
111
101
  const [snapshot] = await runDiscordProbe();
112
102
 
113
103
  expect(findCheck(snapshot, "bot_token").passed).toBe(false);
114
- expect(snapshot.setupStatus).toBe("incomplete");
104
+ expect(snapshot.setupStatus).toBe("not_configured");
115
105
  });
116
106
 
117
- test("a token, a live connection and an allow-list read as ready", async () => {
107
+ test("a token and a live connection read as ready", async () => {
118
108
  mockSecureKeys[credentialKey("discord_channel", "bot_token")] = "bot-fake";
119
- admittedChannelCount = 2;
120
109
  socketHealth = {
121
110
  channel: "discord",
122
111
  status: "connected",
@@ -130,61 +119,11 @@ describe("discord readiness", () => {
130
119
  expect(delivery.passed).toBe(true);
131
120
  expect(delivery.message).toContain("Discord");
132
121
  expect(delivery.message).toContain("2026-08-21T17:00:00.000Z");
133
- expect(findCheck(snapshot, "guild_admission").message).toContain(
134
- "2 channels",
135
- );
136
122
  expect(snapshot.ready).toBe(true);
137
123
  });
138
124
 
139
- test("an empty allow-list is unfinished setup, not a live channel", async () => {
140
- mockSecureKeys[credentialKey("discord_channel", "bot_token")] = "bot-fake";
141
- admittedChannelCount = 0;
142
-
143
- const [snapshot] = await runDiscordProbe();
144
-
145
- // A connected socket that admits nothing still drops every guild message,
146
- // so claiming ready here would promise delivery the gate is refusing.
147
- const admission = findCheck(snapshot, "guild_admission");
148
- expect(admission.passed).toBe(false);
149
- expect(snapshot.setupStatus).toBe("incomplete");
150
- expect(snapshot.ready).toBe(false);
151
-
152
- // And it is a setup step rather than an outage: direct messages carry no
153
- // guild and never meet this gate, so the connection itself is fine.
154
- expect(snapshot.health).not.toBe("failing");
155
- expect(findCheck(snapshot, "inbound_delivery").passed).toBe(true);
156
- });
157
-
158
- test("one admitted channel reads in the singular", async () => {
159
- mockSecureKeys[credentialKey("discord_channel", "bot_token")] = "bot-fake";
160
- admittedChannelCount = 1;
161
-
162
- const [snapshot] = await runDiscordProbe();
163
-
164
- expect(findCheck(snapshot, "guild_admission").message).toContain(
165
- "1 channel",
166
- );
167
- expect(findCheck(snapshot, "guild_admission").message).not.toContain(
168
- "1 channels",
169
- );
170
- });
171
-
172
- test("an unreachable gateway does not claim the allow-list is empty", async () => {
173
- mockSecureKeys[credentialKey("discord_channel", "bot_token")] = "bot-fake";
174
- admittedChannelCount = new Error("gateway down");
175
-
176
- const [snapshot] = await runDiscordProbe();
177
-
178
- const admission = findCheck(snapshot, "guild_admission");
179
- expect(admission.passed).toBe(true);
180
- expect(admission.indeterminate).toBe(true);
181
- });
182
-
183
125
  test("a dead socket reads as configured and failing, not unconfigured", async () => {
184
126
  mockSecureKeys[credentialKey("discord_channel", "bot_token")] = "bot-fake";
185
- // Setup has to be finished for this to isolate the socket: an empty
186
- // allow-list would leave it incomplete for a reason that is not the socket.
187
- admittedChannelCount = 1;
188
127
  socketHealth = { channel: "discord", status: "disconnected" };
189
128
 
190
129
  const [snapshot] = await runDiscordProbe();