@vellumai/assistant 0.4.45 → 0.4.48

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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
@@ -44,11 +44,6 @@ class BrowserNavigateTool implements Tool {
44
44
  description:
45
45
  "If true, allows navigation to localhost/private-network hosts. Disabled by default for SSRF safety.",
46
46
  },
47
- reason: {
48
- type: "string",
49
- description:
50
- "Brief non-technical explanation of what you are navigating to and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
51
- },
52
47
  },
53
48
  required: ["url"],
54
49
  },
@@ -80,13 +75,7 @@ class BrowserSnapshotTool implements Tool {
80
75
  description: this.description,
81
76
  input_schema: {
82
77
  type: "object",
83
- properties: {
84
- reason: {
85
- type: "string",
86
- description:
87
- "Brief non-technical explanation of what you are inspecting and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
88
- },
89
- },
78
+ properties: {},
90
79
  },
91
80
  };
92
81
  }
@@ -122,11 +111,6 @@ class BrowserScreenshotTool implements Tool {
122
111
  description:
123
112
  "Capture the full scrollable page instead of just the viewport.",
124
113
  },
125
- reason: {
126
- type: "string",
127
- description:
128
- "Brief non-technical explanation of what you are capturing and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
129
- },
130
114
  },
131
115
  },
132
116
  };
@@ -163,11 +147,6 @@ class BrowserCloseTool implements Tool {
163
147
  description:
164
148
  "If true, close all browser pages and the browser context. Default: false (close only the current session page).",
165
149
  },
166
- reason: {
167
- type: "string",
168
- description:
169
- "Brief non-technical explanation of what you are doing and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
170
- },
171
150
  },
172
151
  },
173
152
  };
@@ -214,11 +193,6 @@ class BrowserClickTool implements Tool {
214
193
  description:
215
194
  "Max time in ms to wait for the element to be clickable (default: 10000).",
216
195
  },
217
- reason: {
218
- type: "string",
219
- description:
220
- "Brief non-technical explanation of what you are clicking and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
221
- },
222
196
  },
223
197
  },
224
198
  };
@@ -273,11 +247,6 @@ class BrowserTypeTool implements Tool {
273
247
  type: "boolean",
274
248
  description: "If true, press Enter after typing the text.",
275
249
  },
276
- reason: {
277
- type: "string",
278
- description:
279
- "Brief non-technical explanation of what you are typing and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
280
- },
281
250
  },
282
251
  required: ["text"],
283
252
  },
@@ -323,11 +292,6 @@ class BrowserPressKeyTool implements Tool {
323
292
  type: "string",
324
293
  description: "Optional CSS selector to target.",
325
294
  },
326
- reason: {
327
- type: "string",
328
- description:
329
- "Brief non-technical explanation of what you are doing and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
330
- },
331
295
  },
332
296
  required: ["key"],
333
297
  },
@@ -378,11 +342,6 @@ class BrowserScrollTool implements Tool {
378
342
  type: "string",
379
343
  description: "Optional CSS selector of element to scroll within.",
380
344
  },
381
- reason: {
382
- type: "string",
383
- description:
384
- "Brief non-technical explanation of what you are scrolling to see and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
385
- },
386
345
  },
387
346
  required: ["direction"],
388
347
  },
@@ -437,11 +396,6 @@ class BrowserSelectOptionTool implements Tool {
437
396
  type: "number",
438
397
  description: "The zero-based index of the <option> to select.",
439
398
  },
440
- reason: {
441
- type: "string",
442
- description:
443
- "Brief non-technical explanation of what you are selecting and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
444
- },
445
399
  },
446
400
  },
447
401
  };
@@ -483,11 +437,6 @@ class BrowserHoverTool implements Tool {
483
437
  description:
484
438
  "A CSS selector to target. Used as fallback when element_id is not available.",
485
439
  },
486
- reason: {
487
- type: "string",
488
- description:
489
- "Brief non-technical explanation of what you are hovering over and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
490
- },
491
440
  },
492
441
  },
493
442
  };
