@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,372 @@
1
+ /**
2
+ * Tests for the ChatGPT subscription auth routes.
3
+ *
4
+ * The device-code path is driven by mocking `openai-device-auth` so the route's
5
+ * own contract is under test: `device-auth` returns the user code immediately
6
+ * and the status route reports `pending` until the background poll settles,
7
+ * then `connected` or `error` with the provider's error code. The credential +
8
+ * connection store is mocked so no CES or database write is attempted.
9
+ */
10
+
11
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ import type {
14
+ OAuth2FlowResult,
15
+ OAuth2TokenResult,
16
+ } from "../../../security/oauth2.js";
17
+ import type { DeviceAuthRequest } from "../../../security/openai-device-auth.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Mocks, wired BEFORE importing the module under test.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const actualDeviceAuth =
24
+ await import("../../../security/openai-device-auth.js");
25
+ const { DeviceAuthError } = actualDeviceAuth;
26
+
27
+ const DEVICE_REQUEST: DeviceAuthRequest = {
28
+ deviceAuthId: "deviceauth_abc123",
29
+ userCode: "AMOO-SIISY",
30
+ verificationUrl: "https://auth.openai.com/codex/device",
31
+ expiresAt: "2099-01-01T00:00:00Z",
32
+ intervalSeconds: 5,
33
+ };
34
+
35
+ // Every mint gets its own id, as OpenAI's do, so one flow's background poll can
36
+ // never settle a key another flow owns.
37
+ let mintCount = 0;
38
+ let lastMinted: DeviceAuthRequest = DEVICE_REQUEST;
39
+
40
+ const requestDeviceCodeMock = mock(async () => {
41
+ mintCount++;
42
+ lastMinted = {
43
+ ...DEVICE_REQUEST,
44
+ deviceAuthId: `${DEVICE_REQUEST.deviceAuthId}_${mintCount}`,
45
+ };
46
+ return lastMinted;
47
+ });
48
+
49
+ /** Resolver handles for the in-flight poll each test drives by hand. */
50
+ let resolveDeviceAuth: (result: OAuth2FlowResult) => void = () => {};
51
+ let rejectDeviceAuth: (err: unknown) => void = () => {};
52
+ /** Every signal the route handed the poll, in mint order. */
53
+ const pollSignals: AbortSignal[] = [];
54
+ /**
55
+ * Whether an abort settles the mocked poll. Off for the races where the poll
56
+ * has already returned an authorization code and the exchange is mid-flight,
57
+ * which is exactly the window the route has to guard on its own.
58
+ */
59
+ let rejectPollOnAbort = true;
60
+
61
+ const completeDeviceAuthMock = mock(
62
+ (
63
+ _config: unknown,
64
+ _request: DeviceAuthRequest,
65
+ options: { signal?: AbortSignal } = {},
66
+ ) =>
67
+ new Promise<OAuth2FlowResult>((resolve, reject) => {
68
+ resolveDeviceAuth = resolve;
69
+ rejectDeviceAuth = reject;
70
+ if (options.signal) {
71
+ pollSignals.push(options.signal);
72
+ // The real poll throws once the signal fires; mirroring that keeps the
73
+ // route's own settle path under test.
74
+ options.signal.addEventListener("abort", () => {
75
+ if (!rejectPollOnAbort) {
76
+ return;
77
+ }
78
+ reject(
79
+ new DeviceAuthError(
80
+ "Device authorization was cancelled.",
81
+ "aborted",
82
+ ),
83
+ );
84
+ });
85
+ }
86
+ }),
87
+ );
88
+
89
+ mock.module("../../../security/openai-device-auth.js", () => ({
90
+ ...actualDeviceAuth,
91
+ requestDeviceCode: requestDeviceCodeMock,
92
+ completeDeviceAuth: completeDeviceAuthMock,
93
+ }));
94
+
95
+ const actualChatgptAuth =
96
+ await import("../../../providers/inference/chatgpt-subscription-auth.js");
97
+ /** When set, the store blocks on it, so a test can interleave a cancel. */
98
+ let storeGate: Promise<void> | null = null;
99
+ const storeChatgptSubscriptionTokensMock = mock(
100
+ async (_tokens: OAuth2TokenResult) => {
101
+ if (storeGate) {
102
+ await storeGate;
103
+ }
104
+ },
105
+ );
106
+ mock.module(
107
+ "../../../providers/inference/chatgpt-subscription-auth.js",
108
+ () => ({
109
+ ...actualChatgptAuth,
110
+ storeChatgptSubscriptionTokens: storeChatgptSubscriptionTokensMock,
111
+ }),
112
+ );
113
+
114
+ const { ROUTES } = await import("../chatgpt-subscription-auth-routes.js");
115
+
116
+ const startDeviceAuthHandler = ROUTES.find(
117
+ (r) => r.operationId === "inference_chatgpt_subscription_device_auth",
118
+ )!.handler;
119
+ const deviceAuthStatusHandler = ROUTES.find(
120
+ (r) => r.operationId === "inference_chatgpt_subscription_device_auth_status",
121
+ )!.handler;
122
+ const cancelDeviceAuthHandler = ROUTES.find(
123
+ (r) => r.operationId === "inference_chatgpt_subscription_device_auth_cancel",
124
+ )!.handler;
125
+ const startAuthHandler = ROUTES.find(
126
+ (r) => r.operationId === "inference_chatgpt_subscription_auth",
127
+ )!.handler;
128
+
129
+ interface DeviceStartResult {
130
+ state: string;
131
+ user_code: string;
132
+ verification_url: string;
133
+ expires_at: string;
134
+ interval_seconds: number;
135
+ }
136
+
137
+ interface StatusResult {
138
+ status: "pending" | "connected" | "error";
139
+ error?: string;
140
+ error_code?: string;
141
+ }
142
+
143
+ const TOKENS: OAuth2FlowResult = {
144
+ tokens: {
145
+ accessToken: "access-1",
146
+ refreshToken: "refresh-1",
147
+ expiresIn: 3600,
148
+ },
149
+ grantedScopes: ["openid"],
150
+ rawTokenResponse: {},
151
+ };
152
+
153
+ async function startDeviceAuth(): Promise<DeviceStartResult> {
154
+ return (await startDeviceAuthHandler({})) as DeviceStartResult;
155
+ }
156
+
157
+ function readStatus(state: string): StatusResult {
158
+ return deviceAuthStatusHandler({ pathParams: { state } }) as StatusResult;
159
+ }
160
+
161
+ function cancelDeviceAuth(state: string): { cancelled: boolean } {
162
+ return cancelDeviceAuthHandler({ pathParams: { state } }) as {
163
+ cancelled: boolean;
164
+ };
165
+ }
166
+
167
+ /** Let the background `.then`/`.catch` chain run before asserting. */
168
+ async function flush(): Promise<void> {
169
+ await Promise.resolve();
170
+ await Promise.resolve();
171
+ await Promise.resolve();
172
+ }
173
+
174
+ describe("chatgpt subscription device auth routes", () => {
175
+ beforeEach(() => {
176
+ requestDeviceCodeMock.mockClear();
177
+ completeDeviceAuthMock.mockClear();
178
+ storeChatgptSubscriptionTokensMock.mockClear();
179
+ pollSignals.length = 0;
180
+ rejectPollOnAbort = true;
181
+ storeGate = null;
182
+ });
183
+
184
+ test("returns the user code immediately and starts pending", async () => {
185
+ const result = await startDeviceAuth();
186
+
187
+ expect(result).toEqual({
188
+ state: lastMinted.deviceAuthId,
189
+ user_code: "AMOO-SIISY",
190
+ verification_url: "https://auth.openai.com/codex/device",
191
+ expires_at: "2099-01-01T00:00:00Z",
192
+ interval_seconds: 5,
193
+ });
194
+ expect(completeDeviceAuthMock).toHaveBeenCalledTimes(1);
195
+ expect(readStatus(result.state)).toEqual({ status: "pending" });
196
+ });
197
+
198
+ test("stores the tokens and flips to connected when the poll succeeds", async () => {
199
+ const result = await startDeviceAuth();
200
+
201
+ resolveDeviceAuth(TOKENS);
202
+ await flush();
203
+
204
+ expect(storeChatgptSubscriptionTokensMock).toHaveBeenCalledTimes(1);
205
+ expect(storeChatgptSubscriptionTokensMock.mock.calls[0]?.[0]).toEqual(
206
+ TOKENS.tokens,
207
+ );
208
+ expect(readStatus(result.state)).toEqual({ status: "connected" });
209
+ });
210
+
211
+ test("reports the provider error code when the poll fails", async () => {
212
+ const result = await startDeviceAuth();
213
+
214
+ rejectDeviceAuth(
215
+ new DeviceAuthError("Device auth is off.", "deviceauth_not_enabled"),
216
+ );
217
+ await flush();
218
+
219
+ expect(storeChatgptSubscriptionTokensMock).not.toHaveBeenCalled();
220
+ expect(readStatus(result.state)).toEqual({
221
+ status: "error",
222
+ error: "Device auth is off.",
223
+ error_code: "deviceauth_not_enabled",
224
+ });
225
+ });
226
+
227
+ test("falls back to a generic code for a non-device-auth failure", async () => {
228
+ const result = await startDeviceAuth();
229
+
230
+ rejectDeviceAuth(new Error("token exchange exploded"));
231
+ await flush();
232
+
233
+ expect(readStatus(result.state)).toEqual({
234
+ status: "error",
235
+ error: "token exchange exploded",
236
+ error_code: "device_auth_failed",
237
+ });
238
+ });
239
+
240
+ test("404s an unknown state", async () => {
241
+ expect(() => readStatus("deviceauth_missing")).toThrow(
242
+ /No active ChatGPT device sign-in/,
243
+ );
244
+ });
245
+ });
246
+
247
+ describe("chatgpt subscription device auth cancellation", () => {
248
+ beforeEach(() => {
249
+ requestDeviceCodeMock.mockClear();
250
+ completeDeviceAuthMock.mockClear();
251
+ storeChatgptSubscriptionTokensMock.mockClear();
252
+ pollSignals.length = 0;
253
+ rejectPollOnAbort = true;
254
+ storeGate = null;
255
+ });
256
+
257
+ test("stops the poll and reports the flow aborted", async () => {
258
+ const result = await startDeviceAuth();
259
+
260
+ expect(cancelDeviceAuth(result.state)).toEqual({ cancelled: true });
261
+
262
+ expect(pollSignals[0]?.aborted).toBe(true);
263
+ const status = readStatus(result.state);
264
+ expect(status.status).toBe("error");
265
+ expect(status.error_code).toBe("aborted");
266
+ await flush();
267
+ expect(readStatus(result.state).error_code).toBe("aborted");
268
+ });
269
+
270
+ test("leaves a flow that already settled alone", async () => {
271
+ const result = await startDeviceAuth();
272
+
273
+ resolveDeviceAuth(TOKENS);
274
+ await flush();
275
+
276
+ expect(cancelDeviceAuth(result.state)).toEqual({ cancelled: false });
277
+ expect(readStatus(result.state)).toEqual({ status: "connected" });
278
+ });
279
+
280
+ test("starting a flow aborts the one before it", async () => {
281
+ const first = await startDeviceAuth();
282
+ const second = await startDeviceAuth();
283
+ await flush();
284
+
285
+ expect(second.state).not.toBe(first.state);
286
+ expect(pollSignals[0]?.aborted).toBe(true);
287
+ expect(pollSignals[1]?.aborted).toBe(false);
288
+
289
+ const firstStatus = readStatus(first.state);
290
+ expect(firstStatus.status).toBe("error");
291
+ expect(firstStatus.error_code).toBe("aborted");
292
+ expect(readStatus(second.state)).toEqual({ status: "pending" });
293
+ });
294
+
295
+ test("404s an unknown state", async () => {
296
+ expect(() => cancelDeviceAuth("deviceauth_missing")).toThrow(
297
+ /No active ChatGPT device sign-in/,
298
+ );
299
+ });
300
+
301
+ test("discards tokens from a flow cancelled during the exchange", async () => {
302
+ rejectPollOnAbort = false;
303
+ const result = await startDeviceAuth();
304
+
305
+ expect(cancelDeviceAuth(result.state)).toEqual({ cancelled: true });
306
+ resolveDeviceAuth(TOKENS);
307
+ await flush();
308
+
309
+ expect(storeChatgptSubscriptionTokensMock).not.toHaveBeenCalled();
310
+ const status = readStatus(result.state);
311
+ expect(status.status).toBe("error");
312
+ expect(status.error_code).toBe("aborted");
313
+ });
314
+
315
+ test("leaves a flow cancelled mid-store marked aborted", async () => {
316
+ rejectPollOnAbort = false;
317
+ let releaseStore = () => {};
318
+ storeGate = new Promise<void>((resolve) => {
319
+ releaseStore = resolve;
320
+ });
321
+ const result = await startDeviceAuth();
322
+
323
+ resolveDeviceAuth(TOKENS);
324
+ await flush();
325
+ expect(storeChatgptSubscriptionTokensMock).toHaveBeenCalledTimes(1);
326
+
327
+ expect(cancelDeviceAuth(result.state)).toEqual({ cancelled: true });
328
+ releaseStore();
329
+ await flush();
330
+
331
+ const status = readStatus(result.state);
332
+ expect(status.status).toBe("error");
333
+ expect(status.error_code).toBe("aborted");
334
+ });
335
+
336
+ test("a superseded flow cannot store over the one that replaced it", async () => {
337
+ rejectPollOnAbort = false;
338
+ const first = await startDeviceAuth();
339
+ const resolveFirst = resolveDeviceAuth;
340
+ const second = await startDeviceAuth();
341
+
342
+ resolveFirst(TOKENS);
343
+ await flush();
344
+
345
+ expect(storeChatgptSubscriptionTokensMock).not.toHaveBeenCalled();
346
+ expect(readStatus(first.state).error_code).toBe("aborted");
347
+ expect(readStatus(second.state)).toEqual({ status: "pending" });
348
+ });
349
+ });
350
+
351
+ describe("chatgpt subscription paste flow", () => {
352
+ test("builds a PKCE authorize URL against OpenAI's client", async () => {
353
+ const result = (await startAuthHandler({})) as {
354
+ authorize_url: string;
355
+ state: string;
356
+ };
357
+
358
+ const url = new URL(result.authorize_url);
359
+ expect(url.origin + url.pathname).toBe(
360
+ "https://auth.openai.com/oauth/authorize",
361
+ );
362
+ expect(url.searchParams.get("client_id")).toBe(
363
+ "app_EMoamEEZ73f0CkXaXp7hrann",
364
+ );
365
+ expect(url.searchParams.get("code_challenge_method")).toBe("S256");
366
+ expect(url.searchParams.get("state")).toBe(result.state);
367
+ expect(url.searchParams.get("redirect_uri")).toBe(
368
+ "http://localhost:1455/auth/callback",
369
+ );
370
+ expect(url.searchParams.get("id_token_add_organizations")).toBe("true");
371
+ });
372
+ });
@@ -316,6 +316,7 @@ const nativeGatewayRead = {
316
316
  contactType: "human",
317
317
  lastInteraction: 9200,
318
318
  interactionCount: 11,
319
+ autoApproveThreshold: "high",
319
320
  createdAt: 1000,
320
321
  updatedAt: 1500,
321
322
  channels: [
@@ -376,6 +377,9 @@ describe("filtered/native contact reads: daemon filters, gateway hydrates teleme
376
377
  // (4/4200/4100).
377
378
  expect(contact.interactionCount).toBe(11);
378
379
  expect(contact.lastInteraction).toBe(9200);
380
+ expect(
381
+ (contact as { autoApproveThreshold?: string | null }).autoApproveThreshold,
382
+ ).toBe("high");
379
383
  const channel = contact.channels[0] as Record<string, unknown>;
380
384
  expect(channel.interactionCount).toBe(11);
381
385
  expect(channel.lastSeenAt).toBe(9100);
@@ -404,6 +408,9 @@ describe("filtered/native contact reads: daemon filters, gateway hydrates teleme
404
408
  const [contact] = result.contacts;
405
409
  expect(contact.interactionCount).toBe(0);
406
410
  expect(contact.lastInteraction).toBeNull();
411
+ expect(
412
+ (contact as { autoApproveThreshold?: string | null }).autoApproveThreshold,
413
+ ).toBeNull();
407
414
  const channel = contact.channels[0] as Record<string, unknown>;
408
415
  expect(channel.interactionCount).toBe(0);
409
416
  expect(channel.lastSeenAt).toBeNull();
@@ -50,6 +50,7 @@ mock.module("../../../persistence/embeddings/embedding-backend.js", () => ({
50
50
  },
51
51
  }));
52
52
 
53
+ import { BACKUP_PROFILE_KEYS } from "../../../config/default-profile-names.js";
53
54
  import { getConfig, loadRawConfig } from "../../../config/loader.js";
54
55
  import { LLMConfigBase } from "../../../config/schemas/llm.js";
55
56
  import type { ConversationCreateType } from "../../../persistence/conversation-types.js";
@@ -836,6 +837,49 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
836
837
  seedRawConfig();
837
838
  });
838
839
 
840
+ describe("fallbackProfile write protection", () => {
841
+ beforeEach(() => {
842
+ const llm = rawConfigFixture.llm as {
843
+ profiles: Record<string, Record<string, unknown>>;
844
+ };
845
+ llm.profiles.backup = {
846
+ provider: "anthropic",
847
+ model: "claude-sonnet-4-6",
848
+ };
849
+ seedRawConfig();
850
+ });
851
+
852
+ test("rejects a custom fallbackProfile without writing", async () => {
853
+ await expect(
854
+ replaceProfileRoute.handler({
855
+ pathParams: { name: "custom" },
856
+ body: {
857
+ provider: "anthropic",
858
+ model: "claude-sonnet-4-6",
859
+ fallbackProfile: "backup",
860
+ },
861
+ }),
862
+ ).rejects.toThrow(/Automatic fallbacks are code-owned/);
863
+ expect(persistedProfile("custom").fallbackProfile).toBeUndefined();
864
+ expect(initializeProvidersCalls).toBe(0);
865
+ });
866
+
867
+ test("rejects a custom pointer at a managed backup name", async () => {
868
+ await expect(
869
+ replaceProfileRoute.handler({
870
+ pathParams: { name: "custom" },
871
+ body: {
872
+ provider: "anthropic",
873
+ model: "claude-sonnet-4-6",
874
+ fallbackProfile: "balanced-backup",
875
+ },
876
+ }),
877
+ ).rejects.toThrow(/Automatic fallbacks are code-owned/);
878
+ expect(persistedProfile("custom").fallbackProfile).toBeUndefined();
879
+ expect(initializeProvidersCalls).toBe(0);
880
+ });
881
+ });
882
+
839
883
  test("owns contextWindow maxInputTokens while preserving non-UI profile leaves", async () => {
840
884
  const result = await replaceProfileRoute.handler({
841
885
  pathParams: { name: "custom" },
@@ -1242,6 +1286,48 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
1242
1286
  });
1243
1287
  });
1244
1288
 
1289
+ describe("PATCH /v1/config fallbackProfile write protection", () => {
1290
+ const patchRoute = ROUTES.find((r) => r.operationId === "config_patch")!;
1291
+
1292
+ beforeEach(() => {
1293
+ initializeProvidersCalls = 0;
1294
+ clearEmbeddingBackendCacheCalls = 0;
1295
+ rawConfigFixture = {
1296
+ llm: {
1297
+ profiles: {
1298
+ custom: { provider: "anthropic", model: "claude-sonnet-4-6" },
1299
+ backup: { provider: "anthropic", model: "claude-sonnet-4-6" },
1300
+ },
1301
+ },
1302
+ };
1303
+ seedRawConfig();
1304
+ });
1305
+
1306
+ test("rejects a custom fallbackProfile without writing", async () => {
1307
+ await expect(
1308
+ patchRoute.handler({
1309
+ body: { llm: { profiles: { custom: { fallbackProfile: "backup" } } } },
1310
+ }),
1311
+ ).rejects.toThrow(/Automatic fallbacks are code-owned/);
1312
+ expect(persistedProfile("custom").fallbackProfile).toBeUndefined();
1313
+ expect(initializeProvidersCalls).toBe(0);
1314
+ });
1315
+
1316
+ test("rejects a custom pointer at a managed backup name", async () => {
1317
+ await expect(
1318
+ patchRoute.handler({
1319
+ body: {
1320
+ llm: {
1321
+ profiles: { custom: { fallbackProfile: "balanced-backup" } },
1322
+ },
1323
+ },
1324
+ }),
1325
+ ).rejects.toThrow(/Automatic fallbacks are code-owned/);
1326
+ expect(persistedProfile("custom").fallbackProfile).toBeUndefined();
1327
+ expect(initializeProvidersCalls).toBe(0);
1328
+ });
1329
+ });
1330
+
1245
1331
  describe("custom profile write normalization (complete overrides)", () => {
1246
1332
  const configPatchRoute = ROUTES.find(
1247
1333
  (r) => r.operationId === "config_patch",
@@ -1696,6 +1782,16 @@ describe("config invariant flag enrichment", () => {
1696
1782
  }
1697
1783
  });
1698
1784
 
1785
+ test("GET /v1/config hides managed backups from the picker catalog", async () => {
1786
+ const body = await configGetRoute.handler({});
1787
+ const profiles = wireProfiles(body);
1788
+
1789
+ for (const name of BACKUP_PROFILE_KEYS) {
1790
+ expect(profiles).not.toHaveProperty(name);
1791
+ }
1792
+ expect(profiles.balanced).toBeDefined();
1793
+ });
1794
+
1699
1795
  test("PATCH /v1/config stamps the flag on the response but never persists it", async () => {
1700
1796
  const body = await configPatchRoute.handler({
1701
1797
  body: { memory: { enabled: true } },
@@ -64,6 +64,7 @@ mock.module("../../sync/resource-sync-events.js", () => ({
64
64
  // ── Real imports (after mocks) ────────────────────────────────────────────────
65
65
 
66
66
  import { setConfig } from "../../../__tests__/helpers/set-config.js";
67
+ import { BACKUP_PROFILE_KEYS } from "../../../config/default-profile-names.js";
67
68
  import { loadRawConfig } from "../../../config/loader.js";
68
69
  import { getDb } from "../../../persistence/db-connection.js";
69
70
  import { initializeDb } from "../../../persistence/db-init.js";
@@ -625,6 +626,11 @@ describe("collectProfileReferences", () => {
625
626
  source: "user",
626
627
  mix: [{ profile: "my-fast", weight: 1 }],
627
628
  },
629
+ "my-primary": {
630
+ source: "user",
631
+ provider: "anthropic",
632
+ fallbackProfile: "my-fast",
633
+ },
628
634
  },
629
635
  };
630
636
  expect(collectProfileReferences(llm, "my-fast").sort()).toEqual(
@@ -633,6 +639,7 @@ describe("collectProfileReferences", () => {
633
639
  "llm.advisorProfile",
634
640
  "llm.callSites.memoryExtraction",
635
641
  "llm.profiles.my-mix.mix",
642
+ "llm.profiles.my-primary.fallbackProfile",
636
643
  ].sort(),
637
644
  );
638
645
  });
@@ -676,6 +683,22 @@ describe("DELETE inference/profiles/:name reference guard", () => {
676
683
  ).rejects.toThrow(/llm\.profiles\.my-mix\.mix/);
677
684
  });
678
685
 
686
+ test("rejects deletion referenced by another profile's fallbackProfile", async () => {
687
+ setConfig("llm", {
688
+ profiles: {
689
+ "my-fast": { source: "user", provider: "anthropic" },
690
+ "my-primary": {
691
+ source: "user",
692
+ provider: "anthropic",
693
+ fallbackProfile: "my-fast",
694
+ },
695
+ },
696
+ });
697
+ await expect(
698
+ call("inference_profiles_delete", { pathParams: { name: "my-fast" } }),
699
+ ).rejects.toThrow(/llm\.profiles\.my-primary\.fallbackProfile/);
700
+ });
701
+
679
702
  test("rejects deletion referenced by a call site", async () => {
680
703
  setConfig("llm", {
681
704
  callSites: { memoryExtraction: { profile: "my-fast" } },
@@ -706,6 +729,20 @@ describe("DELETE inference/profiles/:name reference guard", () => {
706
729
  // ── provider-aware list/get (Finding 2) ───────────────────────────────────────
707
730
 
708
731
  describe("GET inference/profiles honors llm.defaultProvider", () => {
732
+ test("omits managed backup routes from the user-facing catalog", async () => {
733
+ setConfig("llm", { profiles: {} });
734
+
735
+ const listed = (await call("inference_profiles_list", {})) as {
736
+ profiles: Array<{ name: string }>;
737
+ };
738
+ const names = new Set(listed.profiles.map((profile) => profile.name));
739
+
740
+ for (const key of BACKUP_PROFILE_KEYS) {
741
+ expect(names.has(key)).toBe(false);
742
+ }
743
+ expect(names.has("balanced")).toBe(true);
744
+ });
745
+
709
746
  test("expands balanced through a BYOK default provider, not the vellum column", async () => {
710
747
  setConfig("llm", {
711
748
  defaultProvider: { provider: "anthropic" },
@@ -36,8 +36,9 @@ const probeTunnelMock = mock(
36
36
  mock.module("../../../daemon/handlers/config-ingress.js", () => ({
37
37
  getIngressConfigResult: () => ingressConfig,
38
38
  isVelayManagedIngress: () => velayManaged,
39
- loadLastTunnelRecord: () => lastTunnel,
40
39
  loadPairingTunnelRecord: () => pairingTunnel,
40
+ // Mirrors the real selector: a pairing-only record wins over the last run.
41
+ loadRestartTunnelRecord: () => pairingTunnel ?? lastTunnel,
41
42
  loadRecordedAssistantId: () => recordedAssistantId,
42
43
  }));
43
44
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `internal_mcp_add` must not freeze a risk level into the entry it writes.
3
+ *
4
+ * The shipped default lives in `McpServerConfigSchema`, which parses
5
+ * `config.json` on every load. An entry that carries no `defaultRiskLevel`
6
+ * therefore tracks that default; one that carries an explicit level keeps it.
7
+ * Writing a level the caller never asked for would pin every added server to
8
+ * whatever the default happened to be on the day it was added.
9
+ */
10
+
11
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ import { McpServerConfigSchema } from "../../../config/schemas/mcp.js";
14
+ import { BadRequestError } from "../errors.js";
15
+
16
+ let raw: Record<string, unknown> = {};
17
+ let saved: Record<string, unknown> | undefined;
18
+
19
+ const actualLoader = await import("../../../config/loader.js");
20
+ mock.module("../../../config/loader.js", () => ({
21
+ ...actualLoader,
22
+ loadRawConfig: () => raw,
23
+ saveRawConfig: (next: Record<string, unknown>) => {
24
+ saved = next;
25
+ },
26
+ }));
27
+
28
+ mock.module("../../../daemon/mcp-reload-service.js", () => ({
29
+ reloadMcpServers: async () => {},
30
+ }));
31
+
32
+ const { ROUTES } = await import("../mcp-auth-routes.js");
33
+
34
+ const addRoute = ROUTES.find((r) => r.operationId === "internal_mcp_add")!;
35
+
36
+ function addedEntry(): Record<string, unknown> {
37
+ const mcp = saved?.mcp as { servers: Record<string, unknown> };
38
+ return mcp.servers["srv"] as Record<string, unknown>;
39
+ }
40
+
41
+ describe("internal_mcp_add risk level", () => {
42
+ beforeEach(() => {
43
+ raw = {};
44
+ saved = undefined;
45
+ });
46
+
47
+ test("omits defaultRiskLevel when the caller names no risk", async () => {
48
+ await addRoute.handler({
49
+ body: {
50
+ name: "srv",
51
+ transportType: "streamable-http",
52
+ url: "https://example.com/mcp",
53
+ },
54
+ });
55
+
56
+ const entry = addedEntry();
57
+ expect(entry).not.toHaveProperty("defaultRiskLevel");
58
+ expect(McpServerConfigSchema.parse(entry).defaultRiskLevel).toBe("medium");
59
+ });
60
+
61
+ test("persists an explicit risk level", async () => {
62
+ await addRoute.handler({
63
+ body: {
64
+ name: "srv",
65
+ transportType: "streamable-http",
66
+ url: "https://example.com/mcp",
67
+ risk: "high",
68
+ },
69
+ });
70
+
71
+ expect(addedEntry().defaultRiskLevel).toBe("high");
72
+ });
73
+
74
+ test("rejects an unknown risk level", async () => {
75
+ await expect(
76
+ addRoute.handler({
77
+ body: {
78
+ name: "srv",
79
+ transportType: "streamable-http",
80
+ url: "https://example.com/mcp",
81
+ risk: "extreme",
82
+ },
83
+ }),
84
+ ).rejects.toThrow(BadRequestError);
85
+ expect(saved).toBeUndefined();
86
+ });
87
+ });