@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -7,7 +7,7 @@ import {
7
7
  interface IntegrationProbe {
8
8
  name: string;
9
9
  category: string;
10
- isConnected: () => boolean;
10
+ isConnected: () => Promise<boolean>;
11
11
  }
12
12
 
13
13
  // Registry — add new integrations here:
@@ -15,7 +15,7 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
15
15
  {
16
16
  name: "Gmail",
17
17
  category: "email",
18
- isConnected: () => isProviderConnected("integration:gmail"),
18
+ isConnected: () => isProviderConnected("integration:google"),
19
19
  },
20
20
  {
21
21
  name: "Slack",
@@ -25,39 +25,46 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
25
25
  {
26
26
  name: "Twilio",
27
27
  category: "telephony",
28
- isConnected: () => hasTwilioCredentials(),
28
+ isConnected: async () => hasTwilioCredentials(),
29
29
  },
30
30
  {
31
31
  name: "Telegram",
32
32
  category: "messaging",
33
- isConnected: () => {
33
+ isConnected: async () => {
34
34
  const conn = getConnectionByProvider("telegram");
35
35
  return !!(conn && conn.status === "active");
36
36
  },
37
37
  },
38
38
  ];
39
39
 
40
- export function getIntegrationSummary(): Array<{
41
- name: string;
42
- category: string;
43
- connected: boolean;
44
- }> {
45
- return INTEGRATION_PROBES.map((probe) => ({
46
- name: probe.name,
47
- category: probe.category,
48
- connected: probe.isConnected(),
49
- }));
40
+ export async function getIntegrationSummary(): Promise<
41
+ Array<{
42
+ name: string;
43
+ category: string;
44
+ connected: boolean;
45
+ }>
46
+ > {
47
+ return Promise.all(
48
+ INTEGRATION_PROBES.map(async (probe) => ({
49
+ name: probe.name,
50
+ category: probe.category,
51
+ connected: await probe.isConnected(),
52
+ })),
53
+ );
50
54
  }
51
55
 
52
- export function formatIntegrationSummary(): string {
53
- const summary = getIntegrationSummary();
56
+ export async function formatIntegrationSummary(): Promise<string> {
57
+ const summary = await getIntegrationSummary();
54
58
  return summary
55
59
  .map((s) => `${s.name} ${s.connected ? "\u2713" : "\u2717"}`)
56
60
  .join(" | ");
57
61
  }
58
62
 
59
- export function hasCapability(category: string): boolean {
60
- return INTEGRATION_PROBES.some(
61
- (probe) => probe.category === category && probe.isConnected(),
63
+ export async function hasCapability(category: string): Promise<boolean> {
64
+ const results = await Promise.all(
65
+ INTEGRATION_PROBES.filter((probe) => probe.category === category).map(
66
+ (probe) => probe.isConnected(),
67
+ ),
62
68
  );
69
+ return results.some(Boolean);
63
70
  }
@@ -33,11 +33,18 @@ const REQUEST_TIMEOUT_MS = 5_000;
33
33
  * back); `{ found: false }` means the key doesn't exist in the keychain. */
34
34
  export type BrokerGetResult = { found: boolean; value?: string } | null;
35
35
 
36
+ /** Result of a `set()` call — distinguishes broker-unreachable from an active
37
+ * rejection so callers can log meaningful diagnostics. */
38
+ export type BrokerSetResult =
39
+ | { status: "ok" }
40
+ | { status: "unreachable" }
41
+ | { status: "rejected"; code: string; message: string };
42
+
36
43
  export interface KeychainBrokerClient {
37
44
  isAvailable(): boolean;
38
45
  ping(): Promise<{ pong: boolean } | null>;
39
46
  get(account: string): Promise<BrokerGetResult>;
40
- set(account: string, value: string): Promise<boolean>;
47
+ set(account: string, value: string): Promise<BrokerSetResult>;
41
48
  del(account: string): Promise<boolean>;
42
49
  list(): Promise<string[]>;
43
50
  }
@@ -360,12 +367,18 @@ export function createBrokerClient(): KeychainBrokerClient {
360
367
  }
361
368
  },
362
369
 
363
- async set(account: string, value: string): Promise<boolean> {
370
+ async set(account: string, value: string): Promise<BrokerSetResult> {
364
371
  try {
365
372
  const response = await doRequest("key.set", { account, value });
366
- return response?.ok === true;
373
+ if (!response) return { status: "unreachable" };
374
+ if (response.ok) return { status: "ok" };
375
+ return {
376
+ status: "rejected",
377
+ code: response.error?.code ?? "UNKNOWN",
378
+ message: response.error?.message ?? "unknown error",
379
+ };
367
380
  } catch {
368
- return false;
381
+ return { status: "unreachable" };
369
382
  }
370
383
  },
371
384
 
@@ -420,18 +420,17 @@ export interface OAuth2PreparedFlow {
420
420
  * URL directly in chat and the callback arrives asynchronously via the gateway.
421
421
  *
422
422
  * Supports two transports:
423
- * - **gateway** (default): routes callbacks through the public ingress URL.
424
- * Requires `ingress.publicBaseUrl` to be configured.
425
- * - **loopback**: starts a temporary localhost server to receive the callback.
426
- * Used for services like Slack that require pre-registered localhost redirect
427
- * URIs. The daemon is always local, so this works even in non-interactive
428
- * (channel) sessions.
423
+ * - **loopback** (default): starts a temporary localhost server to receive the
424
+ * callback. Works without any public URL or tunnel.
425
+ * - **gateway**: routes callbacks through the public ingress URL.
426
+ * Requires `ingress.publicBaseUrl` to be configured. Used for providers that
427
+ * don't support localhost redirects (e.g. Twitter, Notion).
429
428
  */
430
429
  export async function prepareOAuth2Flow(
431
430
  config: OAuth2Config,
432
431
  options?: OAuth2FlowOptions,
433
432
  ): Promise<OAuth2PreparedFlow> {
434
- const transport = options?.callbackTransport ?? "gateway";
433
+ const transport = options?.callbackTransport ?? "loopback";
435
434
 
436
435
  if (transport === "loopback") {
437
436
  return prepareLoopbackFlow(config, options?.loopbackPort);
@@ -3,14 +3,17 @@
3
3
  * available (macOS app embedded), with transparent fallback to the
4
4
  * encrypted-at-rest file store.
5
5
  *
6
- * Async variants try the broker first; sync variants always use the
6
+ * Async variants try the encrypted store first; sync variants always use the
7
7
  * encrypted store (startup code paths cannot do async I/O).
8
8
  */
9
9
 
10
+ import { getLogger } from "../util/logger.js";
10
11
  import * as encryptedStore from "./encrypted-store.js";
11
12
  import type { KeychainBrokerClient } from "./keychain-broker-client.js";
12
13
  import { createBrokerClient } from "./keychain-broker-client.js";
13
14
 
15
+ const log = getLogger("secure-keys");
16
+
14
17
  let _broker: KeychainBrokerClient | undefined;
15
18
 
16
19
  function getBroker(): KeychainBrokerClient {
@@ -79,30 +82,33 @@ export function isDowngradedFromKeychain(): boolean {
79
82
  }
80
83
 
81
84
  // ---------------------------------------------------------------------------
82
- // Async variants — try broker first, fall back to encrypted store
85
+ // Async variants — try encrypted store first, fall back to broker
83
86
  // ---------------------------------------------------------------------------
84
87
 
85
88
  /**
86
- * Async version of `getSecureKey`. When the broker is available it is
87
- * queried first. A `null` return from the broker means error (fall back
88
- * to encrypted store). A `{ found: false }` also falls back to the
89
- * encrypted store keys may exist only in `keys.enc` (e.g. written
90
- * while the broker was unavailable or via sync `setSecureKey`).
89
+ * Async version of `getSecureKey`. Checks the encrypted store first
90
+ * (instant) since `setSecureKeyAsync` always writes to both stores.
91
+ * Falls back to the broker for keys that may exist only in the macOS
92
+ * Keychain. Returns `undefined` if the key is not found in either store.
91
93
  */
92
94
  export async function getSecureKeyAsync(
93
95
  account: string,
94
96
  ): Promise<string | undefined> {
97
+ // Check encrypted store first (sync, instant). Since setSecureKeyAsync
98
+ // always writes to both broker and encrypted store, a hit here is
99
+ // authoritative and avoids the broker IPC round-trip.
100
+ const encResult = encryptedStore.getKey(account);
101
+ if (encResult != null && encResult.length > 0) return encResult;
102
+
103
+ // Not in encrypted store — try broker as fallback for keys that may
104
+ // exist only in the macOS Keychain (e.g. written by the app directly).
95
105
  const broker = getBroker();
96
106
  if (broker.isAvailable()) {
97
107
  const result = await broker.get(account);
98
- // null = broker error, fall back to encrypted store
99
- if (result == null) return encryptedStore.getKey(account);
100
- // Broker found the key — use it
101
- if (result.found) return result.value;
102
- // Broker says not found — check encrypted store as fallback
103
- return encryptedStore.getKey(account);
108
+ if (result?.found) return result.value;
104
109
  }
105
- return encryptedStore.getKey(account);
110
+
111
+ return undefined;
106
112
  }
107
113
 
108
114
  /**
@@ -112,7 +118,7 @@ export async function getSecureKeyAsync(
112
118
  *
113
119
  * If the broker is available but `broker.set()` fails we return `false`
114
120
  * immediately — falling through to an encrypted-store-only write would
115
- * leave the broker with stale data that async readers would still see.
121
+ * leave the two stores out of sync.
116
122
  */
117
123
  export async function setSecureKeyAsync(
118
124
  account: string,
@@ -120,13 +126,32 @@ export async function setSecureKeyAsync(
120
126
  ): Promise<boolean> {
121
127
  const broker = getBroker();
122
128
  if (broker.isAvailable()) {
123
- const brokerOk = await broker.set(account, value);
124
- if (!brokerOk) return false;
129
+ const result = await broker.set(account, value);
130
+ if (result.status !== "ok") {
131
+ log.warn(
132
+ {
133
+ account,
134
+ brokerStatus: result.status,
135
+ ...(result.status === "rejected"
136
+ ? { brokerCode: result.code, brokerMessage: result.message }
137
+ : {}),
138
+ },
139
+ "Broker set failed for secure key",
140
+ );
141
+ return false;
142
+ }
125
143
  // Broker succeeded — also persist to encrypted store for sync callers.
126
144
  const encOk = encryptedStore.setKey(account, value);
145
+ if (!encOk) {
146
+ log.warn({ account }, "Encrypted store set failed after broker success");
147
+ }
127
148
  return encOk;
128
149
  }
129
- return encryptedStore.setKey(account, value);
150
+ const encOk = encryptedStore.setKey(account, value);
151
+ if (!encOk) {
152
+ log.warn({ account }, "Encrypted store set failed (broker unavailable)");
153
+ }
154
+ return encOk;
130
155
  }
131
156
 
132
157
  /**
@@ -139,7 +164,7 @@ export async function setSecureKeyAsync(
139
164
  *
140
165
  * If the broker is available but `broker.del()` fails we return `"error"`
141
166
  * immediately — falling through to an encrypted-store-only delete would
142
- * leave the broker with the key, and async readers would still see it.
167
+ * leave the broker with the key, causing stale reads on broker fallback.
143
168
  */
144
169
  export async function deleteSecureKeyAsync(
145
170
  account: string,
@@ -9,17 +9,22 @@
9
9
 
10
10
  import {
11
11
  getApp,
12
+ getConnection,
12
13
  getConnectionByProvider,
13
14
  getProvider,
14
15
  updateConnection,
15
16
  } from "../oauth/oauth-store.js";
16
17
  import { getLogger } from "../util/logger.js";
17
18
  import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
18
- import { getSecureKey, setSecureKeyAsync } from "./secure-keys.js";
19
+ import {
20
+ getSecureKey,
21
+ getSecureKeyAsync,
22
+ setSecureKeyAsync,
23
+ } from "./secure-keys.js";
19
24
 
20
25
  const log = getLogger("token-manager");
21
26
 
22
- const MESSAGING_SERVICES = new Set(["integration:gmail", "integration:slack"]);
27
+ const MESSAGING_SERVICES = new Set(["integration:google", "integration:slack"]);
23
28
 
24
29
  function recoveryHint(service: string): string {
25
30
  const shortName = service.startsWith("integration:")
@@ -116,14 +121,14 @@ function recordRefreshFailure(service: string): void {
116
121
 
117
122
  const inflightRefreshes = new Map<string, Promise<string>>();
118
123
 
119
- function deduplicatedRefresh(service: string): Promise<string> {
120
- const existing = inflightRefreshes.get(service);
124
+ function deduplicatedRefresh(service: string, connId: string): Promise<string> {
125
+ const existing = inflightRefreshes.get(connId);
121
126
  if (existing) return existing;
122
127
 
123
- const promise = doRefresh(service).finally(() => {
124
- inflightRefreshes.delete(service);
128
+ const promise = doRefresh(service, connId).finally(() => {
129
+ inflightRefreshes.delete(connId);
125
130
  });
126
- inflightRefreshes.set(service, promise);
131
+ inflightRefreshes.set(connId, promise);
127
132
  return promise;
128
133
  }
129
134
 
@@ -157,18 +162,11 @@ export class TokenExpiredError extends Error {
157
162
  }
158
163
 
159
164
  /**
160
- * Check whether the access token for a service is expired or will expire
161
- * within the buffer window, based on the `expiresAt` field in the
162
- * oauth_connection row.
165
+ * Check whether a token is expired or will expire within the buffer window.
163
166
  */
164
- function isTokenExpired(service: string): boolean {
165
- try {
166
- const conn = getConnectionByProvider(service);
167
- if (!conn?.expiresAt) return false;
168
- return Date.now() >= conn.expiresAt - EXPIRY_BUFFER_MS;
169
- } catch {
170
- return false;
171
- }
167
+ function isTokenExpired(expiresAt: number | null): boolean {
168
+ if (!expiresAt) return false;
169
+ return Date.now() >= expiresAt - EXPIRY_BUFFER_MS;
172
170
  }
173
171
 
174
172
  // ── Refresh config resolution ─────────────────────────────────────────
@@ -191,8 +189,11 @@ interface RefreshConfig {
191
189
  * authMethod. Throws `TokenExpiredError` if the connection is not found
192
190
  * or incomplete.
193
191
  */
194
- function resolveRefreshConfig(service: string): RefreshConfig {
195
- const conn = getConnectionByProvider(service);
192
+ async function resolveRefreshConfig(
193
+ service: string,
194
+ connId: string,
195
+ ): Promise<RefreshConfig> {
196
+ const conn = getConnection(connId);
196
197
  if (!conn) {
197
198
  throw new TokenExpiredError(
198
199
  service,
@@ -217,17 +218,17 @@ function resolveRefreshConfig(service: string): RefreshConfig {
217
218
  }
218
219
 
219
220
  const tokenUrl = provider.tokenUrl;
220
- const clientId = app.clientId;
221
- if (!tokenUrl || !clientId) {
221
+ const resolvedClientId = app.clientId;
222
+ if (!tokenUrl || !resolvedClientId) {
222
223
  throw new TokenExpiredError(
223
224
  service,
224
225
  `Missing OAuth2 refresh config for "${service}".${recoveryHint(service)}`,
225
226
  );
226
227
  }
227
228
 
228
- const secret = getSecureKey(`oauth_app/${app.id}/client_secret`);
229
+ const secret = await getSecureKeyAsync(app.clientSecretCredentialPath);
229
230
 
230
- const refreshToken = getSecureKey(
231
+ const refreshToken = await getSecureKeyAsync(
231
232
  `oauth_connection/${conn.id}/refresh_token`,
232
233
  );
233
234
 
@@ -238,7 +239,7 @@ function resolveRefreshConfig(service: string): RefreshConfig {
238
239
  return {
239
240
  connId: conn.id,
240
241
  tokenUrl,
241
- clientId,
242
+ clientId: resolvedClientId,
242
243
  secret,
243
244
  refreshToken,
244
245
  authMethod,
@@ -254,10 +255,15 @@ function resolveRefreshConfig(service: string): RefreshConfig {
254
255
  * Returns the new access token on success.
255
256
  * Throws `TokenExpiredError` if refresh is not possible.
256
257
  */
257
- async function doRefresh(service: string): Promise<string> {
258
- const refreshConfig = resolveRefreshConfig(service);
259
- const { tokenUrl, clientId, secret, authMethod, connId, refreshToken } =
260
- refreshConfig;
258
+ async function doRefresh(service: string, connId: string): Promise<string> {
259
+ const refreshConfig = await resolveRefreshConfig(service, connId);
260
+ const {
261
+ tokenUrl,
262
+ clientId: resolvedClientId,
263
+ secret,
264
+ authMethod,
265
+ refreshToken,
266
+ } = refreshConfig;
261
267
 
262
268
  if (!refreshToken) {
263
269
  throw new TokenExpiredError(
@@ -266,8 +272,8 @@ async function doRefresh(service: string): Promise<string> {
266
272
  );
267
273
  }
268
274
 
269
- if (isRefreshBreakerOpen(service)) {
270
- const state = refreshBreakers.get(service)!;
275
+ if (isRefreshBreakerOpen(connId)) {
276
+ const state = refreshBreakers.get(connId)!;
271
277
  const remainingMs = state.cooldownMs - (Date.now() - state.openedAt);
272
278
  throw new TokenExpiredError(
273
279
  service,
@@ -282,13 +288,13 @@ async function doRefresh(service: string): Promise<string> {
282
288
  try {
283
289
  result = await refreshOAuth2Token(
284
290
  tokenUrl,
285
- clientId,
291
+ resolvedClientId,
286
292
  refreshToken,
287
293
  secret,
288
294
  authMethod,
289
295
  );
290
296
  } catch (err) {
291
- recordRefreshFailure(service);
297
+ recordRefreshFailure(connId);
292
298
  if (isCredentialError(err)) {
293
299
  const msg = err instanceof Error ? err.message : String(err);
294
300
  throw new TokenExpiredError(
@@ -349,7 +355,7 @@ async function doRefresh(service: string): Promise<string> {
349
355
  );
350
356
  }
351
357
 
352
- recordRefreshSuccess(service);
358
+ recordRefreshSuccess(connId);
353
359
  log.info({ service }, "OAuth2 access token refreshed successfully");
354
360
  return result.accessToken;
355
361
  }
@@ -368,12 +374,13 @@ async function doRefresh(service: string): Promise<string> {
368
374
  export async function withValidToken<T>(
369
375
  service: string,
370
376
  callback: (token: string) => Promise<T>,
377
+ clientId?: string,
371
378
  ): Promise<T> {
372
- const conn = getConnectionByProvider(service);
379
+ const conn = getConnectionByProvider(service, clientId);
373
380
  let token = conn
374
381
  ? getSecureKey(`oauth_connection/${conn.id}/access_token`)
375
382
  : undefined;
376
- if (!token) {
383
+ if (!token || !conn) {
377
384
  throw new TokenExpiredError(
378
385
  service,
379
386
  `No access token found for "${service}". Authorization required.${recoveryHint(service)}`,
@@ -381,15 +388,15 @@ export async function withValidToken<T>(
381
388
  }
382
389
 
383
390
  // Proactively refresh if expired or about to expire.
384
- if (isTokenExpired(service)) {
385
- token = await deduplicatedRefresh(service);
391
+ if (isTokenExpired(conn.expiresAt)) {
392
+ token = await deduplicatedRefresh(service, conn.id);
386
393
  }
387
394
 
388
395
  try {
389
396
  return await callback(token);
390
397
  } catch (err: unknown) {
391
398
  if (is401Error(err)) {
392
- token = await deduplicatedRefresh(service);
399
+ token = await deduplicatedRefresh(service, conn.id);
393
400
  return callback(token);
394
401
  }
395
402
  throw err;
@@ -55,27 +55,3 @@ export async function deployHtmlToVercel(opts: {
55
55
 
56
56
  return { url: publicUrl, deploymentId: data.id };
57
57
  }
58
-
59
- export async function deleteVercelDeployment(
60
- deploymentId: string,
61
- token: string,
62
- ): Promise<void> {
63
- const response = await fetch(
64
- `https://api.vercel.com/v13/deployments/${deploymentId}`,
65
- {
66
- method: "DELETE",
67
- headers: {
68
- Authorization: `Bearer ${token}`,
69
- },
70
- },
71
- );
72
-
73
- if (!response.ok) {
74
- const text = await response.text();
75
- throw new ProviderError(
76
- `Vercel delete deployment failed (${response.status}): ${text}`,
77
- "vercel",
78
- response.status,
79
- );
80
- }
81
- }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Handle confirmation decisions delivered via signal files from the CLI.
3
+ *
4
+ * The built-in CLI writes JSON to `signals/confirm` instead of making an
5
+ * HTTP POST to `/v1/confirm`. The daemon's ConfigWatcher detects the file
6
+ * change and invokes {@link handleConfirmationSignal}, which reads the
7
+ * payload and resolves the pending interaction in-process.
8
+ */
9
+
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ import type { UserDecision } from "../permissions/types.js";
14
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
15
+ import { getLogger } from "../util/logger.js";
16
+ import { getWorkspaceDir } from "../util/platform.js";
17
+
18
+ const log = getLogger("signal:confirm");
19
+
20
+ const VALID_DECISIONS: ReadonlySet<string> = new Set<string>([
21
+ "allow",
22
+ "allow_10m",
23
+ "allow_thread",
24
+ "always_allow",
25
+ "always_allow_high_risk",
26
+ "deny",
27
+ "always_deny",
28
+ "temporary_override",
29
+ ]);
30
+
31
+ function isUserDecision(value: string): value is UserDecision {
32
+ return VALID_DECISIONS.has(value);
33
+ }
34
+
35
+ /**
36
+ * Read the `signals/confirm` file and resolve the pending interaction.
37
+ * Called by ConfigWatcher when the signal file is written or modified.
38
+ */
39
+ export function handleConfirmationSignal(): void {
40
+ try {
41
+ const content = readFileSync(
42
+ join(getWorkspaceDir(), "signals", "confirm"),
43
+ "utf-8",
44
+ );
45
+ const parsed = JSON.parse(content) as {
46
+ requestId?: string;
47
+ decision?: string;
48
+ };
49
+ const { requestId, decision } = parsed;
50
+
51
+ if (!requestId || typeof requestId !== "string") {
52
+ log.warn("Confirmation signal missing requestId");
53
+ return;
54
+ }
55
+ if (!decision || !isUserDecision(decision)) {
56
+ log.warn({ decision }, "Confirmation signal has invalid decision");
57
+ return;
58
+ }
59
+
60
+ const interaction = pendingInteractions.resolve(requestId);
61
+ if (!interaction) {
62
+ log.warn({ requestId }, "No pending interaction for confirmation signal");
63
+ return;
64
+ }
65
+
66
+ interaction.session.handleConfirmationResponse(
67
+ requestId,
68
+ decision,
69
+ undefined,
70
+ undefined,
71
+ undefined,
72
+ { source: "button" },
73
+ );
74
+ log.info({ requestId, decision }, "Confirmation resolved via signal file");
75
+ } catch (err) {
76
+ log.error({ err }, "Failed to handle confirmation signal");
77
+ }
78
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Handle MCP reload signals from the CLI.
3
+ *
4
+ * When the CLI writes to `signals/mcp-reload`, the daemon's ConfigWatcher
5
+ * detects the file change and invokes {@link handleMcpReloadSignal} to
6
+ * restart MCP servers with the latest configuration.
7
+ */
8
+
9
+ import { reloadMcpServers } from "../daemon/mcp-reload-service.js";
10
+ import { getLogger } from "../util/logger.js";
11
+
12
+ const log = getLogger("signal:mcp-reload");
13
+
14
+ export function handleMcpReloadSignal(): void {
15
+ reloadMcpServers().catch((err: unknown) => {
16
+ log.error({ err }, "MCP reload triggered by signal file failed");
17
+ });
18
+ }