@@ -537,11 +486,6 @@ class BrowserWaitForTool implements Tool {
537
486
  description:
538
487
  "Maximum wait time in milliseconds (default and max: 30000).",
539
488
  },
540
- reason: {
541
- type: "string",
542
- description:
543
- "Brief non-technical explanation of what you are waiting for and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
544
- },
545
489
  },
546
490
  },
547
491
  };
@@ -578,11 +522,6 @@ class BrowserExtractTool implements Tool {
578
522
  description:
579
523
  "If true, include a list of links found on the page (up to 200).",
580
524
  },
581
- reason: {
582
- type: "string",
583
- description:
584
- "Brief non-technical explanation of what you are extracting and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
585
- },
586
525
  },
587
526
  },
588
527
  };
@@ -634,11 +573,6 @@ class BrowserFillCredentialTool implements Tool {
634
573
  type: "boolean",
635
574
  description: "Press Enter after filling",
636
575
  },
637
- reason: {
638
- type: "string",
639
- description:
640
- "Brief non-technical explanation of what you are filling in and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
641
- },
642
576
  },
643
577
  required: ["service", "field"],
644
578
  },
@@ -123,11 +123,6 @@ export const claudeCodeTool: Tool = {
123
123
  description:
124
124
  "Worker profile that scopes tool access. Defaults to general (backward compatible).",
125
125
  },
126
- reason: {
127
- type: "string",
128
- description:
129
- "Brief non-technical explanation of what you are delegating and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
130
- },
131
126
  },
132
127
  },
133
128
  };
@@ -42,11 +42,6 @@ export const requestComputerControlTool: Tool = {
42
42
  description:
43
43
  "Concise description of what computer use should accomplish",
44
44
  },
45
- reason: {
46
- type: "string",
47
- description:
48
- "Brief non-technical explanation of what you are doing and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
49
- },
50
45
  },
51
46
  required: ["task"],
52
47
  },
@@ -1,5 +1,6 @@
1
1
  import { v4 as uuid } from "uuid";
2
2
 
3
+ import { credentialKey, migrateKeys } from "../../security/credential-key.js";
3
4
  import { getSecureKey } from "../../security/secure-keys.js";
4
5
  import { getLogger } from "../../util/logger.js";
