@vellumai/assistant 0.4.48 → 0.4.49

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 (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -83,6 +83,20 @@ function extractNonAuthHeaders(
83
83
  return Object.keys(result).length > 0 ? result : undefined;
84
84
  }
85
85
 
86
+ /**
87
+ * Convert URLSearchParams to a query record, collapsing multi-valued keys into arrays.
88
+ */
89
+ function paramsToQuery(
90
+ params: URLSearchParams,
91
+ ): Record<string, string | string[]> {
92
+ const result: Record<string, string | string[]> = {};
93
+ for (const key of new Set(params.keys())) {
94
+ const values = params.getAll(key);
95
+ result[key] = values.length === 1 ? values[0] : values;
96
+ }
97
+ return result;
98
+ }
99
+
86
100
  /**
87
101
  * Extract the JSON body from request options for use with OAuthConnection.
88
102
  */
@@ -102,6 +116,7 @@ async function request<T>(
102
116
  connection: OAuthConnection,
103
117
  path: string,
104
118
  options?: GmailRequestOptions,
119
+ query?: Record<string, string | string[]>,
105
120
  ): Promise<T> {
106
121
  const canRetry = options?.retryable ?? isIdempotent(options);
107
122
  const method = (options?.method ?? "GET").toUpperCase();
@@ -112,6 +127,7 @@ async function request<T>(
112
127
  resp = await connection.request({
113
128
  method,
114
129
  path,
130
+ query,
115
131
  headers: {
116
132
  "Content-Type": "application/json",
117
133
  ...extractNonAuthHeaders(options),
@@ -171,7 +187,12 @@ export async function listMessages(
171
187
  if (labelIds) {
172
188
  for (const id of labelIds) params.append("labelIds", id);
173
189
  }
174
- return request<GmailMessageListResponse>(connection, `/messages?${params}`);
190
+ return request<GmailMessageListResponse>(
191
+ connection,
192
+ "/messages",
193
+ undefined,
194
+ paramsToQuery(params),
195
+ );
175
196
  }
176
197
 
177
198
  /** Get a single message by ID. */
@@ -187,7 +208,12 @@ export async function getMessage(
187
208
  for (const h of metadataHeaders) params.append("metadataHeaders", h);
188
209
  }
189
210
  if (fields) params.set("fields", fields);
190
- return request<GmailMessage>(connection, `/messages/${messageId}?${params}`);
211
+ return request<GmailMessage>(
212
+ connection,
213
+ `/messages/${messageId}`,
214
+ undefined,
215
+ paramsToQuery(params),
216
+ );
191
217
  }
192
218
 
193
219
  /**
@@ -332,10 +358,44 @@ async function executeBatchCall(
332
358
  return connection.withToken(doBatchFetch);
333
359
  }
334
360
 
361
+ /** Max concurrent individual getMessage requests (matches batch concurrency) */
362
+ const INDIVIDUAL_CONCURRENCY = BATCH_CONCURRENCY;
363
+
364
+ /**
365
+ * Fetch all messages individually using getMessage (no batch endpoint).
366
+ * Used as a fallback when the batch API is unavailable (e.g. platform connections
367
+ * that cannot expose raw tokens for the multipart batch endpoint).
368
+ *
369
+ * Processes messages in waves of INDIVIDUAL_CONCURRENCY to avoid unbounded
370
+ * parallelism that would trigger 429s on high-volume paths like senderDigest.
371
+ */
372
+ async function fetchMessagesIndividually(
373
+ connection: OAuthConnection,
374
+ messageIds: string[],
375
+ format: GmailMessageFormat,
376
+ metadataHeaders?: string[],
377
+ fields?: string,
378
+ ): Promise<GmailMessage[]> {
379
+ const results: GmailMessage[] = [];
380
+ for (let i = 0; i < messageIds.length; i += INDIVIDUAL_CONCURRENCY) {
381
+ const wave = messageIds.slice(i, i + INDIVIDUAL_CONCURRENCY);
382
+ const waveResults = await Promise.all(
383
+ wave.map((id) =>
384
+ getMessage(connection, id, format, metadataHeaders, fields),
385
+ ),
386
+ );
387
+ results.push(...waveResults);
388
+ }
389
+ return results;
390
+ }
391
+
335
392
  /**
336
393
  * Get multiple messages using Gmail's batch HTTP endpoint.
337
394
  * Packs up to 100 sub-requests per HTTP call and runs up to BATCH_CONCURRENCY calls in parallel.
338
395
  * Falls back to individual getMessage for any sub-requests that fail within a batch.
396
+ *
397
+ * For connections that do not support raw token access (e.g. platform-managed connections),
398
+ * falls back to fetching each message individually via connection.request().
339
399
  */
340
400
  export async function batchGetMessages(
341
401
  connection: OAuthConnection,
@@ -359,6 +419,26 @@ export async function batchGetMessages(
359
419
  ];
360
420
  }
361
421
 
422
+ // Try batch API first; fall back to individual fetches if withToken is unavailable
423
+ // (e.g. platform-managed connections where raw tokens cannot be exposed).
424
+ let useBatch = true;
425
+ try {
426
+ // Probe withToken availability with a no-op call
427
+ await connection.withToken(async (token) => token);
428
+ } catch {
429
+ useBatch = false;
430
+ }
431
+
432
+ if (!useBatch) {
433
+ return fetchMessagesIndividually(
434
+ connection,
435
+ messageIds,
436
+ format,
437
+ metadataHeaders,
438
+ fields,
439
+ );
440
+ }
441
+
362
442
  const results = new Array<GmailMessage | undefined>(messageIds.length).fill(
363
443
  undefined,
364
444
  );
@@ -4,7 +4,8 @@
4
4
  */
5
5
 
6
6
  import type { OAuthConnection } from "../../../oauth/connection.js";
7
- import { GOOGLE_PEOPLE_BASE_URL } from "../../../oauth/provider-base-urls.js";
7
+
8
+ const GOOGLE_PEOPLE_BASE_URL = "https://people.googleapis.com/v1";
8
9
  import { GmailApiError } from "./client.js";
9
10
  import type {
10
11
  PeopleConnectionsResponse,
@@ -16,10 +17,12 @@ const PERSON_FIELDS = "names,emailAddresses,phoneNumbers,organizations";
16
17
  async function request<T>(
17
18
  connection: OAuthConnection,
18
19
  path: string,
20
+ query?: Record<string, string | string[]>,
19
21
  ): Promise<T> {
20
22
  const resp = await connection.request({
21
23
  method: "GET",
22
24
  path,
25
+ query,
23
26
  baseUrl: GOOGLE_PEOPLE_BASE_URL,
24
27
  });
25
28
 
@@ -44,14 +47,15 @@ export async function listContacts(
44
47
  pageSize = 50,
45
48
  pageToken?: string,
46
49
  ): Promise<PeopleConnectionsResponse> {
47
- const params = new URLSearchParams({
50
+ const query: Record<string, string> = {
48
51
  personFields: PERSON_FIELDS,
49
52
  pageSize: String(pageSize),
50
- });
51
- if (pageToken) params.set("pageToken", pageToken);
53
+ };
54
+ if (pageToken) query.pageToken = pageToken;
52
55
  return request<PeopleConnectionsResponse>(
53
56
  connection,
54
- `/people/me/connections?${params}`,
57
+ "/people/me/connections",
58
+ query,
55
59
  );
56
60
  }
57
61
 
@@ -60,12 +64,8 @@ export async function searchContacts(
60
64
  connection: OAuthConnection,
61
65
  query: string,
62
66
  ): Promise<PeopleSearchResponse> {
63
- const params = new URLSearchParams({
67
+ return request<PeopleSearchResponse>(connection, "/people:searchContacts", {
64
68
  query,
65
69
  readMask: PERSON_FIELDS,
66
70
  });
67
- return request<PeopleSearchResponse>(
68
- connection,
69
- `/people:searchContacts?${params}`,
70
- );
71
71
  }
@@ -6,14 +6,16 @@
6
6
  * with OAuth tokens, Telegram delivery is proxied through the gateway which
7
7
  * owns the bot token and handles Telegram API retries.
8
8
  *
9
- * The `token` parameter in MessagingProvider methods is unused for Telegram
10
- * because delivery is authenticated via the gateway's bearer token, not
11
- * a per-user OAuth token.
9
+ * The `connectionOrToken` parameter in MessagingProvider methods is unused
10
+ * for Telegram because delivery is authenticated via the gateway's bearer
11
+ * token, not a per-user OAuth token.
12
12
  */
13
13
 
14
14
  import { getGatewayInternalBaseUrl } from "../../../config/env.js";
15
15
  import { getOrCreateConversation } from "../../../memory/conversation-key-store.js";
16
16
  import * as externalConversationStore from "../../../memory/external-conversation-store.js";
17
+ import type { OAuthConnection } from "../../../oauth/connection.js";
18
+ import { getConnectionByProvider } from "../../../oauth/oauth-store.js";
17
19
  import { mintDaemonDeliveryToken } from "../../../runtime/auth/token-service.js";
18
20
  import { credentialKey } from "../../../security/credential-key.js";
19
21
  import { getSecureKey } from "../../../security/secure-keys.js";
@@ -31,7 +33,7 @@ import type {
31
33
  } from "../../provider-types.js";
32
34
  import * as telegram from "./client.js";
33
35
 
34
- /** Resolve the gateway base URL, preferring GATEWAY_INTERNAL_BASE_URL if set. */
36
+ /** Resolve the gateway base URL. */
35
37
  function getGatewayUrl(): string {
36
38
  return getGatewayInternalBaseUrl();
37
39
  }
@@ -53,23 +55,21 @@ export const telegramBotMessagingProvider: MessagingProvider = {
53
55
  capabilities: new Set(["send"]),
54
56
 
55
57
  /**
56
- * Custom connectivity check. The standard registry check looks for
57
- * credential/telegram/access_token, but the Telegram bot token is
58
- * stored as credential/telegram/bot_token. This method lets the
59
- * registry detect that Telegram credentials exist.
58
+ * Custom connectivity check using the oauth_connection record as the
59
+ * single source of truth, consistent with integration-status.ts.
60
60
  *
61
61
  * Both bot_token and webhook_secret are required — the gateway's
62
62
  * /deliver/telegram endpoint rejects requests without the webhook
63
63
  * secret, so partial credentials would cause every send to fail.
64
64
  */
65
65
  isConnected(): boolean {
66
- return (
67
- getBotToken() !== undefined &&
68
- !!getSecureKey(credentialKey("telegram", "webhook_secret"))
69
- );
66
+ const conn = getConnectionByProvider("telegram");
67
+ return !!(conn && conn.status === "active");
70
68
  },
71
69
 
72
- async testConnection(_token: string): Promise<ConnectionInfo> {
70
+ async testConnection(
71
+ _connectionOrToken: OAuthConnection | string,
72
+ ): Promise<ConnectionInfo> {
73
73
  const botToken = getBotToken();
74
74
  if (!botToken) {
75
75
  return {
@@ -114,7 +114,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
114
114
  },
115
115
 
116
116
  async sendMessage(
117
- _token: string,
117
+ _connectionOrToken: OAuthConnection | string,
118
118
  conversationId: string,
119
119
  text: string,
120
120
  _options?: SendOptions,
@@ -152,7 +152,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
152
152
  // interact with chats where users have initiated contact or the bot
153
153
  // has been added to a group.
154
154
  async listConversations(
155
- _token: string,
155
+ _connectionOrToken: OAuthConnection | string,
156
156
  _options?: ListOptions,
157
157
  ): Promise<Conversation[]> {
158
158
  return [];
@@ -160,7 +160,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
160
160
 
161
161
  // Telegram Bot API does not provide message history retrieval.
162
162
  async getHistory(
163
- _token: string,
163
+ _connectionOrToken: OAuthConnection | string,
164
164
  _conversationId: string,
165
165
  _options?: HistoryOptions,
166
166
  ): Promise<Message[]> {
@@ -169,7 +169,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
169
169
 
170
170
  // Telegram Bot API does not support message search.
171
171
  async search(
172
- _token: string,
172
+ _connectionOrToken: OAuthConnection | string,
173
173
  _query: string,
174
174
  _options?: SearchOptions,
175
175
  ): Promise<SearchResult> {
@@ -5,14 +5,15 @@
5
5
  * endpoint. Delivery is proxied through the gateway which owns the Meta Cloud API
6
6
  * credentials (phone_number_id + access_token).
7
7
  *
8
- * The `token` parameter in MessagingProvider methods is unused for WhatsApp
9
- * because delivery is authenticated via the gateway's bearer token, not
10
- * a per-user OAuth token.
8
+ * The `connectionOrToken` parameter in MessagingProvider methods is unused
9
+ * for WhatsApp because delivery is authenticated via the gateway's bearer
10
+ * token, not a per-user OAuth token.
11
11
  */
12
12
 
13
13
  import { getGatewayInternalBaseUrl } from "../../../config/env.js";
14
14
  import { getOrCreateConversation } from "../../../memory/conversation-key-store.js";
15
15
  import * as externalConversationStore from "../../../memory/external-conversation-store.js";
16
+ import type { OAuthConnection } from "../../../oauth/connection.js";
16
17
  import { mintDaemonDeliveryToken } from "../../../runtime/auth/token-service.js";
17
18
  import { credentialKey } from "../../../security/credential-key.js";
18
19
  import { getSecureKey } from "../../../security/secure-keys.js";
@@ -61,7 +62,9 @@ export const whatsappMessagingProvider: MessagingProvider = {
61
62
  return hasWhatsAppCredentials();
62
63
  },
63
64
 
64
- async testConnection(_token: string): Promise<ConnectionInfo> {
65
+ async testConnection(
66
+ _connectionOrToken: OAuthConnection | string,
67
+ ): Promise<ConnectionInfo> {
65
68
  if (!hasWhatsAppCredentials()) {
66
69
  return {
67
70
  connected: false,
@@ -89,7 +92,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
89
92
  },
90
93
 
91
94
  async sendMessage(
92
- _token: string,
95
+ _connectionOrToken: OAuthConnection | string,
93
96
  conversationId: string,
94
97
  text: string,
95
98
  options?: SendOptions,
@@ -133,7 +136,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
133
136
 
134
137
  // WhatsApp does not support listing conversations via this provider.
135
138
  async listConversations(
136
- _token: string,
139
+ _connectionOrToken: OAuthConnection | string,
137
140
  _options?: ListOptions,
138
141
  ): Promise<Conversation[]> {
139
142
  return [];
@@ -141,7 +144,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
141
144
 
142
145
  // WhatsApp does not provide message history retrieval via the gateway.
143
146
  async getHistory(
144
- _token: string,
147
+ _connectionOrToken: OAuthConnection | string,
145
148
  _conversationId: string,
146
149
  _options?: HistoryOptions,
147
150
  ): Promise<Message[]> {
@@ -150,7 +153,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
150
153
 
151
154
  // WhatsApp does not support message search.
152
155
  async search(
153
- _token: string,
156
+ _connectionOrToken: OAuthConnection | string,
154
157
  _query: string,
155
158
  _options?: SearchOptions,
156
159
  ): Promise<SearchResult> {
@@ -2,21 +2,9 @@
2
2
  * Messaging provider registry — register/lookup providers by platform ID.
3
3
  */
4
4
 
5
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
6
- import { getConfig } from "../config/loader.js";
7
- import { credentialKey } from "../security/credential-key.js";
8
- import { getSecureKey } from "../security/secure-keys.js";
5
+ import { isProviderConnected } from "../oauth/oauth-store.js";
9
6
  import type { MessagingProvider } from "./provider.js";
10
7
 
11
- /**
12
- * Per-platform feature flag keys. Platforms not listed here are allowed
13
- * by default (undeclared keys resolve to `true`).
14
- */
15
- const PLATFORM_FLAG_KEYS: Record<string, string> = {
16
- gmail: "feature_flags.messaging.gmail.enabled",
17
- telegram: "feature_flags.messaging.telegram.enabled",
18
- };
19
-
20
8
  const providers = new Map<string, MessagingProvider>();
21
9
 
22
10
  export function registerMessagingProvider(provider: MessagingProvider): void {
@@ -31,32 +19,14 @@ export function getMessagingProvider(id: string): MessagingProvider {
31
19
  `Messaging provider "${id}" not found. Available: ${available}`,
32
20
  );
33
21
  }
34
- assertPlatformEnabled(id);
35
22
  return provider;
36
23
  }
37
24
 
38
- export function isPlatformEnabled(platformId: string): boolean {
39
- const flagKey = PLATFORM_FLAG_KEYS[platformId];
40
- if (!flagKey) return true;
41
- return isAssistantFeatureFlagEnabled(flagKey, getConfig());
42
- }
43
-
44
- function assertPlatformEnabled(platformId: string): void {
45
- if (!isPlatformEnabled(platformId)) {
46
- throw new Error(
47
- `The ${platformId} platform is not enabled. Enable it in Settings > Features.`,
48
- );
49
- }
50
- }
51
-
52
25
  /** Return all registered providers that have stored credentials. */
53
26
  export function getConnectedProviders(): MessagingProvider[] {
54
27
  return Array.from(providers.values()).filter((p) => {
55
28
  if (p.isConnected) return p.isConnected();
56
- const token = getSecureKey(
57
- credentialKey(p.credentialService, "access_token"),
58
- );
59
- return token !== undefined;
29
+ return isProviderConnected(p.credentialService);
60
30
  });
61
31
  }
62
32
 
@@ -351,11 +351,6 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
351
351
  title: "Voice Response",
352
352
  body: str(payload.preview, "A voice response is ready"),
353
353
  }),
354
-
355
- "ride_shotgun.invitation": (payload) => ({
356
- title: "Ride Shotgun",
357
- body: str(payload.message, "You have been invited to ride shotgun"),
358
- }),
359
354
  };
360
355
 
361
356
  /**
@@ -37,7 +37,10 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
37
37
  id: "user.send_notification",
38
38
  description: "User-initiated notification via assistant tool",
39
39
  },
40
- { id: "schedule.notify", description: "Scheduled notification triggered (one-shot or recurring)" },
40
+ {
41
+ id: "schedule.notify",
42
+ description: "Scheduled notification triggered (one-shot or recurring)",
43
+ },
41
44
  { id: "schedule.complete", description: "Scheduled task finished running" },
42
45
  {
43
46
  id: "guardian.question",
@@ -89,10 +92,6 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
89
92
  id: "voice.response_ready",
90
93
  description: "Voice response ready for playback",
91
94
  },
92
- {
93
- id: "ride_shotgun.invitation",
94
- description: "Invitation to ride shotgun on a session",
95
- },
96
95
  ] as const;
97
96
 
98
97
  export type NotificationSourceEventName =
@@ -63,14 +63,49 @@ mock.module("../security/oauth2.js", () => {
63
63
  };
64
64
  });
65
65
 
66
+ // ---------------------------------------------------------------------------
67
+ // Mock oauth-store — token-manager reads refresh config from SQLite
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /** Mutable per-test map of provider connections for getConnectionByProvider */
71
+ const mockConnections = new Map<
72
+ string,
73
+ {
74
+ id: string;
75
+ providerKey: string;
76
+ oauthAppId: string;
77
+ expiresAt: number | null;
78
+ grantedScopes?: string;
79
+ accountInfo?: string | null;
80
+ }
81
+ >();
82
+ const mockApps = new Map<
83
+ string,
84
+ { id: string; providerKey: string; clientId: string }
85
+ >();
86
+ const mockProviders = new Map<
87
+ string,
88
+ {
89
+ key: string;
90
+ tokenUrl: string;
91
+ tokenEndpointAuthMethod?: string;
92
+ baseUrl?: string;
93
+ }
94
+ >();
95
+
96
+ mock.module("./oauth-store.js", () => ({
97
+ getConnectionByProvider: (service: string) => mockConnections.get(service),
98
+ getApp: (id: string) => mockApps.get(id),
99
+ getProvider: (key: string) => mockProviders.get(key),
100
+ updateConnection: () => {},
101
+ getMostRecentAppByProvider: () => undefined,
102
+ listConnections: () => [],
103
+ }));
104
+
66
105
  // ---------------------------------------------------------------------------
67
106
  // Imports (after mocks)
68
107
  // ---------------------------------------------------------------------------
69
108
 
70
- import {
71
- _resetMigrationFlag,
72
- credentialKey,
73
- } from "../security/credential-key.js";
74
109
  import { setSecureKey } from "../security/secure-keys.js";
75
110
  import {
76
111
  _resetInflightRefreshes,
@@ -88,7 +123,6 @@ import { resolveOAuthConnection } from "./connection-resolver.js";
88
123
  // ---------------------------------------------------------------------------
89
124
 
90
125
  const originalFetch = globalThis.fetch;
91
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
126
  let mockFetch: ReturnType<typeof mock<any>>;
93
127
 
94
128
  // ---------------------------------------------------------------------------
@@ -110,7 +144,10 @@ beforeEach(() => {
110
144
  _resetBackend();
111
145
  _resetRefreshBreakers();
112
146
  _resetInflightRefreshes();
113
- _resetMigrationFlag();
147
+ // Clear mock oauth-store maps
148
+ mockConnections.clear();
149
+ mockApps.clear();
150
+ mockProviders.clear();
114
151
 
115
152
  // Default mock fetch returning 200 JSON
116
153
  mockFetch = mock(() =>
@@ -139,16 +176,40 @@ function setupCredential(
139
176
  service: string,
140
177
  opts?: { expiresAt?: number; grantedScopes?: string[] },
141
178
  ) {
142
- setSecureKey(credentialKey(service, "access_token"), "test-access-token");
143
- setSecureKey(credentialKey(service, "refresh_token"), "test-refresh-token");
144
- setSecureKey(credentialKey(service, "client_secret"), "test-client-secret");
145
- upsertCredentialMetadata(service, "access_token", {
179
+ // Seed mock oauth-store maps so token-manager can resolve refresh config
180
+ const appId = `app-${service}`;
181
+ const connId = `conn-${service}`;
182
+ mockProviders.set(service, {
183
+ key: service,
184
+ tokenUrl: "https://oauth2.googleapis.com/token",
185
+ // Only well-known providers (gmail) have a baseUrl; custom services don't
186
+ baseUrl:
187
+ service === "integration:gmail"
188
+ ? "https://gmail.googleapis.com/gmail/v1/users/me"
189
+ : undefined,
190
+ });
191
+ mockApps.set(appId, {
192
+ id: appId,
193
+ providerKey: service,
194
+ clientId: "test-client-id",
195
+ });
196
+ mockConnections.set(service, {
197
+ id: connId,
198
+ providerKey: service,
199
+ oauthAppId: appId,
146
200
  expiresAt: opts?.expiresAt ?? Date.now() + 3600 * 1000,
147
- grantedScopes: opts?.grantedScopes ?? ["read", "write"],
148
- oauth2TokenUrl: "https://oauth2.googleapis.com/token",
149
- oauth2ClientId: "test-client-id",
150
- hasRefreshToken: true,
201
+ grantedScopes: JSON.stringify(opts?.grantedScopes ?? ["read", "write"]),
202
+ accountInfo: null,
151
203
  });
204
+ // Store access token in oauth-store key format
205
+ setSecureKey(`oauth_connection/${connId}/access_token`, "test-access-token");
206
+ // Store refresh token and client_secret in secure keys (token-manager reads them)
207
+ setSecureKey(
208
+ `oauth_connection/${connId}/refresh_token`,
209
+ "test-refresh-token",
210
+ );
211
+ setSecureKey(`oauth_app/${appId}/client_secret`, "test-client-secret");
212
+ upsertCredentialMetadata(service, "access_token", {});
152
213
  }
153
214
 
154
215
  function createConnection(service = "integration:gmail"): BYOOAuthConnection {
@@ -185,10 +246,10 @@ describe("BYOOAuthConnection", () => {
185
246
  expect(url).toBe(
186
247
  "https://gmail.googleapis.com/gmail/v1/users/me/messages",
187
248
  );
188
- expect((init as RequestInit).headers).toMatchObject({
189
- Authorization: "Bearer test-access-token",
190
- "Content-Type": "application/json",
191
- });
249
+ const headers = (init as RequestInit).headers as Headers;
250
+ expect(headers.get("Authorization")).toBe("Bearer test-access-token");
251
+ // GET requests have no body, so Content-Type should not be set
252
+ expect(headers.has("Content-Type")).toBe(false);
192
253
  expect((init as RequestInit).method).toBe("GET");
193
254
  });
194
255
 
@@ -237,6 +298,9 @@ describe("BYOOAuthConnection", () => {
237
298
  JSON.stringify({ raw: "base64-encoded-email" }),
238
299
  );
239
300
  expect((init as RequestInit).method).toBe("POST");
301
+ // POST requests with a body should include Content-Type
302
+ const headers = (init as RequestInit).headers as Headers;
303
+ expect(headers.get("Content-Type")).toBe("application/json");
240
304
  });
241
305
 
242
306
  test("retries once on 401 response", async () => {
@@ -336,10 +400,9 @@ describe("BYOOAuthConnection", () => {
336
400
  });
337
401
 
338
402
  const [, init] = mockFetch.mock.calls[0];
339
- expect((init as RequestInit).headers).toMatchObject({
340
- "X-Custom-Header": "custom-value",
341
- Authorization: "Bearer test-access-token",
342
- });
403
+ const headers = (init as RequestInit).headers as Headers;
404
+ expect(headers.get("X-Custom-Header")).toBe("custom-value");
405
+ expect(headers.get("Authorization")).toBe("Bearer test-access-token");
343
406
  });
344
407
  });
345
408
 
@@ -361,9 +424,10 @@ describe("BYOOAuthConnection", () => {
361
424
 
362
425
  // The request should use the refreshed token
363
426
  const [, init] = mockFetch.mock.calls[0];
364
- expect((init as RequestInit).headers).toMatchObject({
365
- Authorization: "Bearer refreshed-access-token",
366
- });
427
+ const headers = (init as RequestInit).headers as Headers;
428
+ expect(headers.get("Authorization")).toBe(
429
+ "Bearer refreshed-access-token",
430
+ );
367
431
  });
368
432
  });
369
433
 
@@ -433,4 +497,41 @@ describe("resolveOAuthConnection", () => {
433
497
  /No base URL configured for "integration:custom-service"/,
434
498
  );
435
499
  });
500
+
501
+ test("resolves base URL via app's canonical providerKey for custom credential_service", () => {
502
+ // Set up a well-known provider with a baseUrl
503
+ mockProviders.set("github", {
504
+ key: "github",
505
+ tokenUrl: "https://github.com/login/oauth/access_token",
506
+ baseUrl: "https://api.github.com",
507
+ });
508
+ // The custom credential service has no provider entry of its own
509
+ // (getProvider("integration:github-work") returns undefined)
510
+
511
+ // App points to the canonical "github" provider
512
+ const appId = "app-github-work";
513
+ mockApps.set(appId, {
514
+ id: appId,
515
+ providerKey: "github",
516
+ clientId: "test-client-id",
517
+ });
518
+
519
+ // Connection uses the custom credential service as its providerKey
520
+ const connId = "conn-github-work";
521
+ mockConnections.set("integration:github-work", {
522
+ id: connId,
523
+ providerKey: "integration:github-work",
524
+ oauthAppId: appId,
525
+ expiresAt: Date.now() + 3600 * 1000,
526
+ grantedScopes: JSON.stringify(["repo"]),
527
+ accountInfo: null,
528
+ });
529
+ setSecureKey(`oauth_connection/${connId}/access_token`, "ghp-test-token");
530
+
531
+ const conn = resolveOAuthConnection("integration:github-work");
532
+
533
+ expect(conn).toBeInstanceOf(BYOOAuthConnection);
534
+ expect(conn.providerKey).toBe("integration:github-work");
535
+ expect(conn.grantedScopes).toEqual(["repo"]);
536
+ });
436
537
  });