@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
@@ -1,88 +1,259 @@
1
1
  /**
2
- * Route definitions for ChatGPT subscription OAuth authentication.
2
+ * Route definitions for ChatGPT subscription ("Sign in with ChatGPT") auth.
3
+ *
4
+ * Device code is the primary path: the daemon mints a short user code, the user
5
+ * types it at OpenAI's device page, and the daemon polls in the background and
6
+ * stores the tokens itself. The copy-paste PKCE path stays as the fallback for
7
+ * accounts that have not enabled device-code authorization for Codex.
8
+ *
9
+ * POST /v1/inference/chatgpt-subscription/device-auth: mint a user code and
10
+ * start the background poll. Returns `{ state, user_code, verification_url,
11
+ * expires_at, interval_seconds }`.
12
+ * GET /v1/inference/chatgpt-subscription/device-auth/status/:state: poll the
13
+ * device flow's status (`pending` | `connected` | `error`).
14
+ * DELETE /v1/inference/chatgpt-subscription/device-auth/:state: stop the
15
+ * background poll for an abandoned flow.
3
16
  *
4
17
  * POST /v1/inference/chatgpt-subscription/auth — generate a PKCE authorize
5
18
  * URL for the user to visit. Returns `{ authorize_url, state }`.
6
- *
7
19
  * POST /v1/inference/chatgpt-subscription/auth/exchange — accept the
8
- * authorization code + state from the redirect, exchange for tokens,
9
- * store in CES, and upsert the provider connection.
20
+ * authorization code + state from the redirect and exchange it for tokens.
10
21
  */
11
22
 
12
23
  import { z } from "zod";
13
24
 
14
- import { getDb } from "../../persistence/db-connection.js";
15
- import { CHATGPT_SUBSCRIPTION_CONNECTION_NAME } from "../../providers/inference/auth.js";
16
- import {
17
- createConnection,
18
- getConnection,
19
- updateConnection,
20
- } from "../../providers/inference/connections.js";
21
- import type { OAuth2Config } from "../../security/oauth2.js";
25
+ import { storeChatgptSubscriptionTokens } from "../../providers/inference/chatgpt-subscription-auth.js";
26
+ import { OPENAI_OAUTH_CONFIG } from "../../providers/inference/chatgpt-subscription-credentials.js";
22
27
  import {
23
28
  exchangeCodeForTokens,
24
29
  generateCodeChallenge,
25
30
  generateCodeVerifier,
26
31
  generateState,
27
32
  } from "../../security/oauth2.js";
28
- import { setSecureKeyAsync } from "../../security/secure-keys.js";
33
+ import {
34
+ completeDeviceAuth,
35
+ DeviceAuthError,
36
+ OPENAI_DEVICE_AUTH_MAX_LIFETIME_MS,
37
+ requestDeviceCode,
38
+ } from "../../security/openai-device-auth.js";
29
39
  import { getLogger } from "../../util/logger.js";
30
40
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
41
+ import { BadRequestError, NotFoundError } from "./errors.js";
42
+ import { createPendingFlowRegistry } from "./oauth-pending-flows.js";
31
43
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
32
44
 
33
- const log = getLogger("chatgpt-subscription-auth");
45
+ const log = getLogger("chatgpt-subscription-auth-routes");
34
46
 
35
- // ---------------------------------------------------------------------------
36
- // OAuth config
37
- // ---------------------------------------------------------------------------
47
+ const REDIRECT_URI = "http://localhost:1455/auth/callback";
38
48
 
39
- const OPENAI_OAUTH_CONFIG: OAuth2Config = {
40
- authorizeUrl: "https://auth.openai.com/oauth/authorize",
41
- tokenExchangeUrl: "https://auth.openai.com/oauth/token",
42
- clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
43
- scopes: ["openid", "profile", "email", "offline_access"],
44
- scopeSeparator: " ",
45
- authorizeParams: { id_token_add_organizations: "true" },
46
- };
49
+ /**
50
+ * Copy-paste PKCE flows, keyed by the `state` in the authorize URL. The
51
+ * verifier is held until the user pastes the redirect's code back.
52
+ */
53
+ const pendingPkceFlows = createPendingFlowRegistry<{ codeVerifier: string }>();
47
54
 