5
6
  import type {
@@ -46,7 +47,7 @@ export class CredentialBroker {
46
47
  * browserFill or consume call for this service/field pair, then discarded.
47
48
  */
48
49
  injectTransient(service: string, field: string, value: string): void {
49
- const key = `credential:${service}:${field}`;
50
+ const key = credentialKey(service, field);
50
51
  this.transientValues.set(key, { value });
51
52
  log.info(
52
53
  { service, field },
@@ -59,6 +60,7 @@ export class CredentialBroker {
59
60
  * Returns a single-use token on success, or a denial reason on failure.
60
61
  */
61
62
  authorize(request: AuthorizeRequest): AuthorizeResult {
63
+ migrateKeys();
62
64
  const metadata = getCredentialMetadata(request.service, request.field);
63
65
  if (!metadata) {
64
66
  return {
@@ -123,7 +125,7 @@ export class CredentialBroker {
123
125
  }
124
126
 
125
127
  token.consumed = true;
126
- const storageKey = `credential:${token.service}:${token.field}`;
128
+ const storageKey = credentialKey(token.service, token.field);
127
129
  // Check for transient value first (one-time send) — consume and return the value
128
130
  // directly since transient values are never persisted to secure storage.
129
131
  const transient = this.transientValues.get(storageKey);
@@ -211,7 +213,7 @@ export class CredentialBroker {
211
213
  }
212
214
  }
213
215
 
214
- const storageKey = `credential:${request.service}:${request.field}`;
216
+ const storageKey = credentialKey(request.service, request.field);
215
217
  // Check transient values first (one-time send), then fall back to keychain.
216
218
  // Deletion is deferred until after a successful fill so the value survives
217
219
  // transient failures (e.g. stale element, page navigation, Playwright timeout).
@@ -299,7 +301,7 @@ export class CredentialBroker {
299
301
  };
300
302
  }
301
303
 
302
- const storageKey = `credential:${request.service}:${request.field}`;
304
+ const storageKey = credentialKey(request.service, request.field);
303
305
  const transient = this.transientValues.get(storageKey);
304
306
  const value = transient?.value ?? getSecureKey(storageKey);
305
307
  if (!value) {
@@ -27,20 +27,20 @@ export interface CredentialMetadata {
27
27
  oauth2TokenUrl?: string;
28
28
  /** OAuth2 client ID — paired with oauth2TokenUrl for refresh. */
29
29
  oauth2ClientId?: string;
30
- /** OAuth2 client secret — for providers that require it (e.g. Slack). Stored in metadata for autonomous refresh. */
31
- oauth2ClientSecret?: string;
32
30
  /** How the client authenticates at the token endpoint (client_secret_basic or client_secret_post). */
33
31
  oauth2TokenEndpointAuthMethod?: string;
34
32
  /** Human-friendly name for this credential (e.g. "fal-primary"). */
35
33
  alias?: string;
36
34
  /** Templates describing how to inject this credential into proxied requests. */
37
35
  injectionTemplates?: CredentialInjectionTemplate[];
36
+ /** Whether a refresh token exists in the secure store for this service. */
37
+ hasRefreshToken?: boolean;
38
38
  createdAt: number;
39
39
  updatedAt: number;
40
40
  }
41
41
 
42
42
  /** Current on-disk schema version. */
43
- const CURRENT_VERSION = 2;
43
+ const CURRENT_VERSION = 4;
44
44
 
45
45
  interface MetadataFile {
46
46
  version: typeof CURRENT_VERSION;
@@ -120,10 +120,6 @@ function migrateRecordV1toV2(
120
120
  typeof record.oauth2ClientId === "string"
121
121
  ? record.oauth2ClientId
122
122
  : undefined,
123
- oauth2ClientSecret:
124
- typeof record.oauth2ClientSecret === "string"
125
- ? record.oauth2ClientSecret
126
- : undefined,
127
123
  oauth2TokenEndpointAuthMethod:
128
124
  typeof record.oauth2TokenEndpointAuthMethod === "string"
129
125
  ? record.oauth2TokenEndpointAuthMethod
@@ -132,11 +128,52 @@ function migrateRecordV1toV2(
132
128
  injectionTemplates: Array.isArray(record.injectionTemplates)
133
129
  ? (record.injectionTemplates as CredentialInjectionTemplate[])
134
130
  : undefined,
131
+ hasRefreshToken:
132
+ typeof record.hasRefreshToken === "boolean"
133
+ ? record.hasRefreshToken
134
+ : undefined,
135
135
  createdAt: record.createdAt as number,
136
136
  updatedAt: record.updatedAt as number,
137
137
  };
138
138
  }
139
139
 
140
+ /**
141
+ * Migrate a v2 record to v3 by stripping the oauth2ClientSecret field.
142
+ * Client secrets are now read exclusively from the secure key store.
143
+ */
144
+ function migrateRecordV2toV3(record: CredentialMetadata): CredentialMetadata {
145
+ const { oauth2ClientSecret: _removed, ...rest } =
146
+ record as CredentialMetadata & { oauth2ClientSecret?: string };
147
+ return rest;
148
+ }
149
+
150
+ /**
151
+ * Migrate v3 credentials to v4:
152
+ * - Delete ghost `refresh_token` metadata records
153
+ * - Set `hasRefreshToken: true` on corresponding `access_token` records
154
+ */
155
+ function migrateV3toV4(
156
+ credentials: CredentialMetadata[],
157
+ ): CredentialMetadata[] {
158
+ // Collect services that had refresh_token ghost records
159
+ const servicesWithRefresh = new Set<string>();
160
+ for (const c of credentials) {
161
+ if (c.field === "refresh_token") {
162
+ servicesWithRefresh.add(c.service);
163
+ }
164
+ }
165
+
166
+ // Remove all refresh_token records and set hasRefreshToken on access_token records
167
+ const filtered = credentials.filter((c) => c.field !== "refresh_token");
168
+ for (const c of filtered) {
169
+ if (c.field === "access_token" && servicesWithRefresh.has(c.service)) {
170
+ c.hasRefreshToken = true;
171
+ }
172
+ }
173
+
174
+ return filtered;
175
+ }
176
+
140
177
  function loadFile(): LoadResult {
141
178
  const raw = readTextFileSync(getMetadataPath());
142
179
  if (raw == null) {
@@ -148,7 +185,12 @@ function loadFile(): LoadResult {
148
185
  return { version: CURRENT_VERSION, credentials: [] };
149
186
  }
150
187
  const fileVersion = typeof data.version === "number" ? data.version : 1;
151
- if (fileVersion !== 1 && fileVersion !== 2) {
188
+ if (
189
+ fileVersion !== 1 &&
190
+ fileVersion !== 2 &&
191
+ fileVersion !== 3 &&
192
+ fileVersion !== 4
193
+ ) {
152
194
  // Unrecognized version (future, fractional, negative, zero) — refuse to touch it
153
195
  return { unknownVersion: true };
154
196
  }
@@ -159,8 +201,24 @@ function loadFile(): LoadResult {
159
201
  const validRecords = rawCredentials.filter(isValidCredentialRecord);
160
202
 
161
203
  if (fileVersion < CURRENT_VERSION) {
162
- // Migrate from v1 to v2 and persist the upgrade so we don't re-migrate on every read
163
- const credentials = validRecords.map(migrateRecordV1toV2);
204
+ // Apply migrations in sequence: v1→v2→v3→v4
205
+ let credentials: CredentialMetadata[];
206
+ if (fileVersion === 1) {
207
+ credentials = migrateV3toV4(
208
+ validRecords.map(migrateRecordV1toV2).map(migrateRecordV2toV3),
209
+ );
210
+ } else if (fileVersion === 2) {
211
+ credentials = migrateV3toV4(
212
+ (validRecords as unknown as CredentialMetadata[]).map(
213
+ migrateRecordV2toV3,
214
+ ),
215
+ );
216
+ } else {
217
+ // fileVersion === 3
218
+ credentials = migrateV3toV4(
219
+ validRecords as unknown as CredentialMetadata[],
220
+ );
221
+ }
164
222
  const migrated: MetadataFile = { version: CURRENT_VERSION, credentials };
165
223
  try {
166
224
  saveFile(migrated);
@@ -219,13 +277,12 @@ export function upsertCredentialMetadata(
219
277
  grantedScopes?: string[];
220
278
  oauth2TokenUrl?: string;
221
279
  oauth2ClientId?: string;
222
- /** Pass `null` to explicitly clear a previously-set client secret. */
223
- oauth2ClientSecret?: string | null;
224
280
  oauth2TokenEndpointAuthMethod?: string;
225
281
  /** Pass `null` to explicitly clear a previously-set alias. */
226
282
  alias?: string | null;
227
283
  /** Pass `null` to explicitly clear injection templates. */
228
284
  injectionTemplates?: CredentialInjectionTemplate[] | null;
285
+ hasRefreshToken?: boolean;
229
286
  },
230
287
  ): CredentialMetadata {
231
288
  const result = loadFile();
@@ -261,13 +318,6 @@ export function upsertCredentialMetadata(
261
318
  existing.oauth2TokenUrl = policy.oauth2TokenUrl;
262
319
  if (policy?.oauth2ClientId !== undefined)
263
320
  existing.oauth2ClientId = policy.oauth2ClientId;
264
- if (policy?.oauth2ClientSecret !== undefined) {
265
- if (policy.oauth2ClientSecret == null) {
266
- delete existing.oauth2ClientSecret;
267
- } else {
268
- existing.oauth2ClientSecret = policy.oauth2ClientSecret;
269
- }
270
- }
271
321
  if (policy?.oauth2TokenEndpointAuthMethod !== undefined)
272
322
  existing.oauth2TokenEndpointAuthMethod =
273
323
  policy.oauth2TokenEndpointAuthMethod;
@@ -285,6 +335,8 @@ export function upsertCredentialMetadata(
285
335
  existing.injectionTemplates = policy.injectionTemplates;
286
336
  }
287
337
  }
338
+ if (policy?.hasRefreshToken !== undefined)
339
+ existing.hasRefreshToken = policy.hasRefreshToken;
288
340
  existing.updatedAt = now;
289
341
  saveFile(data);
290
342
  return existing;
@@ -301,10 +353,10 @@ export function upsertCredentialMetadata(
301
353
  grantedScopes: policy?.grantedScopes,
302
354
  oauth2TokenUrl: policy?.oauth2TokenUrl,
303
355
  oauth2ClientId: policy?.oauth2ClientId,
304
- oauth2ClientSecret: policy?.oauth2ClientSecret ?? undefined,
305
356
  oauth2TokenEndpointAuthMethod: policy?.oauth2TokenEndpointAuthMethod,
306
357
  alias: policy?.alias ?? undefined,
307
358
  injectionTemplates: policy?.injectionTemplates ?? undefined,
359
+ hasRefreshToken: policy?.hasRefreshToken,
308
360
  createdAt: now,
309
361
  updatedAt: now,
310
362
  };
@@ -6,6 +6,7 @@
6
6
  * secure key naming convention.
7
7
  */
8
8
 
9
+ import { credentialKey } from "../../security/credential-key.js";
9
10
  import { matchHostPattern } from "./host-pattern-match.js";
10
11
  import {
11
12
  type CredentialMetadata,
@@ -33,7 +34,7 @@ function toResolved(metadata: CredentialMetadata): ResolvedCredential {
33
34
  credentialId: metadata.credentialId,
34
35
  service: metadata.service,
35
36
  field: metadata.field,
36
- storageKey: `credential:${metadata.service}:${metadata.field}`,
37
+ storageKey: credentialKey(metadata.service, metadata.field),
37
38
  alias: metadata.alias,
38
39
  injectionTemplates: metadata.injectionTemplates ?? [],
39
40
  metadata,
@@ -7,6 +7,10 @@ import {
7
7
  } from "../../oauth/provider-profiles.js";
8
8
  import { RiskLevel } from "../../permissions/types.js";
9
9
  import type { ToolDefinition } from "../../providers/types.js";
10
+ import { buildAssistantEvent } from "../../runtime/assistant-event.js";
11
+ import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
12
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
13
+ import { credentialKey, migrateKeys } from "../../security/credential-key.js";
10
14
  import type { TokenEndpointAuthMethod } from "../../security/oauth2.js";
11
15
  import {
12
16
  deleteSecureKeyAsync,
@@ -33,10 +37,10 @@ import { toPolicyFromInput, validatePolicyInput } from "./policy-validate.js";
33
37
  const log = getLogger("credential-vault");
34
38
 
35
39
  /**
36
- * Look up a stored OAuth field (e.g. client_id or client_secret) for a service.
37
- * Checks both the canonical and alias service names.
40
+ * Look up a stored OAuth secret (e.g. client_secret) for a service from the
41
+ * secure store. Checks both the canonical and alias service names.
38
42
  */
39
- function findStoredOAuthField(
43
+ function findStoredOAuthSecret(
40
44
  service: string,
41
45
  field: string,
42
46
  ): string | undefined {
@@ -47,7 +51,30 @@ function findStoredOAuthField(
47
51
  if (alias === service) servicesToCheck.push(canonical);
48
52
  }
49
53
  for (const svc of servicesToCheck) {
50
- const value = getSecureKey(`credential:${svc}:${field}`);
54
+ const value = getSecureKey(credentialKey(svc, field));
55
+ if (value) return value;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * Look up the stored OAuth client_id for a service from credential metadata.
62
+ * Checks both the canonical and alias service names.
63
+ */
64
+ function findStoredOAuthClientId(service: string): string | undefined {
65
+ const servicesToCheck = [service];
66
+ for (const [alias, canonical] of Object.entries(SERVICE_ALIASES)) {
67
+ if (canonical === service) servicesToCheck.push(alias);
68
+ if (alias === service) servicesToCheck.push(canonical);
69
+ }
70
+ // Check metadata first (written by oauth2_connect after successful auth)
71
+ for (const svc of servicesToCheck) {
72
+ const meta = getCredentialMetadata(svc, "access_token");
73
+ if (meta?.oauth2ClientId) return meta.oauth2ClientId;
74
+ }
75
+ // Fall back to secure key store (written by credential_store prompt action)
76
+ for (const svc of servicesToCheck) {
77
+ const value = getSecureKey(credentialKey(svc, "client_id"));
51
78
  if (value) return value;
52
79
  }
53
80
  return undefined;
@@ -206,11 +233,6 @@ class CredentialStoreTool implements Tool {
206
233
  description:
207
234
  "Templates describing how to inject this credential into proxied requests (for store and prompt actions)",
208
235
  },
209
- reason: {
210
- type: "string",
211
- description:
212
- "Brief non-technical explanation of what you are doing and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
213
- },
214
236
  },
215
237
  required: ["action"],
216
238
  },
@@ -221,6 +243,7 @@ class CredentialStoreTool implements Tool {
221
243
  input: Record<string, unknown>,
222
244
  context: ToolContext,
223
245
  ): Promise<ToolExecutionResult> {
246
+ migrateKeys();
224
247
  const action = input.action as string;
225
248
 
226
249
  switch (action) {
@@ -359,7 +382,7 @@ class CredentialStoreTool implements Tool {
359
382
  };
360
383
  }
361
384
 
362
- const key = `credential:${service}:${field}`;
385
+ const key = credentialKey(service, field);
363
386
  const ok = await setSecureKeyAsync(key, value);
364
387
  if (!ok) {
365
388
  return {
@@ -422,7 +445,7 @@ class CredentialStoreTool implements Tool {
422
445
  const entries = allMetadata
423
446
  .filter((m) => {
424
447
  if (secureKeySet)
425
- return secureKeySet.has(`credential:${m.service}:${m.field}`);
448
+ return secureKeySet.has(credentialKey(m.service, m.field));
426
449
  return true;
427
450
  })
428
451
  .map((m) => {
@@ -472,7 +495,7 @@ class CredentialStoreTool implements Tool {
472
495
  };
473
496
  }
474
497
 
475
- const key = `credential:${service}:${field}`;
498
+ const key = credentialKey(service, field);
476
499
  const result = await deleteSecureKeyAsync(key);
477
500
  if (result === "error") {
478
501
  return {
@@ -712,7 +735,7 @@ class CredentialStoreTool implements Tool {
712
735
  }
713
736
 
714
737
  // Default: persist to keychain
715
- const key = `credential:${service}:${field}`;
738
+ const key = credentialKey(service, field);
716
739
  const ok = await setSecureKeyAsync(key, result.value);
717
740
  if (!ok) {
718
741
  return {
@@ -757,13 +780,13 @@ class CredentialStoreTool implements Tool {
757
780
  // Fill missing params from provider profile
758
781
  const profile = getProviderProfile(service);
759
782
 
760
- // Look up client_id/client_secret from stored credentials if not provided
783
+ // Look up client_id from metadata and client_secret from secure store
761
784
  const clientId =
762
785
  (input.client_id as string | undefined) ??
763
- findStoredOAuthField(service, "client_id");
786
+ findStoredOAuthClientId(service);
764
787
  const clientSecret =
765
788
  (input.client_secret as string | undefined) ??
766
- findStoredOAuthField(service, "client_secret");
789
+ findStoredOAuthSecret(service, "client_secret");
767
790
 
768
791
  // Early guardrails that stay in vault.ts (credential resolution is vault-specific)
769
792
  const inputScopes = input.scopes as string[] | undefined;
@@ -867,6 +890,44 @@ class CredentialStoreTool implements Tool {
867
890
  userinfoUrl:
868
891
  (input.userinfo_url as string | undefined) ?? profile?.userinfoUrl,
869
892
  tokenEndpointAuthMethod,
893
+ onDeferredComplete: (deferredResult) => {
894
+ // Emit oauth_connect_result to all connected SSE clients so the
895
+ // UI can update immediately when the deferred browser flow completes.
896
+ assistantEventHub
897
+ .publish(
898
+ buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, {
899
+ type: "oauth_connect_result",
900
+ success: deferredResult.success,
901
+ service: deferredResult.service,
902
+ accountInfo: deferredResult.accountInfo,
903
+ error: deferredResult.error,
904
+ }),
905
+ )
906
+ .catch((err) => {
907
+ log.warn(
908
+ { err, service: deferredResult.service },
909
+ "Failed to publish oauth_connect_result event",
910
+ );
911
+ });
912
+
913
+ if (deferredResult.success) {
914
+ log.info(
915
+ {
916
+ service: deferredResult.service,
917
+ accountInfo: deferredResult.accountInfo,
918
+ },
919
+ "Deferred OAuth connect completed successfully",
920
+ );
921
+ } else {
922
+ log.warn(
923
+ {
924
+ service: deferredResult.service,
925
+ err: deferredResult.error,
926
+ },
927
+ "Deferred OAuth connect failed",
928
+ );
929
+ }
930
+ },
870
931
  });
871
932
 
872
933
  if (!result.success) {
@@ -38,13 +38,8 @@ class FileEditTool implements Tool {
38
38
  description:
39
39
  "Replace all occurrences of old_string instead of requiring a unique match (default: false)",
40
40
  },
41
- reason: {
42
- type: "string",
43
- description:
44
- "Brief non-technical explanation of why this file is being edited, shown to the user as a status update. Use simple language a non-technical person would understand.",
45
- },
46
41
  },
47
- required: ["path", "old_string", "new_string", "reason"],
42
+ required: ["path", "old_string", "new_string"],
48
43
  },
49
44
  };
50
45
  }
@@ -38,11 +38,6 @@ class FileReadTool implements Tool {
38
38
  type: "number",
39
39
  description: "Maximum number of lines to read",
40
40
  },
41
- reason: {
42
- type: "string",
43
- description:
44
- "Brief non-technical explanation of what you are reading and why, shown to the user as a status update. Use simple language a non-technical person would understand.",
45
- },
46
41
  },
47
42
  required: ["path"],
48
43
  },
@@ -28,13 +28,8 @@ class FileWriteTool implements Tool {
28
28
  type: "string",
29
29
  description: "The content to write to the file",
30
30
  },
31
- reason: {
32
- type: "string",
33
- description:
34
- "Brief non-technical explanation of why this file is being written, shown to the user as a status update. Use simple language a non-technical person would understand.",
35
- },
36
31
  },
37
- required: ["path", "content", "reason"],
32
+ required: ["path", "content"],
38
33
  },
39
34
  };
40
35
  }
@@ -35,13 +35,8 @@ class HostFileEditTool implements Tool {
35
35
  description:
36
36
  "Replace all occurrences instead of requiring a unique match (default: false)",
37
37
  },
38
- reason: {
39
- type: "string",
40
- description:
41
- "Brief non-technical explanation of why this file is being edited, shown to the user as a status update. Use simple language a non-technical person would understand.",
42
- },
43
38
  },
44
- required: ["path", "old_string", "new_string", "reason"],
39
+ required: ["path", "old_string", "new_string"],
45
40
  },
46
41
  };
47
42
  }
@@ -29,13 +29,8 @@ class HostFileReadTool implements Tool {
29
29
  type: "number",
30
30
  description: "Maximum number of lines to read",
31
31
  },
32
- reason: {
33
- type: "string",
34
- description:
35
- "Brief non-technical explanation of why this file is being read, shown to the user as a status update. Use simple language a non-technical person would understand.",
36
- },
37
32
  },
38
- required: ["path", "reason"],
33
+ required: ["path"],
39
34
  },
40
35
  };
41
36
  }