@vellumai/assistant 0.12.1 → 0.12.2-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 (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -8,18 +8,29 @@
8
8
  * builds one against the manual redirect page and parses the `code#state`
9
9
  * string the user copies back. Both converge on `storeAcpClaudeToken`, which
10
10
  * writes the `acp/claude_oauth_token` vault field the ACP broker reads at
11
- * spawn time and provisions the `acp_spawn` read policy.
11
+ * spawn time, persists any refresh token and expiry the exchange returned,
12
+ * and provisions the `acp_spawn` read policy.
13
+ *
14
+ * Refresh, expiry, and the bound-access digest are stored without credential
15
+ * metadata so the broker cannot hand them to a spawned agent. The policy for
16
+ * when to spend the refresh token lives in `claude-token-refresh.ts`.
12
17
  */
13
18
 
19
+ import { computeExpiresAt, isTokenExpired } from "@vellumai/credential-storage";
20
+
14
21
  import { credentialKey } from "../security/credential-key.js";
15
22
  import type { OAuth2Config } from "../security/oauth2.js";
16
23
  import {
24
+ deleteSecureKeyAsync,
17
25
  getSecureKeyAsync,
18
26
  setSecureKeyAsync,
19
27
  } from "../security/secure-keys.js";
20
28
  import { getLogger } from "../util/logger.js";
21
29
  import { claudeTokenDigest } from "./acp-auth-marker-store.js";
22
30
  import {
31
+ ACP_OAUTH_ACCESS_DIGEST_FIELD,
32
+ ACP_OAUTH_EXPIRES_AT_FIELD,
33
+ ACP_OAUTH_REFRESH_TOKEN_FIELD,
23
34
  ACP_OAUTH_TOKEN_FIELD,
24
35
  ACP_SERVICE,
25
36
  classifyAnthropicToken,
@@ -32,6 +43,22 @@ import {
32
43
 
33
44
  const log = getLogger("acp:claude-oauth");
34
45
 
46
+ /**
47
+ * Serializes every Claude OAuth token-set write (Connect, renewal persist,
48
+ * companion clear) so a compare-and-write cannot interleave with another
49
+ * writer. The network refresh itself stays outside this queue.
50
+ */
51
+ let tokenWriteQueue: Promise<unknown> = Promise.resolve();
52
+
53
+ function withAcpClaudeTokenWrite<T>(fn: () => Promise<T>): Promise<T> {
54
+ const run = tokenWriteQueue.then(fn, fn);
55
+ tokenWriteQueue = run.then(
56
+ () => undefined,
57
+ () => undefined,
58
+ );
59
+ return run;
60
+ }
61
+
35
62
  /**
36
63
  * Verified Claude Code public OAuth client. PKCE-only (no client secret);
37
64
  * the single `user:inference` scope is what the ACP adapter's
@@ -137,20 +164,103 @@ export async function storedClaudeTokenDigest(): Promise<string | undefined> {
137
164
  return token ? claudeTokenDigest(token) : undefined;
138
165
  }
139
166
 
167
+ /** Tokens as returned by an authorization-code exchange or a refresh. */
168
+ export interface AcpClaudeTokens {
169
+ accessToken: string;
170
+ refreshToken?: string;
171
+ /** Lifetime in seconds, as the provider reports it. */
172
+ expiresIn?: number;
173
+ }
174
+
175
+ function vaultKey(field: string): string {
176
+ return credentialKey(ACP_SERVICE, field);
177
+ }
178
+
179
+ /**
180
+ * Write a companion field, or delete it when there is no value.
181
+ *
182
+ * Both outcomes are checked. The store signals failure by return value rather
183
+ * than by throwing (`setSecureKeyAsync` returns false, `deleteSecureKeyAsync`
184
+ * returns `"error"` on timeout), so ignoring them would let a backend hiccup
185
+ * report success while leaving the token-set fields out of sync: an access
186
+ * token with no way to renew it, or a new access token still paired with a
187
+ * previous connect's refresh token.
188
+ */
189
+ async function writeOrClear(
190
+ field: string,
191
+ value: string | undefined,
192
+ ): Promise<void> {
193
+ if (value) {
194
+ const stored = await setSecureKeyAsync(vaultKey(field), value);
195
+ if (!stored) {
196
+ throw new Error(
197
+ `Failed to store Claude OAuth ${field} in secure storage.`,
198
+ );
199
+ }
200
+ return;
201
+ }
202
+ const result = await deleteSecureKeyAsync(vaultKey(field));
203
+ if (result === "error") {
204
+ throw new Error(
205
+ `Failed to clear Claude OAuth ${field} from secure storage.`,
206
+ );
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Write a Claude OAuth token set to the `acp/claude_oauth_*` vault fields.
212
+ * Throws when the backing store rejects any of the writes.
213
+ *
214
+ * The refresh token and expiry are written as a set with the access token:
215
+ * when the provider returns neither, any previously stored values are cleared
216
+ * rather than left behind. A stale refresh token paired with a newly connected
217
+ * access token would otherwise renew into the credential from a previous
218
+ * connect.
219
+ *
220
+ * Only the access-token field gets credential metadata. `serverUse` refuses
221
+ * any field without metadata, so the refresh token and expiry stay
222
+ * unreachable through the broker and cannot be injected into a spawned
223
+ * agent's env.
224
+ */
225
+ async function writeConnectedTokenSet(tokens: AcpClaudeTokens): Promise<void> {
226
+ await withAcpClaudeTokenWrite(async () => {
227
+ const stored = await setSecureKeyAsync(
228
+ vaultKey(ACP_OAUTH_TOKEN_FIELD),
229
+ tokens.accessToken,
230
+ );
231
+ if (!stored) {
232
+ throw new Error("Failed to store Claude OAuth token in secure storage.");
233
+ }
234
+
235
+ await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, tokens.refreshToken);
236
+ const expiresAt = computeExpiresAt(tokens.expiresIn);
237
+ await writeOrClear(
238
+ ACP_OAUTH_EXPIRES_AT_FIELD,
239
+ expiresAt == null ? undefined : String(expiresAt),
240
+ );
241
+ const hasRenewal = Boolean(tokens.refreshToken) || expiresAt != null;
242
+ await writeOrClear(
243
+ ACP_OAUTH_ACCESS_DIGEST_FIELD,
244
+ hasRenewal ? claudeTokenDigest(tokens.accessToken) : undefined,
245
+ );
246
+ });
247
+ }
248
+
140
249
  /**
141
- * Store a captured Claude OAuth token in the `acp/claude_oauth_token` vault
142
- * field and provision the policy the broker applies at spawn time: grant the
250
+ * Store a captured Claude OAuth token set in the `acp/claude_oauth_*` vault
251
+ * fields and provision the policy the broker applies at spawn time: grant the
143
252
  * `acp_spawn` read and lift any domain restriction. Throws when the backing
144
253
  * store rejects the write.
254
+ *
255
+ * A string argument is treated as an access-token-only write and clears any
256
+ * companion refresh or expiry fields so they cannot describe a previous token.
145
257
  */
146
- export async function storeAcpClaudeToken(token: string): Promise<void> {
147
- const stored = await setSecureKeyAsync(
148
- credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD),
149
- token,
150
- );
151
- if (!stored) {
152
- throw new Error("Failed to store Claude OAuth token in secure storage.");
153
- }
258
+ export async function storeAcpClaudeToken(
259
+ tokens: string | AcpClaudeTokens,
260
+ ): Promise<void> {
261
+ const tokenSet: AcpClaudeTokens =
262
+ typeof tokens === "string" ? { accessToken: tokens } : tokens;
263
+ await writeConnectedTokenSet(tokenSet);
154
264
  // Repair rather than merely ensure the policy: an explicit Connect is a
155
265
  // deliberate opt-in to ACP, so this widens a credential the broker would
156
266
  // otherwise keep denying the spawn read on, which would dead-loop the Connect
@@ -241,6 +351,11 @@ export async function notifyAcpConnectRetired(): Promise<void> {
241
351
  * spawn-time broker read applies, so "connected" means precisely "the spawn
242
352
  * would get this token". The token-shape guard stays here instead: the broker
243
353
  * knows nothing about Anthropic token formats.
354
+ *
355
+ * An expired token with no refresh token is the same shape of problem, and is
356
+ * likewise NOT connected: the card has to stay up so the user can reconnect.
357
+ * An expired token that still has a refresh token IS connected, because
358
+ * `ensureFreshAcpClaudeToken` renews it on the next spawn.
244
359
  */
245
360
  export async function hasAcpClaudeToken(): Promise<boolean> {
246
361
  return (await usableStoredClaudeToken()) !== undefined;
@@ -257,9 +372,7 @@ export async function hasAcpClaudeToken(): Promise<boolean> {
257
372
  * card and no working token.
258
373
  */
259
374
  async function usableStoredClaudeToken(): Promise<string | undefined> {
260
- const token = await getSecureKeyAsync(
261
- credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD),
262
- );
375
+ const token = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
263
376
  if (token == null || token.length === 0) {
264
377
  return undefined;
265
378
  }
@@ -274,6 +387,15 @@ async function usableStoredClaudeToken(): Promise<string | undefined> {
274
387
  );
275
388
  return undefined;
276
389
  }
390
+ if (await isAcpClaudeTokenExpiring()) {
391
+ const boundDigest = await readBoundAccessDigest();
392
+ if (boundDigest && boundDigest !== claudeTokenDigest(token)) {
393
+ return token;
394
+ }
395
+ if (!(await hasAcpClaudeRefreshToken())) {
396
+ return undefined;
397
+ }
398
+ }
277
399
  return token;
278
400
  }
279
401
 
@@ -326,3 +448,195 @@ export async function acpConnectCardStillWarranted(
326
448
  await import("./acp-auth-marker-store.js");
327
449
  return claudeCredentialRefused(resolved);
328
450
  }
451
+
452
+ /**
453
+ * Persist a token set obtained by renewing an existing credential, leaving
454
+ * the read policy exactly as it was.
455
+ *
456
+ * A renewal happens on a passive spawn with no user in the loop, so it must
457
+ * not widen what the credential is allowed to do. Granting `acp_spawn` here
458
+ * would let a background refresh restore a permission a user or admin had
459
+ * removed from `allowedTools`.
460
+ *
461
+ * A rotated refresh token is written when the provider returns one. When the
462
+ * response omits a refresh token, the stored refresh token is kept so a
463
+ * non-rotating grant does not lose its renewal material. A missing or
464
+ * non-positive `expiresIn` clears the recorded expiry so a stale past expiry
465
+ * cannot condemn the new access token.
466
+ *
467
+ * `expectedRefreshToken` is the refresh token this request spent. If the
468
+ * vault no longer holds that value, the access-token field is gone, or the
469
+ * stored access token is not the one this refresh material was written
470
+ * with, the persist is skipped so a newer token set is not overwritten and
471
+ * a deleted or pasted credential is not replaced. Returns whether the write
472
+ * happened.
473
+ */
474
+ export async function persistRefreshedAcpClaudeTokens(
475
+ tokens: AcpClaudeTokens,
476
+ expectedRefreshToken: string,
477
+ ): Promise<boolean> {
478
+ if (!tokens.accessToken) {
479
+ throw new Error("Refreshed Claude OAuth response had no access token.");
480
+ }
481
+ return withAcpClaudeTokenWrite(async () => {
482
+ const current = await readAcpClaudeRefreshToken();
483
+ if (current !== expectedRefreshToken) {
484
+ log.info(
485
+ "Skipping Claude OAuth refresh persist because the stored refresh token changed while the request was in flight",
486
+ );
487
+ return false;
488
+ }
489
+ const access = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
490
+ if (access == null || access.length === 0) {
491
+ log.info(
492
+ "Skipping Claude OAuth refresh persist because the access token is no longer stored",
493
+ );
494
+ return false;
495
+ }
496
+ if (!(await renewalIsBoundTo(access))) {
497
+ log.info(
498
+ "Skipping Claude OAuth refresh persist because the stored access token is not the one this refresh material was written with",
499
+ );
500
+ return false;
501
+ }
502
+ const stored = await setSecureKeyAsync(
503
+ vaultKey(ACP_OAUTH_TOKEN_FIELD),
504
+ tokens.accessToken,
505
+ );
506
+ if (!stored) {
507
+ throw new Error("Failed to store Claude OAuth token in secure storage.");
508
+ }
509
+ if (tokens.refreshToken) {
510
+ await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, tokens.refreshToken);
511
+ }
512
+ const expiresAt = computeExpiresAt(tokens.expiresIn);
513
+ await writeOrClear(
514
+ ACP_OAUTH_EXPIRES_AT_FIELD,
515
+ expiresAt == null ? undefined : String(expiresAt),
516
+ );
517
+ await writeOrClear(
518
+ ACP_OAUTH_ACCESS_DIGEST_FIELD,
519
+ claudeTokenDigest(tokens.accessToken),
520
+ );
521
+ return true;
522
+ });
523
+ }
524
+
525
+ /**
526
+ * The stored absolute expiry in epoch milliseconds, or null when unknown.
527
+ *
528
+ * Unknown is the norm for tokens connected before expiry was recorded, and is
529
+ * treated as "assume usable": we cannot tell fresh from expired, and guessing
530
+ * expired would refresh or discard a perfectly good token.
531
+ */
532
+ async function readExpiresAt(): Promise<number | null> {
533
+ const raw = await getSecureKeyAsync(vaultKey(ACP_OAUTH_EXPIRES_AT_FIELD));
534
+ if (!raw) {
535
+ return null;
536
+ }
537
+ const parsed = Number(raw);
538
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
539
+ }
540
+
541
+ /**
542
+ * Whether the stored access token is past its recorded expiry, or close
543
+ * enough that it should be renewed before use. False when no expiry was
544
+ * recorded.
545
+ */
546
+ export async function isAcpClaudeTokenExpiring(): Promise<boolean> {
547
+ return isTokenExpired(await readExpiresAt());
548
+ }
549
+
550
+ /** The stored refresh token, or null when none was captured. */
551
+ export async function readAcpClaudeRefreshToken(): Promise<string | null> {
552
+ const token = await getSecureKeyAsync(vaultKey(ACP_OAUTH_REFRESH_TOKEN_FIELD));
553
+ return token != null && token.length > 0 ? token : null;
554
+ }
555
+
556
+ /** Whether renewal material is on hand, without revealing it. */
557
+ export async function hasAcpClaudeRefreshToken(): Promise<boolean> {
558
+ return (await readAcpClaudeRefreshToken()) != null;
559
+ }
560
+
561
+ /**
562
+ * Whether the access-token vault field is present. Presence only: not
563
+ * whether the value is usable, expired, or readable by the spawn broker.
564
+ *
565
+ * Renewal spends a refresh token to replace this field. If the field is
566
+ * gone, there is no credential to renew.
567
+ */
568
+ export async function hasStoredAcpClaudeAccessToken(): Promise<boolean> {
569
+ const token = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
570
+ return token != null && token.length > 0;
571
+ }
572
+
573
+ async function readBoundAccessDigest(): Promise<string | null> {
574
+ const digest = await getSecureKeyAsync(
575
+ vaultKey(ACP_OAUTH_ACCESS_DIGEST_FIELD),
576
+ );
577
+ return digest != null && digest.length > 0 ? digest : null;
578
+ }
579
+
580
+ async function renewalIsBoundTo(accessToken: string): Promise<boolean> {
581
+ const digest = await readBoundAccessDigest();
582
+ return digest != null && digest === claudeTokenDigest(accessToken);
583
+ }
584
+
585
+ async function clearRenewalFields(): Promise<void> {
586
+ await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, undefined);
587
+ await writeOrClear(ACP_OAUTH_EXPIRES_AT_FIELD, undefined);
588
+ await writeOrClear(ACP_OAUTH_ACCESS_DIGEST_FIELD, undefined);
589
+ }
590
+
591
+ /**
592
+ * Drop the refresh token, keeping the recorded expiry and the access token.
593
+ * Called when the provider rejects the refresh token.
594
+ *
595
+ * The expiry has to survive. `hasAcpClaudeToken()` reads "not connected" from
596
+ * the combination of an expired token and no refresh token. Clearing the
597
+ * expiry as well would make `readExpiresAt()` return null, which
598
+ * {@link isAcpClaudeTokenExpiring} treats as "assume usable", and the dead
599
+ * credential would report itself connected.
600
+ */
601
+ export async function clearAcpClaudeRefreshToken(
602
+ expectedRefreshToken?: string,
603
+ ): Promise<void> {
604
+ await withAcpClaudeTokenWrite(async () => {
605
+ if (expectedRefreshToken !== undefined) {
606
+ const current = await readAcpClaudeRefreshToken();
607
+ if (current !== expectedRefreshToken) {
608
+ log.info(
609
+ "Skipping Claude OAuth refresh-token clear because the stored refresh token changed while the request was in flight",
610
+ );
611
+ return;
612
+ }
613
+ }
614
+ await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, undefined);
615
+ });
616
+ }
617
+
618
+ /**
619
+ * Drop refresh, expiry, and the bound-access digest when they do not describe
620
+ * the access token currently stored.
621
+ *
622
+ * Connect and persist write those fields as a set with the access token. A
623
+ * later paste or CLI write replaces only the access-token field. Spending the
624
+ * leftover refresh token would overwrite that replacement.
625
+ */
626
+ export async function forgetAcpClaudeRenewalStateIfUnbound(): Promise<void> {
627
+ await withAcpClaudeTokenWrite(async () => {
628
+ const access = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
629
+ const refresh = await readAcpClaudeRefreshToken();
630
+ const expiresAt = await readExpiresAt();
631
+ const digest = await readBoundAccessDigest();
632
+ const hasRenewal = refresh != null || expiresAt != null || digest != null;
633
+ if (!hasRenewal) {
634
+ return;
635
+ }
636
+ if (access && digest && digest === claudeTokenDigest(access)) {
637
+ return;
638
+ }
639
+ await clearRenewalFields();
640
+ });
641
+ }
642
+
@@ -14,6 +14,25 @@
14
14
  export const ACP_SERVICE = "acp";
15
15
  export const ACP_OAUTH_TOKEN_FIELD = "claude_oauth_token";
16
16
 
17
+ /**
18
+ * Refresh and expiry for {@link ACP_OAUTH_TOKEN_FIELD}, written by the Connect
19
+ * Claude exchange that stored that access token. Expiry is epoch milliseconds,
20
+ * matching `computeExpiresAt` and `isTokenExpired` in
21
+ * `@vellumai/credential-storage`.
22
+ *
23
+ * {@link ACP_OAUTH_ACCESS_DIGEST_FIELD} is a digest of the access token those
24
+ * fields were written with, so a later paste of a different access token cannot
25
+ * spend leftover refresh material.
26
+ *
27
+ * These fields are given no credential metadata. `credentialBroker.serverUse`
28
+ * refuses any field without metadata, so they stay unreachable through the
29
+ * broker and cannot be injected into a spawned agent's env. Only the access
30
+ * token crosses into the child process.
31
+ */
32
+ export const ACP_OAUTH_REFRESH_TOKEN_FIELD = "claude_oauth_refresh_token";
33
+ export const ACP_OAUTH_EXPIRES_AT_FIELD = "claude_oauth_expires_at";
34
+ export const ACP_OAUTH_ACCESS_DIGEST_FIELD = "claude_oauth_access_digest";
35
+
17
36
  /**
18
37
  * True for the ACP vault field the "Connect Claude" flow owns
19
38
  * (`acp/claude_oauth_token`). Used to route this credential to the inline
@@ -194,7 +194,10 @@ describe("AcpAgentProcess.createSession auth_required handling", () => {
194
194
  },
195
195
  });
196
196
 
197
- await expect(proc.createSession("/tmp")).resolves.toBe("session-1");
197
+ await expect(proc.createSession("/tmp")).resolves.toEqual({
198
+ sessionId: "session-1",
199
+ configOptions: [],
200
+ });
198
201
  expect(authenticateCalls()).toBe(1);
199
202
  });
200
203
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { type ChildProcess, spawn } from "node:child_process";
9
+ import { basename } from "node:path";
9
10
  import { Readable, Writable } from "node:stream";
10
11
 
11
12
  import type {
@@ -14,14 +15,25 @@ import type {
14
15
  AuthMethodEnvVar,
15
16
  Client,
16
17
  InitializeResponse,
18
+ LoadSessionResponse,
17
19
  NewSessionResponse,
18
20
  PromptResponse,
21
+ ResumeSessionResponse,
22
+ SessionConfigOption,
23
+ SetSessionConfigOptionRequest,
19
24
  } from "@agentclientprotocol/sdk";
20
25
  import * as acp from "@agentclientprotocol/sdk";
21
26
 
22
27
  import { getLogger } from "../util/logger.js";
23
- import { AcpAuthRequiredError, isAcpAuthRequired } from "./auth-required.js";
24
- import type { AcpAgentConfig } from "./types.js";
28
+ import { safeStringSlice } from "../util/unicode.js";
29
+ import {
30
+ AcpAuthRequiredError,
31
+ CLAUDE_ACP_COMMAND,
32
+ isAcpAuthRequired,
33
+ isClaudeAuthFailureMessage,
34
+ requestErrorReason,
35
+ } from "./auth-required.js";
36
+ import { type AcpAgentConfig, AcpConfigOptionRefusedError } from "./types.js";
25
37
 
26
38
  const log = getLogger("acp");
27
39
 
@@ -32,6 +44,16 @@ const log = getLogger("acp");
32
44
  */
33
45
  const STDERR_RETENTION_BYTES = 4096;
34
46
 
47
+ /**
48
+ * Normalizes the SDK's optional-and-nullable config-option field to an array,
49
+ * so every session call hands its caller the same shape.
50
+ */
51
+ function normalizeConfigOptions(
52
+ configOptions: SessionConfigOption[] | null | undefined,
53
+ ): SessionConfigOption[] {
54
+ return configOptions ?? [];
55
+ }
56
+
35
57
  function isEnvVarMethod(
36
58
  method: AuthMethod,
37
59
  ): method is AuthMethodEnvVar & { type: "env_var" } {
@@ -142,7 +164,7 @@ export class AcpAgentProcess {
142
164
  // deriveFailureError reads from the end, so dropping the head is lossless.
143
165
  const line =
144
166
  text.length > STDERR_RETENTION_BYTES
145
- ? text.slice(-STDERR_RETENTION_BYTES)
167
+ ? safeStringSlice(text, text.length - STDERR_RETENTION_BYTES)
146
168
  : text;
147
169
  this.stderrSeq += 1;
148
170
  this.stderrRing.push({ seq: this.stderrSeq, text: line });
@@ -355,16 +377,21 @@ export class AcpAgentProcess {
355
377
 
356
378
  /**
357
379
  * Creates a new ACP session in the specified working directory.
358
- * Returns the session ID.
380
+ * Returns the session ID and the config options the agent reported.
359
381
  */
360
- async createSession(cwd: string): Promise<string> {
382
+ async createSession(
383
+ cwd: string,
384
+ ): Promise<{ sessionId: string; configOptions: SessionConfigOption[] }> {
361
385
  log.info({ agentId: this.agentId, cwd }, "Creating ACP session");
362
386
 
363
387
  const result: NewSessionResponse = await this.withAuthRetry(() =>
364
388
  this.requireConnection().newSession({ cwd, mcpServers: [] }),
365
389
  );
366
390
 
367
- return result.sessionId;
391
+ return {
392
+ sessionId: result.sessionId,
393
+ configOptions: normalizeConfigOptions(result.configOptions),
394
+ };
368
395
  }
369
396
 
370
397
  /**
@@ -375,12 +402,17 @@ export class AcpAgentProcess {
375
402
  * callers should suppress forwarding of those replayed updates (see
376
403
  * VellumAcpClientHandler.beginReplaySuppression).
377
404
  */
378
- async loadSession(sessionId: string, cwd: string): Promise<void> {
405
+ async loadSession(
406
+ sessionId: string,
407
+ cwd: string,
408
+ ): Promise<{ configOptions: SessionConfigOption[] }> {
379
409
  log.info({ agentId: this.agentId, sessionId, cwd }, "Loading ACP session");
380
410
 
381
- await this.withAuthRetry(() =>
411
+ const result: LoadSessionResponse = await this.withAuthRetry(() =>
382
412
  this.requireConnection().loadSession({ sessionId, cwd, mcpServers: [] }),
383
413
  );
414
+
415
+ return { configOptions: normalizeConfigOptions(result.configOptions) };
384
416
  }
385
417
 
386
418
  /**
@@ -390,16 +422,70 @@ export class AcpAgentProcess {
390
422
  * preferred when the agent advertises the capability
391
423
  * (see supportsSessionResume).
392
424
  */
393
- async resumeSession(sessionId: string, cwd: string): Promise<void> {
425
+ async resumeSession(
426
+ sessionId: string,
427
+ cwd: string,
428
+ ): Promise<{ configOptions: SessionConfigOption[] }> {
394
429
  log.info({ agentId: this.agentId, sessionId, cwd }, "Resuming ACP session");
395
430
 
396
- await this.withAuthRetry(() =>
431
+ const result: ResumeSessionResponse = await this.withAuthRetry(() =>
397
432
  this.requireConnection().resumeSession({
398
433
  sessionId,
399
434
  cwd,
400
435
  mcpServers: [],
401
436
  }),
402
437
  );
438
+
439
+ return { configOptions: normalizeConfigOptions(result.configOptions) };
440
+ }
441
+
442
+ /**
443
+ * Sets one session config option (e.g. the model selector) via
444
+ * `session/set_config_option`. The agent answers with the full refreshed
445
+ * option set, which is returned to the caller.
446
+ *
447
+ * A boolean value sends the `type: "boolean"` request variant; a string
448
+ * sends the value-id variant.
449
+ */
450
+ async setConfigOption(
451
+ sessionId: string,
452
+ configId: string,
453
+ value: string | boolean,
454
+ ): Promise<SessionConfigOption[]> {
455
+ log.info(
456
+ { agentId: this.agentId, sessionId, configId, value },
457
+ "Setting ACP session config option",
458
+ );
459
+
460
+ const request: SetSessionConfigOptionRequest =
461
+ typeof value === "boolean"
462
+ ? { sessionId, configId, type: "boolean", value }
463
+ : { sessionId, configId, value };
464
+
465
+ const response = await this.withAuthRetry(async () => {
466
+ try {
467
+ return await this.requireConnection().setSessionConfigOption(request);
468
+ } catch (err) {
469
+ // Wrapped here, at the request itself, so `withAuthRetry` still sees
470
+ // an auth_required answer and a caller cannot confuse the adapter's
471
+ // refusal with a transport or authentication failure. Claude's
472
+ // expired-token failure travels as a generic error whose reason is
473
+ // the only signal, so under the Claude adapter it is left for the
474
+ // caller to classify.
475
+ if (err instanceof acp.RequestError && !isAcpAuthRequired(err)) {
476
+ const reason = requestErrorReason(err);
477
+ const claudeAuthFailure =
478
+ basename(this.config.command) === CLAUDE_ACP_COMMAND &&
479
+ isClaudeAuthFailureMessage(reason);
480
+ if (!claudeAuthFailure) {
481
+ throw new AcpConfigOptionRefusedError(reason);
482
+ }
483
+ }
484
+ throw err;
485
+ }
486
+ });
487
+
488
+ return normalizeConfigOptions(response.configOptions);
403
489
  }
404
490
 
405
491
  /**
@@ -8,7 +8,8 @@
8
8
  * 2. Credentials present but rejected (the expired/revoked case): the CLI
9
9
  * surfaces the API 401 as an error message it authors, and the adapter
10
10
  * relays it as a generic -32603 internal error, NOT as `auth_required`.
11
- * {@link isClaudeAuthFailureMessage}.
11
+ * {@link isClaudeAuthFailureMessage}, over the text
12
+ * {@link requestErrorReason} decodes out of the rejection.
12
13
  *
13
14
  * A positive detection becomes {@link ACP_CLAUDE_AUTH_REQUIRED_CODE} on the
14
15
  * `acp_auth_required` event, which the client turns into the inline
@@ -81,6 +82,54 @@ export function isClaudeAuthFailureMessage(
81
82
  );
82
83
  }
83
84
 
85
+ /** Messages the SDK's `RequestError` factories build when given no text. */
86
+ const GENERIC_RPC_MESSAGES: ReadonlySet<string> = new Set([
87
+ "Parse error",
88
+ "Invalid request",
89
+ "Invalid params",
90
+ "Internal error",
91
+ "Request cancelled",
92
+ "Authentication required",
93
+ "Resource not found",
94
+ ]);
95
+
96
+ /**
97
+ * The sentence behind a JSON-RPC rejection. An adapter that throws a plain
98
+ * Error reaches the client as a bare "Internal error" whose real text the
99
+ * agent-side SDK moved into `data.details`. An adapter that raises
100
+ * `RequestError.internalError(data, text)` keeps its words in the message and
101
+ * only supplemental context in `data`, so a specific message wins. Behind a
102
+ * generic message such as "Invalid params", `data.details` or the serialized
103
+ * payload supplies the reason instead. Duck-typed like
104
+ * {@link isAcpAuthRequired}:
105
+ * `message` is read as a property, so a rejection that arrives as a plain
106
+ * object off the wire decodes the same as an `Error` instance.
107
+ */
108
+ export function requestErrorReason(err: unknown): string {
109
+ if (typeof err !== "object" || err === null) {
110
+ return String(err);
111
+ }
112
+ const { data, message } = err as { data?: unknown; message?: unknown };
113
+ const own = typeof message === "string" ? message : String(err);
114
+ if (data == null) {
115
+ return own;
116
+ }
117
+ if (
118
+ typeof message === "string" &&
119
+ message.length > 0 &&
120
+ !GENERIC_RPC_MESSAGES.has(message)
121
+ ) {
122
+ return message;
123
+ }
124
+ const details = (data as { details?: unknown }).details;
125
+ if (typeof details === "string" && details.length > 0) {
126
+ return details;
127
+ }
128
+ // JSON.stringify answers undefined for a value it cannot represent.
129
+ const serialized: string | undefined = JSON.stringify(data);
130
+ return serialized ?? own;
131
+ }
132
+
84
133
  /** The adapter whose auth failures the Connect Claude flow can repair. */
85
134
  export const CLAUDE_ACP_COMMAND = "claude-agent-acp";
86
135