48
- const REDIRECT_URI = "http://localhost:1455/auth/callback";
49
- const CONNECTION_NAME = CHATGPT_SUBSCRIPTION_CONNECTION_NAME;
55
+ /**
56
+ * Device-code flows, keyed by OpenAI's `device_auth_id`. Each entry carries the
57
+ * controller that stops its background poll. Entries outlive the device code
58
+ * itself so a client polling right after expiry still reads the terminal status
59
+ * instead of a 404.
60
+ */
61
+ const pendingDeviceFlows = createPendingFlowRegistry<{
62
+ abort: AbortController;
63
+ }>({
64
+ ttlMs: OPENAI_DEVICE_AUTH_MAX_LIFETIME_MS + 5 * 60 * 1000,
65
+ });
66
+
67
+ /**
68
+ * The one device flow whose background poll is still running. A sign-in is a
69
+ * single-user act at a single moment, so starting one ends whichever came
70
+ * before it rather than leaving two polls hitting OpenAI every few seconds.
71
+ */
72
+ let inFlightDeviceAuthId: string | null = null;
73
+
74
+ /** Stop a flow's poll and record why, when it has not already settled. */
75
+ function cancelDeviceFlow(state: string): boolean {
76
+ const flow = pendingDeviceFlows.get(state);
77
+ if (!flow || flow.status !== "pending") {
78
+ return false;
79
+ }
80
+ flow.abort.abort();
81
+ pendingDeviceFlows.mark(state, "error", {
82
+ error: "The sign-in was cancelled.",
83
+ errorCode: "aborted",
84
+ });
85
+ if (inFlightDeviceAuthId === state) {
86
+ inFlightDeviceAuthId = null;
87
+ }
88
+ return true;
89
+ }
90
+
91
+ /** Release the in-flight slot once this flow's poll is done with it. */
92
+ function releaseDeviceFlow(state: string): void {
93
+ if (inFlightDeviceAuthId === state) {
94
+ inFlightDeviceAuthId = null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Whether `state` is still the unsettled flow the daemon is running.
100
+ *
101
+ * A cancel or a newer sign-in can land after the poll has returned an
102
+ * authorization code but before the exchange and the credential writes finish.
103
+ * Checking this on either side of the store keeps an abandoned flow from
104
+ * writing tokens the user asked it to drop, or from overwriting the
105
+ * credentials of the sign-in that replaced it.
106
+ */
107
+ function isDeviceFlowStillLive(state: string): boolean {
108
+ return (
109
+ inFlightDeviceAuthId === state &&
110
+ pendingDeviceFlows.get(state)?.status === "pending"
111
+ );
112
+ }
113
+
114
+ /** Failures the user caused or waited out: expected ends, not defects. */
115
+ function isExpectedDeviceAuthEnd(code: string): boolean {
116
+ return code === "aborted" || code === "expired_token";
117
+ }
118
+
119
+ interface DeviceAuthStartResponse {
120
+ state: string;
121
+ user_code: string;
122
+ verification_url: string;
123
+ expires_at: string;
124
+ interval_seconds: number;
125
+ }
126
+
127
+ interface FlowStatusResponse {
128
+ status: "pending" | "connected" | "error";
129
+ error?: string;
130
+ error_code?: string;
131
+ }
50
132
 
51
133
  // ---------------------------------------------------------------------------
52
- // Module-level PKCE state storage
134
+ // Device-code handlers
53
135
  // ---------------------------------------------------------------------------
54
136
 
55
- interface PendingAuth {
56
- codeVerifier: string;
57
- createdAt: number;
137
+ /**
138
+ * Mint a device code and return it immediately. The approval poll and the
139
+ * token exchange run in the background, one per `device_auth_id`, and flip the
140
+ * tracked flow's status when they settle.
141
+ */
142
+ async function handleStartDeviceAuth(
143
+ _args: RouteHandlerArgs,
144
+ ): Promise<DeviceAuthStartResponse> {
145
+ pendingDeviceFlows.cleanupExpired();
146
+ if (inFlightDeviceAuthId) {
147
+ cancelDeviceFlow(inFlightDeviceAuthId);
148
+ }
149
+
150
+ const request = await requestDeviceCode(OPENAI_OAUTH_CONFIG.clientId);
151
+ const abort = new AbortController();
152
+ pendingDeviceFlows.start(request.deviceAuthId, { abort });
153
+ inFlightDeviceAuthId = request.deviceAuthId;
154
+
155
+ void completeDeviceAuth(OPENAI_OAUTH_CONFIG, request, {
156
+ signal: abort.signal,
157
+ })
158
+ .then(async (result) => {
159
+ if (!isDeviceFlowStillLive(request.deviceAuthId)) {
160
+ log.warn(
161
+ "ChatGPT subscription device auth flow settled after it was dropped; discarding its tokens",
162
+ );
163
+ return;
164
+ }
165
+ await storeChatgptSubscriptionTokens(result.tokens);
166
+ if (!isDeviceFlowStillLive(request.deviceAuthId)) {
167
+ log.warn(
168
+ "ChatGPT subscription device auth flow was dropped while its tokens were stored",
169
+ );
170
+ return;
171
+ }
172
+ pendingDeviceFlows.mark(request.deviceAuthId, "connected");
173
+ log.info("ChatGPT subscription device auth flow connected");
174
+ })
175
+ .catch((err: unknown) => {
176
+ const message = err instanceof Error ? err.message : String(err);
177
+ const errorCode =
178
+ err instanceof DeviceAuthError ? err.code : "device_auth_failed";
179
+ pendingDeviceFlows.mark(request.deviceAuthId, "error", {
180
+ error: message,
181
+ errorCode,
182
+ });
183
+ const detail = { err: message, code: errorCode };
184
+ if (isExpectedDeviceAuthEnd(errorCode)) {
185
+ log.warn(detail, "ChatGPT subscription device auth flow ended");
186
+ } else {
187
+ log.error(detail, "ChatGPT subscription device auth flow failed");
188
+ }
189
+ })
190
+ .finally(() => releaseDeviceFlow(request.deviceAuthId));
191
+
192
+ return {
193
+ state: request.deviceAuthId,
194
+ user_code: request.userCode,
195
+ verification_url: request.verificationUrl,
196
+ expires_at: request.expiresAt,
197
+ interval_seconds: request.intervalSeconds,
198
+ };
199
+ }
200
+
201
+ function handleDeviceAuthStatus({
202
+ pathParams,
203
+ }: RouteHandlerArgs): FlowStatusResponse {
204
+ const { state } = pathParams as { state: string };
205
+ const report = pendingDeviceFlows.readStatus(state);
206
+
207
+ if (!report) {
208
+ throw new NotFoundError(
209
+ "No active ChatGPT device sign-in for the given state. Restart the sign-in flow.",
210
+ );
211
+ }
212
+
213
+ const response: FlowStatusResponse = { status: report.status };
214
+ if (report.error !== undefined) {
215
+ response.error = report.error;
216
+ }
217
+ if (report.errorCode !== undefined) {
218
+ response.error_code = report.errorCode;
219
+ }
220
+ return response;
58
221
  }
59
222
 
60
- const pendingAuths = new Map<string, PendingAuth>();
223
+ /**
224
+ * Stop an abandoned flow's background poll. Without this an abandoned sign-in
225
+ * keeps polling OpenAI every few seconds until the code expires.
226
+ */
227
+ function handleCancelDeviceAuth({ pathParams }: RouteHandlerArgs): {
228
+ cancelled: boolean;
229
+ } {
230
+ const { state } = pathParams as { state: string };
61
231
 
62
- const PENDING_AUTH_TTL_MS = 10 * 60 * 1000; // 10 minutes
232
+ if (!pendingDeviceFlows.get(state)) {
233
+ throw new NotFoundError(
234
+ "No active ChatGPT device sign-in for the given state.",
235
+ );
236
+ }
63
237
 
64
- /** Remove entries older than 10 minutes. */
65
- function cleanupExpiredEntries(): void {
66
- const cutoff = Date.now() - PENDING_AUTH_TTL_MS;
67
- for (const [key, entry] of pendingAuths) {
68
- if (entry.createdAt < cutoff) {
69
- pendingAuths.delete(key);
70
- }
238
+ const cancelled = cancelDeviceFlow(state);
239
+ if (cancelled) {
240
+ log.warn("ChatGPT subscription device auth flow cancelled by the client");
71
241
  }
242
+ return { cancelled };
72
243
  }
73
244
 
74
245
  // ---------------------------------------------------------------------------
75
- // Handlers
246
+ // Copy-paste PKCE handlers
76
247
  // ---------------------------------------------------------------------------
77
248
 
78
249
  async function handleStartAuth(_args: RouteHandlerArgs) {
79
- cleanupExpiredEntries();
250
+ pendingPkceFlows.cleanupExpired();
80
251
 
81
252
  const codeVerifier = generateCodeVerifier();
82
253
  const codeChallenge = generateCodeChallenge(codeVerifier);
83
254
  const state = generateState();
84
255
 
85
- pendingAuths.set(state, { codeVerifier, createdAt: Date.now() });
256
+ pendingPkceFlows.start(state, { codeVerifier });
86
257
 
87
258
  const params = new URLSearchParams({
88
259
  response_type: "code",
@@ -103,18 +274,19 @@ async function handleStartAuth(_args: RouteHandlerArgs) {
103
274
  async function handleExchange(args: RouteHandlerArgs) {
104
275
  const { code, state } = args.body as { code: string; state: string };
105
276
 
106
- const pending = pendingAuths.get(state);
277
+ const pending = pendingPkceFlows.get(state);
107
278
  if (!pending) {
108
- throw new Error(
279
+ throw new BadRequestError(
109
280
  "Invalid or expired state parameter. Please restart the auth flow.",
110
281
  );
111
282
  }
112
283
 
113
- pendingAuths.delete(state);
284
+ pendingPkceFlows.delete(state);
114
285
 
115
- // Check TTL
116
- if (Date.now() - pending.createdAt > PENDING_AUTH_TTL_MS) {
117
- throw new Error("Auth flow expired. Please restart the auth flow.");
286
+ if (pendingPkceFlows.isExpired(pending)) {
287
+ throw new BadRequestError(
288
+ "Auth flow expired. Please restart the auth flow.",
289
+ );
118
290
  }
119
291
 
120
292
  const { tokens } = await exchangeCodeForTokens(
@@ -124,70 +296,7 @@ async function handleExchange(args: RouteHandlerArgs) {
124
296
  pending.codeVerifier,
125
297
  );
126
298
 
127
- // Store tokens in CES
128
- const accessStored = await setSecureKeyAsync(
129
- "credential/chatgpt/access_token",
130
- tokens.accessToken,
131
- );
132
- if (!accessStored) {
133
- log.error("Failed to store ChatGPT access token in CES");
134
- throw new Error("Failed to store access token");
135
- }
136
-
137
- if (tokens.refreshToken) {
138
- const refreshStored = await setSecureKeyAsync(
139
- "credential/chatgpt/refresh_token",
140
- tokens.refreshToken,
141
- );
142
- if (!refreshStored) {
143
- log.error("Failed to store ChatGPT refresh token in CES");
144
- throw new Error("Failed to store refresh token");
145
- }
146
- }
147
-
148
- if (tokens.expiresIn) {
149
- const expiresAt = Math.floor(Date.now() / 1000 + tokens.expiresIn);
150
- await setSecureKeyAsync("credential/chatgpt/expires_at", String(expiresAt));
151
- }
152
-
153
- // Upsert provider connection
154
- const db = getDb();
155
- const authInput = {
156
- type: "oauth_subscription" as const,
157
- credential: "credential/chatgpt/access_token",
158
- };
159
-
160
- const existing = getConnection(db, CONNECTION_NAME);
161
- if (existing) {
162
- // Stamp the provider with the auth: this row IS the subscription route
163
- // (auth modality = provider identity), and dispatch derives the openai
164
- // upstream from the identity per-request. Stamping also heals a
165
- // claiming row whose provider would otherwise misroute the fresh token.
166
- const updateResult = updateConnection(db, CONNECTION_NAME, {
167
- auth: authInput,
168
- provider: "chatgpt",
169
- });
170
- if (!updateResult.ok) {
171
- log.error(
172
- { error: updateResult.error },
173
- "Failed to update chatgpt-subscription connection",
174
- );
175
- throw new Error("Failed to update connection");
176
- }
177
- } else {
178
- const createResult = createConnection(db, {
179
- name: CONNECTION_NAME,
180
- provider: "chatgpt",
181
- auth: authInput,
182
- });
183
- if (!createResult.ok) {
184
- log.error(
185
- { error: createResult.error },
186
- "Failed to create chatgpt-subscription connection",
187
- );
188
- throw new Error("Failed to create connection");
189
- }
190
- }
299
+ await storeChatgptSubscriptionTokens(tokens);
191
300
 
192
301
  log.info("ChatGPT subscription auth flow completed successfully");
193
302
  return { ok: true };
@@ -198,6 +307,80 @@ async function handleExchange(args: RouteHandlerArgs) {
198
307
  // ---------------------------------------------------------------------------
199
308
 
200
309
  export const ROUTES: RouteDefinition[] = [
310
+ {
311
+ operationId: "inference_chatgpt_subscription_device_auth",
312
+ endpoint: "inference/chatgpt-subscription/device-auth",
313
+ method: "POST",
314
+ policy: {
315
+ requiredScopes: ["settings.write"],
316
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
317
+ },
318
+ summary: "Start ChatGPT subscription device-code sign-in",
319
+ description:
320
+ "Mint a short user code the user types at OpenAI's device page, and " +
321
+ "start a background poll that stores the tokens and upserts the " +
322
+ "provider connection once the sign-in is approved. Returns immediately; " +
323
+ "poll `.../device-auth/status/{state}` for the outcome. The account must " +
324
+ "have device-code authorization for Codex enabled in its ChatGPT " +
325
+ "security settings.",
326
+ tags: ["inference"],
327
+ responseBody: z.object({
328
+ state: z.string(),
329
+ user_code: z.string(),
330
+ verification_url: z.string(),
331
+ expires_at: z.string(),
332
+ interval_seconds: z.number(),
333
+ }),
334
+ handler: handleStartDeviceAuth,
335
+ },
336
+ {
337
+ operationId: "inference_chatgpt_subscription_device_auth_status",
338
+ endpoint: "inference/chatgpt-subscription/device-auth/status/:state",
339
+ method: "GET",
340
+ policy: {
341
+ requiredScopes: ["settings.read"],
342
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
343
+ },
344
+ summary: "Poll ChatGPT subscription device-code sign-in status",
345
+ description:
346
+ "Returns the current status of an in-flight device-code sign-in " +
347
+ "(pending/connected/error) so the client knows when the tokens have " +
348
+ "landed. A failure carries OpenAI's error code in `error_code`.",
349
+ tags: ["inference"],
350
+ pathParams: [{ name: "state" }],
351
+ additionalResponses: {
352
+ "404": { description: "No active device sign-in for the given state" },
353
+ },
354
+ responseBody: z.object({
355
+ status: z.enum(["pending", "connected", "error"]),
356
+ error: z.string().optional(),
357
+ error_code: z.string().optional(),
358
+ }),
359
+ handler: handleDeviceAuthStatus,
360
+ },
361
+ {
362
+ operationId: "inference_chatgpt_subscription_device_auth_cancel",
363
+ endpoint: "inference/chatgpt-subscription/device-auth/:state",
364
+ method: "DELETE",
365
+ policy: {
366
+ requiredScopes: ["settings.write"],
367
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
368
+ },
369
+ summary: "Cancel a ChatGPT subscription device-code sign-in",
370
+ description:
371
+ "Stop the background poll for a device-code sign-in the user walked " +
372
+ "away from, and mark the flow `error` with code `aborted`. A flow that " +
373
+ "already settled is left as it is. 404s an unknown state.",
374
+ tags: ["inference"],
375
+ pathParams: [{ name: "state" }],
376
+ additionalResponses: {
377
+ "404": { description: "No active device sign-in for the given state" },
378
+ },
379
+ responseBody: z.object({
380
+ cancelled: z.boolean(),
381
+ }),
382
+ handler: handleCancelDeviceAuth,
383
+ },
201
384
  {
202
385
  operationId: "inference_chatgpt_subscription_auth",
203
386
  endpoint: "inference/chatgpt-subscription/auth",
@@ -8,8 +8,10 @@
8
8
  * 4. User enters an address; client POSTs to the gateway's
9
9
  * `POST /v1/contacts/prompt` HTTP route.
10
10
  * 5. Gateway upserts the contact + channel (gateway owns all contact writes).
11
- * 6. Gateway calls daemon IPC `resolve_contact_prompt` with the new contact info.
12
- * 7. Daemon resolves the pending promise; `contacts/prompt` IPC returns to CLI.
11
+ * 6. If the prompt was opened with --verify, the gateway reads
12
+ * `contact_prompt_flags` and attests the new channel.
13
+ * 7. Gateway calls daemon IPC `resolve_contact_prompt` with the new contact info.
14
+ * 8. Daemon resolves the pending promise; `contacts/prompt` IPC returns to CLI.
13
15
  *
14
16
  * The daemon only broadcasts the prompt and waits. It never writes contacts.
15
17
  * All writes go through the gateway.
@@ -44,6 +46,8 @@ export interface ContactPromptResult {
44
46
  interface PendingContactPrompt {
45
47
  resolve: (result: ContactPromptResult) => void;
46
48
  timer: ReturnType<typeof setTimeout>;
49
+ /** When true, the gateway marks the submitted channel verified (manual attest). */
50
+ verify: boolean;
47
51
  }
48
52
 
49
53
  const pendingContactPrompts = new Map<string, PendingContactPrompt>();
@@ -122,6 +126,16 @@ const ContactPromptParams = z.object({
122
126
  .enum(["guardian", "trusted-contact", "unknown"])
123
127
  .default("unknown")
124
128
  .describe("Intended role of the contact being registered."),
129
+ verify: z
130
+ .boolean()
131
+ .optional()
132
+ .describe(
133
+ "When true, submitting the address also marks the channel verified. Same attest as Contacts Verify me.",
134
+ ),
135
+ });
136
+
137
+ const ContactPromptFlagsParams = z.object({
138
+ requestId: z.string().describe("The pending contact_request id."),
125
139
  });
126
140
 
127
141
  // ---------------------------------------------------------------------------
@@ -131,8 +145,15 @@ const ContactPromptParams = z.object({
131
145
  async function handleContactPrompt({
132
146
  body = {},
133
147
  }: RouteHandlerArgs): Promise<ContactPromptResult> {
134
- const { channel, placeholder, defaultValue, label, description, role } =
135
- ContactPromptParams.parse(body);
148
+ const {
149
+ channel,
150
+ placeholder,
151
+ defaultValue,
152
+ label,
153
+ description,
154
+ role,
155
+ verify,
156
+ } = ContactPromptParams.parse(body);
136
157
 
137
158
  const requestId = uuid();
138
159
 
@@ -143,7 +164,11 @@ async function handleContactPrompt({
143
164
  resolve({ ok: false, error: "Prompt timed out" });
144
165
  }, CONTACT_PROMPT_TIMEOUT_MS);
145
166
 
146
- pendingContactPrompts.set(requestId, { resolve, timer });
167
+ pendingContactPrompts.set(requestId, {
168
+ resolve,
169
+ timer,
170
+ verify: verify === true,
171
+ });
147
172
 
148
173
  broadcastMessage({
149
174
  type: "contact_request",
@@ -156,10 +181,26 @@ async function handleContactPrompt({
156
181
  role,
157
182
  });
158
183
 
159
- log.info({ requestId, channel, role }, "Contact prompt broadcast");
184
+ log.info(
185
+ { requestId, channel, role, verify: verify === true },
186
+ "Contact prompt broadcast",
187
+ );
160
188
  });
161
189
  }
162
190
 
191
+ /**
192
+ * Read-only flags for a pending prompt. The gateway asks this after it
193
+ * writes the channel so a `--verify` prompt can attest without the client
194
+ * having to echo the flag on submit.
195
+ */
196
+ function readContactPromptFlags({ body = {} }: RouteHandlerArgs): {
197
+ verify: boolean;
198
+ } {
199
+ const { requestId } = ContactPromptFlagsParams.parse(body);
200
+ const pending = pendingContactPrompts.get(requestId);
201
+ return { verify: pending?.verify === true };
202
+ }
203
+
163
204
  // ---------------------------------------------------------------------------
164
205
  // Routes
165
206
  // ---------------------------------------------------------------------------
@@ -202,4 +243,22 @@ export const CONTACT_PROMPT_ROUTES: RouteDefinition[] = [
202
243
  "Called by the gateway after it writes the contact and channel. Unblocks the waiting contacts/prompt IPC call.",
203
244
  tags: ["contacts"],
204
245
  },
246
+ {
247
+ operationId: "contact_prompt_flags",
248
+ endpoint: "contact_prompt_flags",
249
+ method: "POST",
250
+ policy: {
251
+ requiredScopes: ["settings.write"],
252
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
253
+ },
254
+ handler: readContactPromptFlags,
255
+ summary: "Read flags for a pending contact prompt",
256
+ description:
257
+ "Returns whether the pending prompt asked the gateway to mark the submitted channel verified.",
258
+ tags: ["contacts"],
259
+ requestBody: ContactPromptFlagsParams,
260
+ responseBody: z.object({
261
+ verify: z.boolean(),
262
+ }),
263
+ },
205
264
  ];
@@ -12,6 +12,7 @@ import {
12
12
  INVITES_IPC_METHODS,
13
13
  RedeemInviteByTokenRequestSchema,
14
14
  RedeemVoiceInviteRequestSchema,
15
+ RiskThresholdSchema,
15
16
  } from "@vellumai/gateway-client";
16
17
  import type { ContactRead } from "@vellumai/gateway-client/gateway-ipc-contracts";
17
18
  import {
@@ -173,11 +174,12 @@ function isContactType(value: string): value is ContactType {
173
174
  // `role` is gateway-sourced: relayed reads trust the role on the `ContactRead`,
174
175
  // while daemon-native reads derive it from the gateway guardian id set at the
175
176
  // serve layer (see prepareContactResponse). Interaction telemetry
176
- // (lastSeenAt/interactionCount/lastInteraction) is gateway-owned: relayed reads
177
- // carry it directly, and daemon-native reads batch-hydrate it from the gateway
178
- // (see hydrateTelemetryFromGateway). On a gateway fail-soft the count
179
- // `interactionCount` defaults to 0 (never served as null, so callers render a
180
- // real number); the `lastSeenAt`/`lastInteraction` timestamps degrade to null.
177
+ // (lastSeenAt/interactionCount/lastInteraction) and `autoApproveThreshold`
178
+ // are gateway-owned: relayed reads carry them directly, and daemon-native
179
+ // reads batch-hydrate them from the gateway (see hydrateTelemetryFromGateway).
180
+ // On a gateway fail-soft the count `interactionCount` defaults to 0 (never
181
+ // served as null, so callers render a real number); the
182
+ // `lastSeenAt`/`lastInteraction` timestamps and the ceiling degrade to null.
181
183
  // The timestamp fields stay `.nullable()`; `interactionCount` is kept nullable
182
184
  // defensively for the relay path, but is never emitted null.
183
185
  const contactChannelSchema = z.object({
@@ -207,6 +209,7 @@ const contactSchema = z.object({
207
209
  contactType: z.enum(["human", "assistant"]),
208
210
  lastInteraction: z.number().nullable().optional(),
209
211
  interactionCount: z.number().nullable(),
212
+ autoApproveThreshold: RiskThresholdSchema.nullable().optional(),
210
213
  createdAt: z.number(),
211
214
  updatedAt: z.number(),
212
215
  channels: z.array(contactChannelSchema),
@@ -252,6 +255,7 @@ async function relayListContacts(limit: number, role: ContactRole | undefined) {
252
255
  type ContactWithGatewayTelemetry = Omit<ContactWithChannels, "channels"> & {
253
256
  interactionCount: number;
254
257
  lastInteraction: number | null;
258
+ autoApproveThreshold: ContactRead["autoApproveThreshold"];
255
259
  channels: Array<
256
260
  ContactChannel & {
257
261
  lastSeenAt: number | null;
@@ -271,16 +275,16 @@ function channelKey(type: string, address: string): string {
271
275
  }
272
276
 
273
277
  /**
274
- * Overlay gateway-owned interaction telemetry onto daemon-native contact reads
275
- * (search / contactType-filtered), which bypass the gateway list relay. The
276
- * daemon still owns the FILTERING (gateway-native search/contactType is
277
- * design-blocked), but telemetry (contact + channel
278
- * interactionCount/lastInteraction, channel lastSeenAt) is gateway-owned — so
279
- * batch-fetch it via `contacts_list_rich` keyed by the filtered id set and
280
- * overlay it, keeping the local assistant-DB aggregation out of the served
281
- * payload. Fail-soft: if the gateway read fails or omits a contact, its
282
- * interaction counts degrade to 0 and its timestamps to null rather than
283
- * falling back to the local assistant-DB aggregation.
278
+ * Overlay gateway-owned interaction telemetry and the contact auto-approve
279
+ * ceiling onto daemon-native contact reads (search / contactType-filtered),
280
+ * which bypass the gateway list relay. The daemon still owns the FILTERING
281
+ * (gateway-native search/contactType is design-blocked), but telemetry
282
+ * (contact + channel interactionCount/lastInteraction, channel lastSeenAt)
283
+ * and `autoApproveThreshold` are gateway-owned, so batch-fetch them via
284
+ * `contacts_list_rich` keyed by the filtered id set and overlay them. Fail-soft:
285
+ * if the gateway read fails or omits a contact, counts degrade to 0,
286
+ * timestamps to null, and the ceiling to null rather than falling back to
287
+ * the local assistant-DB aggregation.
284
288
  */
285
289
  async function hydrateTelemetryFromGateway(
286
290
  contacts: ContactWithChannels[],
@@ -321,6 +325,7 @@ async function hydrateTelemetryFromGateway(
321
325
  ...c,
322
326
  interactionCount: gw?.interactionCount ?? 0,
323
327
  lastInteraction: gw?.lastInteraction ?? null,
328
+ autoApproveThreshold: gw?.autoApproveThreshold ?? null,
324
329
  channels: c.channels.map((ch) => {
325
330
  const gwCh =
326
331
  gwChannelById.get(ch.id) ??