@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,189 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, mock, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import { migrateCreateProviderConnections } from "../../../memory/migrations/243-provider-connections.js";
7
+ import { migrateProviderConnectionStatusLabel } from "../../../memory/migrations/244-provider-connection-status-label.js";
8
+ import { migrateProviderConnectionBaseUrlAndModels } from "../../../memory/migrations/250-provider-connection-base-url-and-models.js";
9
+ import { migrateStripBaseUrlNonOpenaiCompatible } from "../../../memory/migrations/257-strip-base-url-non-openai-compatible.js";
10
+ import * as schema from "../../../memory/schema.js";
11
+ import { providerConnections } from "../../../memory/schema/inference.js";
12
+ import { getConnection } from "../connections.js";
13
+ import { resolveAuth } from "../resolve-auth.js";
14
+
15
+ function createTestDb() {
16
+ const sqlite = new Database(":memory:");
17
+ sqlite.exec("PRAGMA journal_mode=WAL");
18
+ return drizzle(sqlite, { schema });
19
+ }
20
+
21
+ function bootDb() {
22
+ const db = createTestDb();
23
+ migrateCreateProviderConnections(db);
24
+ migrateProviderConnectionStatusLabel(db);
25
+ migrateProviderConnectionBaseUrlAndModels(db);
26
+ return db;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Migration: strip base_url from non-openai-compatible connections
31
+ // ---------------------------------------------------------------------------
32
+
33
+ describe("migration 257: strip base_url from non-openai-compatible connections", () => {
34
+ test("clears base_url on anthropic connection", () => {
35
+ const db = bootDb();
36
+
37
+ // Manually insert a row with a base_url on an anthropic provider (simulating
38
+ // a row created before the validation was added).
39
+ const now = Date.now();
40
+ db.insert(providerConnections)
41
+ .values({
42
+ name: "bad-anthropic",
43
+ provider: "anthropic",
44
+ auth: JSON.stringify({ type: "api_key", credential: "cred-abc" }),
45
+ status: "active",
46
+ baseUrl: "https://evil.example.com/v1",
47
+ createdAt: now,
48
+ updatedAt: now,
49
+ })
50
+ .run();
51
+
52
+ migrateStripBaseUrlNonOpenaiCompatible(db);
53
+
54
+ const conn = getConnection(db, "bad-anthropic");
55
+ expect(conn).not.toBeNull();
56
+ expect(conn!.baseUrl).toBeNull();
57
+ });
58
+
59
+ test("preserves base_url on openai-compatible connection", () => {
60
+ const db = bootDb();
61
+
62
+ const now = Date.now();
63
+ db.insert(providerConnections)
64
+ .values({
65
+ name: "good-vllm",
66
+ provider: "openai-compatible",
67
+ auth: JSON.stringify({ type: "api_key", credential: "cred-vllm" }),
68
+ status: "active",
69
+ baseUrl: "https://my-vllm.example.com/v1",
70
+ models: JSON.stringify([{ id: "my-model" }]),
71
+ createdAt: now,
72
+ updatedAt: now,
73
+ })
74
+ .run();
75
+
76
+ migrateStripBaseUrlNonOpenaiCompatible(db);
77
+
78
+ const conn = getConnection(db, "good-vllm");
79
+ expect(conn).not.toBeNull();
80
+ expect(conn!.baseUrl).toBe("https://my-vllm.example.com/v1");
81
+ });
82
+
83
+ test("is idempotent", () => {
84
+ const db = bootDb();
85
+
86
+ const now = Date.now();
87
+ db.insert(providerConnections)
88
+ .values({
89
+ name: "bad-openai",
90
+ provider: "openai",
91
+ auth: JSON.stringify({ type: "api_key", credential: "cred-abc" }),
92
+ status: "active",
93
+ baseUrl: "https://evil.example.com/v1",
94
+ createdAt: now,
95
+ updatedAt: now,
96
+ })
97
+ .run();
98
+
99
+ migrateStripBaseUrlNonOpenaiCompatible(db);
100
+ migrateStripBaseUrlNonOpenaiCompatible(db);
101
+
102
+ const conn = getConnection(db, "bad-openai");
103
+ expect(conn).not.toBeNull();
104
+ expect(conn!.baseUrl).toBeNull();
105
+ });
106
+ });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // resolveAuth defense-in-depth: strip baseUrl for non-openai-compatible
110
+ // ---------------------------------------------------------------------------
111
+
112
+ describe("resolveAuth defense-in-depth", () => {
113
+ // Mock the secure key store to return a predictable value.
114
+ mock.module("../../../security/secure-keys.js", () => ({
115
+ getSecureKeyAsync: async (credential: string) =>
116
+ credential === "cred-test" ? "sk-test-key" : null,
117
+ }));
118
+
119
+ // Mock the platform proxy context to avoid real platform calls.
120
+ mock.module("../../../providers/platform-proxy/context.js", () => ({
121
+ buildManagedBaseUrl: async () => null,
122
+ resolveManagedProxyContext: async () => ({
123
+ assistantApiKey: "platform-key",
124
+ }),
125
+ }));
126
+
127
+ test("strips baseUrl for anthropic provider", async () => {
128
+ const result = await resolveAuth(
129
+ { type: "api_key", credential: "cred-test" },
130
+ "anthropic",
131
+ { baseUrl: "https://evil.example.com/v1" },
132
+ );
133
+ expect(result.ok).toBe(true);
134
+ if (result.ok) {
135
+ expect(result.resolved.kind).toBe("header");
136
+ if (result.resolved.kind === "header") {
137
+ expect(result.resolved.baseUrl).toBeUndefined();
138
+ }
139
+ }
140
+ });
141
+
142
+ test("strips baseUrl for openai provider", async () => {
143
+ const result = await resolveAuth(
144
+ { type: "api_key", credential: "cred-test" },
145
+ "openai",
146
+ { baseUrl: "https://evil.example.com/v1" },
147
+ );
148
+ expect(result.ok).toBe(true);
149
+ if (result.ok && result.resolved.kind === "header") {
150
+ expect(result.resolved.baseUrl).toBeUndefined();
151
+ }
152
+ });
153
+
154
+ test("strips baseUrl for gemini provider", async () => {
155
+ const result = await resolveAuth(
156
+ { type: "api_key", credential: "cred-test" },
157
+ "gemini",
158
+ { baseUrl: "https://evil.example.com/v1" },
159
+ );
160
+ expect(result.ok).toBe(true);
161
+ if (result.ok && result.resolved.kind === "header") {
162
+ expect(result.resolved.baseUrl).toBeUndefined();
163
+ }
164
+ });
165
+
166
+ test("preserves baseUrl for openai-compatible provider", async () => {
167
+ const result = await resolveAuth(
168
+ { type: "api_key", credential: "cred-test" },
169
+ "openai-compatible",
170
+ { baseUrl: "https://my-vllm.example.com/v1" },
171
+ );
172
+ expect(result.ok).toBe(true);
173
+ if (result.ok && result.resolved.kind === "header") {
174
+ expect(result.resolved.baseUrl).toBe("https://my-vllm.example.com/v1");
175
+ }
176
+ });
177
+
178
+ test("handles null baseUrl gracefully for any provider", async () => {
179
+ const result = await resolveAuth(
180
+ { type: "api_key", credential: "cred-test" },
181
+ "anthropic",
182
+ { baseUrl: null },
183
+ );
184
+ expect(result.ok).toBe(true);
185
+ if (result.ok && result.resolved.kind === "header") {
186
+ expect(result.resolved.baseUrl).toBeUndefined();
187
+ }
188
+ });
189
+ });
@@ -0,0 +1,254 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ mock,
7
+ test,
8
+ } from "bun:test";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Mocks
12
+ // ---------------------------------------------------------------------------
13
+
14
+ mock.module("../../../util/logger.js", () => ({
15
+ getLogger: () =>
16
+ new Proxy({} as Record<string, unknown>, {
17
+ get: () => () => {},
18
+ }),
19
+ }));
20
+
21
+ const mockGetSecureKey = mock<(key: string) => Promise<string | undefined>>(
22
+ async () => undefined,
23
+ );
24
+ const mockSetSecureKey = mock<(key: string, value: string) => Promise<boolean>>(
25
+ async () => true,
26
+ );
27
+
28
+ mock.module("../../../security/secure-keys.js", () => ({
29
+ getSecureKeyAsync: mockGetSecureKey,
30
+ setSecureKeyAsync: mockSetSecureKey,
31
+ }));
32
+
33
+ const mockRefreshOAuth2Token = mock<
34
+ (...args: unknown[]) => Promise<{
35
+ accessToken: string;
36
+ refreshToken?: string;
37
+ expiresIn?: number;
38
+ }>
39
+ >(async () => ({
40
+ accessToken: "new-access-token",
41
+ refreshToken: "new-refresh-token",
42
+ expiresIn: 3600,
43
+ }));
44
+
45
+ mock.module("../../../security/oauth2.js", () => ({
46
+ refreshOAuth2Token: mockRefreshOAuth2Token,
47
+ }));
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Import under test (after mocks)
51
+ // ---------------------------------------------------------------------------
52
+
53
+ import {
54
+ _resetRefreshMutex,
55
+ getValidCodexAccessToken,
56
+ } from "../codex-token-refresh.js";
57
+
58
+ const PREFIX = "credential/openai-codex";
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function setSecureKeyMap(map: Record<string, string>): void {
65
+ mockGetSecureKey.mockImplementation(async (key: string) => map[key]);
66
+ }
67
+
68
+ function futureTimestamp(secondsFromNow: number): string {
69
+ return String(Math.floor(Date.now() / 1000) + secondsFromNow);
70
+ }
71
+
72
+ function pastTimestamp(secondsAgo: number): string {
73
+ return String(Math.floor(Date.now() / 1000) - secondsAgo);
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Tests
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe("getValidCodexAccessToken", () => {
81
+ beforeEach(() => {
82
+ mockGetSecureKey.mockReset();
83
+ mockSetSecureKey.mockReset();
84
+ mockRefreshOAuth2Token.mockReset();
85
+ _resetRefreshMutex();
86
+
87
+ mockSetSecureKey.mockImplementation(async () => true);
88
+ mockRefreshOAuth2Token.mockImplementation(async () => ({
89
+ accessToken: "new-access-token",
90
+ refreshToken: "new-refresh-token",
91
+ expiresIn: 3600,
92
+ }));
93
+ });
94
+
95
+ afterEach(() => {
96
+ _resetRefreshMutex();
97
+ });
98
+
99
+ test("returns null when no access token is stored", async () => {
100
+ setSecureKeyMap({});
101
+ const result = await getValidCodexAccessToken(PREFIX);
102
+ expect(result).toBeNull();
103
+ });
104
+
105
+ test("returns access token when no expires_at is stored (graceful degradation)", async () => {
106
+ setSecureKeyMap({
107
+ [`${PREFIX}/access_token`]: "my-token",
108
+ });
109
+ const result = await getValidCodexAccessToken(PREFIX);
110
+ expect(result).toBe("my-token");
111
+ expect(mockRefreshOAuth2Token).not.toHaveBeenCalled();
112
+ });
113
+
114
+ test("returns access token when not expired", async () => {
115
+ setSecureKeyMap({
116
+ [`${PREFIX}/access_token`]: "my-token",
117
+ [`${PREFIX}/expires_at`]: futureTimestamp(600), // 10 minutes from now
118
+ });
119
+ const result = await getValidCodexAccessToken(PREFIX);
120
+ expect(result).toBe("my-token");
121
+ expect(mockRefreshOAuth2Token).not.toHaveBeenCalled();
122
+ });
123
+
124
+ test("refreshes token when expired", async () => {
125
+ const keys: Record<string, string> = {
126
+ [`${PREFIX}/access_token`]: "old-token",
127
+ [`${PREFIX}/refresh_token`]: "old-refresh",
128
+ [`${PREFIX}/expires_at`]: pastTimestamp(60), // expired 1 minute ago
129
+ };
130
+ setSecureKeyMap(keys);
131
+
132
+ const result = await getValidCodexAccessToken(PREFIX);
133
+
134
+ expect(result).toBe("new-access-token");
135
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
136
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledWith(
137
+ "https://auth.openai.com/oauth/token",
138
+ "app_EMoamEEZ73f0CkXaXp7hrann",
139
+ "old-refresh",
140
+ );
141
+
142
+ // Verify new tokens are stored
143
+ expect(mockSetSecureKey).toHaveBeenCalledWith(
144
+ `${PREFIX}/access_token`,
145
+ "new-access-token",
146
+ );
147
+ expect(mockSetSecureKey).toHaveBeenCalledWith(
148
+ `${PREFIX}/refresh_token`,
149
+ "new-refresh-token",
150
+ );
151
+ // expires_at should be stored as well
152
+ const expiresAtCall = mockSetSecureKey.mock.calls.find(
153
+ (c) => c[0] === `${PREFIX}/expires_at`,
154
+ );
155
+ expect(expiresAtCall).toBeDefined();
156
+ const storedExpiresAt = Number(expiresAtCall![1]);
157
+ const now = Math.floor(Date.now() / 1000);
158
+ // Should be approximately now + 3600 (within 5 seconds tolerance)
159
+ expect(storedExpiresAt).toBeGreaterThan(now + 3590);
160
+ expect(storedExpiresAt).toBeLessThanOrEqual(now + 3610);
161
+ });
162
+
163
+ test("refreshes token when about to expire (within 5-minute margin)", async () => {
164
+ setSecureKeyMap({
165
+ [`${PREFIX}/access_token`]: "old-token",
166
+ [`${PREFIX}/refresh_token`]: "old-refresh",
167
+ [`${PREFIX}/expires_at`]: futureTimestamp(60), // only 1 minute left
168
+ });
169
+
170
+ const result = await getValidCodexAccessToken(PREFIX);
171
+
172
+ expect(result).toBe("new-access-token");
173
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
174
+ });
175
+
176
+ test("concurrent refresh calls are deduplicated (mutex)", async () => {
177
+ setSecureKeyMap({
178
+ [`${PREFIX}/access_token`]: "old-token",
179
+ [`${PREFIX}/refresh_token`]: "old-refresh",
180
+ [`${PREFIX}/expires_at`]: pastTimestamp(60),
181
+ });
182
+
183
+ // Use a deferred promise to control when the refresh completes.
184
+ // We create it upfront so the mock captures it synchronously.
185
+ let resolveRefresh!: (v: {
186
+ accessToken: string;
187
+ refreshToken: string;
188
+ expiresIn: number;
189
+ }) => void;
190
+ const refreshPromise = new Promise<{
191
+ accessToken: string;
192
+ refreshToken: string;
193
+ expiresIn: number;
194
+ }>((resolve) => {
195
+ resolveRefresh = resolve;
196
+ });
197
+
198
+ mockRefreshOAuth2Token.mockImplementation(() => refreshPromise);
199
+
200
+ // Fire two concurrent refreshes
201
+ const p1 = getValidCodexAccessToken(PREFIX);
202
+ const p2 = getValidCodexAccessToken(PREFIX);
203
+
204
+ // Allow the async get-key calls to settle before resolving refresh
205
+ await new Promise((r) => setTimeout(r, 10));
206
+
207
+ // Resolve the single in-flight refresh
208
+ resolveRefresh({
209
+ accessToken: "shared-new-token",
210
+ refreshToken: "shared-new-refresh",
211
+ expiresIn: 3600,
212
+ });
213
+
214
+ const [r1, r2] = await Promise.all([p1, p2]);
215
+
216
+ expect(r1).toBe("shared-new-token");
217
+ expect(r2).toBe("shared-new-token");
218
+ // Only one refresh call should have been made
219
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ test("falls back to existing token when refresh fails", async () => {
223
+ setSecureKeyMap({
224
+ [`${PREFIX}/access_token`]: "old-token",
225
+ [`${PREFIX}/refresh_token`]: "old-refresh",
226
+ [`${PREFIX}/expires_at`]: pastTimestamp(60),
227
+ });
228
+
229
+ mockRefreshOAuth2Token.mockImplementation(async () => {
230
+ throw new Error("OAuth2 token refresh failed (HTTP 400: invalid_grant)");
231
+ });
232
+
233
+ const result = await getValidCodexAccessToken(PREFIX);
234
+
235
+ // Should fall back to the existing access token
236
+ expect(result).toBe("old-token");
237
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
238
+ });
239
+
240
+ test("falls back to existing token when no refresh token available", async () => {
241
+ setSecureKeyMap({
242
+ [`${PREFIX}/access_token`]: "old-token",
243
+ // no refresh_token
244
+ [`${PREFIX}/expires_at`]: pastTimestamp(60),
245
+ });
246
+
247
+ const result = await getValidCodexAccessToken(PREFIX);
248
+
249
+ // Should return the existing access token
250
+ expect(result).toBe("old-token");
251
+ // Should not attempt a refresh
252
+ expect(mockRefreshOAuth2Token).not.toHaveBeenCalled();
253
+ });
254
+ });
@@ -41,6 +41,8 @@ export interface AdapterCreateOpts {
41
41
  baseURL?: string;
42
42
  /** Forwarded to providers that wire native provider-side web search. */
43
43
  useNativeWebSearch: boolean;
44
+ /** When true, the OpenAI adapter targets the Codex subscription endpoint. */
45
+ codexSubscription?: boolean;
44
46
  }
45
47
 
46
48
  type AdapterFactory = (opts: AdapterCreateOpts) => Provider;
@@ -65,10 +67,18 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
65
67
  streamTimeoutMs,
66
68
  ...(baseURL ? { baseURL } : {}),
67
69
  }),
68
- openai: ({ apiKey, model, streamTimeoutMs, baseURL, useNativeWebSearch }) =>
70
+ openai: ({
71
+ apiKey,
72
+ model,
73
+ streamTimeoutMs,
74
+ baseURL,
75
+ useNativeWebSearch,
76
+ codexSubscription,
77
+ }) =>
69
78
  new OpenAIResponsesProvider(apiKey, model, {
70
79
  useNativeWebSearch,
71
80
  streamTimeoutMs,
81
+ codexSubscription,
72
82
  ...(baseURL ? { baseURL } : {}),
73
83
  }),
74
84
  gemini: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
@@ -176,12 +186,16 @@ export function createAdapterFromConnection(
176
186
  const baseURL =
177
187
  resolvedAuth.kind === "header" ? resolvedAuth.baseUrl : undefined;
178
188
 
189
+ const codexSubscription =
190
+ connection.auth.type === "oauth_subscription" && provider === "openai";
191
+
179
192
  const adapter = buildProviderAdapter(provider, {
180
193
  apiKey,
181
194
  model: opts.model,
182
195
  streamTimeoutMs: opts.streamTimeoutMs ?? 1_800_000,
183
196
  baseURL,
184
197
  useNativeWebSearch: opts.useNativeWebSearch ?? false,
198
+ codexSubscription,
185
199
  });
186
200
  if (!adapter) return null;
187
201
 
@@ -9,13 +9,13 @@ import { PROVIDER_CATALOG } from "../model-catalog.js";
9
9
  /**
10
10
  * Auth configuration stored in the `provider_connections` table.
11
11
  *
12
- * v1 runtime-supported variants:
12
+ * Runtime-supported variants:
13
13
  * - api_key: look up `credential` in vault, inject as bearer/provider header.
14
14
  * - platform: route via Vellum managed proxy; no client-side credential.
15
15
  * - none: no auth (e.g. Ollama running locally).
16
+ * - oauth_subscription: OAuth-based subscription auth (e.g. ChatGPT Codex).
16
17
  *
17
- * v2 schema-accepted variants (runtime rejects with a clear "not yet shipped" error):
18
- * - oauth_subscription: OAuth-based subscription auth.
18
+ * Schema-accepted variants (runtime rejects with a clear "not yet shipped" error):
19
19
  * - service_account: service-account credentials (Vertex AI, Bedrock).
20
20
  */
21
21
  export const AuthSchema = z.discriminatedUnion("type", [
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Automatic token refresh for ChatGPT Codex OAuth (subscription auth).
3
+ *
4
+ * OpenAI rotates refresh tokens on every use — concurrent refreshes will
5
+ * invalidate one token. A module-level mutex prevents this race.
6
+ */
7
+
8
+ import { refreshOAuth2Token } from "../../security/oauth2.js";
9
+ import {
10
+ getSecureKeyAsync,
11
+ setSecureKeyAsync,
12
+ } from "../../security/secure-keys.js";
13
+ import { getLogger } from "../../util/logger.js";
14
+
15
+ const log = getLogger("codex-token-refresh");
16
+
17
+ const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
18
+ const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
19
+
20
+ /** Refresh 5 minutes before expiry to avoid using a nearly-expired token. */
21
+ const REFRESH_MARGIN_SECONDS = 300;
22
+
23
+ /**
24
+ * Module-level mutex to prevent concurrent refresh races.
25
+ * OpenAI rotates refresh tokens on every use — two concurrent refreshes
26
+ * will invalidate one token.
27
+ */
28
+ let refreshInFlight: Promise<string | null> | null = null;
29
+
30
+ /**
31
+ * Return a valid Codex access token, refreshing transparently if expired.
32
+ *
33
+ * @param credentialPrefix - Credential key prefix, e.g. `"credential/chatgpt"`.
34
+ * The function reads `<prefix>/access_token`, `<prefix>/refresh_token`,
35
+ * and `<prefix>/expires_at` from the credential store.
36
+ * @returns The access token string, or `null` if no token is stored.
37
+ */
38
+ export async function getValidCodexAccessToken(
39
+ credentialPrefix: string,
40
+ ): Promise<string | null> {
41
+ const accessToken = await getSecureKeyAsync(
42
+ `${credentialPrefix}/access_token`,
43
+ );
44
+ if (!accessToken) return null;
45
+
46
+ const expiresAtStr = await getSecureKeyAsync(
47
+ `${credentialPrefix}/expires_at`,
48
+ );
49
+ if (!expiresAtStr) return accessToken; // no expiry info — use token as-is
50
+
51
+ const expiresAt = Number(expiresAtStr);
52
+ const now = Date.now() / 1000;
53
+
54
+ if (now < expiresAt - REFRESH_MARGIN_SECONDS) {
55
+ return accessToken; // token is still fresh
56
+ }
57
+
58
+ // Token is expired or about to expire — refresh it.
59
+ // Use mutex to prevent concurrent refresh races.
60
+ if (refreshInFlight) {
61
+ return await refreshInFlight;
62
+ }
63
+
64
+ refreshInFlight = doRefresh(credentialPrefix);
65
+ try {
66
+ return await refreshInFlight;
67
+ } finally {
68
+ refreshInFlight = null;
69
+ }
70
+ }
71
+
72
+ async function doRefresh(credentialPrefix: string): Promise<string | null> {
73
+ const refreshToken = await getSecureKeyAsync(
74
+ `${credentialPrefix}/refresh_token`,
75
+ );
76
+ if (!refreshToken) {
77
+ log.warn("No refresh token available for Codex OAuth");
78
+ // Return the existing access token — it might still work even if expired
79
+ return (
80
+ (await getSecureKeyAsync(`${credentialPrefix}/access_token`)) ?? null
81
+ );
82
+ }
83
+
84
+ try {
85
+ const result = await refreshOAuth2Token(
86
+ CODEX_TOKEN_URL,
87
+ CODEX_CLIENT_ID,
88
+ refreshToken,
89
+ );
90
+
91
+ // Store the new tokens
92
+ await setSecureKeyAsync(
93
+ `${credentialPrefix}/access_token`,
94
+ result.accessToken,
95
+ );
96
+ if (result.refreshToken) {
97
+ await setSecureKeyAsync(
98
+ `${credentialPrefix}/refresh_token`,
99
+ result.refreshToken,
100
+ );
101
+ }
102
+ if (result.expiresIn) {
103
+ const newExpiresAt = Math.floor(Date.now() / 1000 + result.expiresIn);
104
+ await setSecureKeyAsync(
105
+ `${credentialPrefix}/expires_at`,
106
+ String(newExpiresAt),
107
+ );
108
+ }
109
+
110
+ log.info("Codex OAuth token refreshed successfully");
111
+ return result.accessToken;
112
+ } catch (err) {
113
+ log.error({ err }, "Codex OAuth token refresh failed");
114
+ // Return the existing access token as fallback
115
+ return (
116
+ (await getSecureKeyAsync(`${credentialPrefix}/access_token`)) ?? null
117
+ );
118
+ }
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Test helpers
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /** @internal Test-only: reset the in-flight refresh mutex. */
126
+ export function _resetRefreshMutex(): void {
127
+ refreshInFlight = null;
128
+ }