@vellumai/assistant 0.5.11 → 0.5.13

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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -32,8 +32,30 @@ import type {
32
32
  // Cache user display names to avoid repeated API calls within a session
33
33
  const userNameCache = new Map<string, string>();
34
34
 
35
+ /**
36
+ * Cached auth resolved during resolveConnection().
37
+ *
38
+ * For Socket Mode this holds a raw bot token string; for OAuth it holds the
39
+ * OAuthConnection. The Slack client functions accept both via their own
40
+ * OAuthConnection | string union, so we can pass this value through directly.
41
+ */
42
+ let _cachedSlackAuth: OAuthConnection | string | null = null;
43
+
44
+ /**
45
+ * Get the Slack auth value to pass to Slack client functions.
46
+ * Prefers the explicit connection from the caller; falls back to the cached
47
+ * value set during resolveConnection().
48
+ */
49
+ function getSlackAuth(connection?: OAuthConnection): OAuthConnection | string {
50
+ if (connection) return connection;
51
+ if (_cachedSlackAuth) return _cachedSlackAuth;
52
+ throw new Error(
53
+ "Slack: no connection or cached token available. Was resolveConnection() called?",
54
+ );
55
+ }
56
+
35
57
  async function resolveUserName(
36
- connectionOrToken: OAuthConnection | string,
58
+ auth: OAuthConnection | string,
37
59
  userId: string,
38
60
  ): Promise<string> {
39
61
  if (!userId) return "unknown";
@@ -41,7 +63,7 @@ async function resolveUserName(
41
63
  if (cached) return cached;
42
64
 
43
65
  try {
44
- const resp = await slack.userInfo(connectionOrToken, userId);
66
+ const resp = await slack.userInfo(auth, userId);
45
67
  const name =
46
68
  resp.user.profile?.display_name ||
47
69
  resp.user.profile?.real_name ||
@@ -113,7 +135,7 @@ function mapSearchMatch(match: SlackSearchMatch): Message {
113
135
  export const slackProvider: MessagingProvider = {
114
136
  id: "slack",
115
137
  displayName: "Slack",
116
- credentialService: "integration:slack",
138
+ credentialService: "slack",
117
139
  capabilities: new Set(["reactions", "threads", "leave_channel"]),
118
140
 
119
141
  async isConnected(): Promise<boolean> {
@@ -124,25 +146,31 @@ export const slackProvider: MessagingProvider = {
124
146
  credentialKey("slack_channel", "bot_token"),
125
147
  );
126
148
  if (botToken) return true;
127
- // Preserve existing OAuth path (integration:slack) for backwards compat.
128
- return isProviderConnected("integration:slack");
149
+ // Preserve existing OAuth path for backwards compat.
150
+ return isProviderConnected("slack");
129
151
  },
130
152
 
131
- async resolveConnection(account?: string): Promise<OAuthConnection | string> {
132
- // Socket Mode: return raw bot token if available.
153
+ async resolveConnection(
154
+ account?: string,
155
+ ): Promise<OAuthConnection | undefined> {
156
+ // Socket Mode: cache the raw bot token for use in adapter methods.
133
157
  // Token presence is sufficient — no connection row required.
134
158
  const botToken = await getSecureKeyAsync(
135
159
  credentialKey("slack_channel", "bot_token"),
136
160
  );
137
- if (botToken) return botToken;
138
- // Preserve existing OAuth path (integration:slack) for backwards compat.
139
- return resolveOAuthConnection("integration:slack", { account });
161
+ if (botToken) {
162
+ _cachedSlackAuth = botToken;
163
+ return undefined;
164
+ }
165
+ // Preserve existing OAuth path for backwards compat.
166
+ const conn = await resolveOAuthConnection("slack", { account });
167
+ _cachedSlackAuth = conn;
168
+ return conn;
140
169
  },
141
170
 
142
- async testConnection(
143
- connectionOrToken: OAuthConnection | string,
144
- ): Promise<ConnectionInfo> {
145
- const resp = await slack.authTest(connectionOrToken);
171
+ async testConnection(connection?: OAuthConnection): Promise<ConnectionInfo> {
172
+ const auth = getSlackAuth(connection);
173
+ const resp = await slack.authTest(auth);
146
174
  return {
147
175
  connected: true,
148
176
  user: resp.user,
@@ -152,9 +180,10 @@ export const slackProvider: MessagingProvider = {
152
180
  },
153
181
 
154
182
  async listConversations(
155
- connectionOrToken: OAuthConnection | string,
183
+ connection: OAuthConnection | undefined,
156
184
  options?: ListOptions,
157
185
  ): Promise<Conversation[]> {
186
+ const auth = getSlackAuth(connection);
158
187
  const typeMap: Record<string, string> = {
159
188
  channel: "public_channel,private_channel",
160
189
  dm: "im",
@@ -174,7 +203,7 @@ export const slackProvider: MessagingProvider = {
174
203
  // Paginate through all results
175
204
  do {
176
205
  const resp = await slack.listConversations(
177
- connectionOrToken,
206
+ auth,
178
207
  types,
179
208
  options?.excludeArchived ?? true,
180
209
  options?.limit ?? 200,
@@ -191,7 +220,7 @@ export const slackProvider: MessagingProvider = {
191
220
  for (const conv of conversations) {
192
221
  if (conv.type === "dm" && conv.metadata?.dmUserId) {
193
222
  conv.name = await resolveUserName(
194
- connectionOrToken,
223
+ auth,
195
224
  conv.metadata.dmUserId as string,
196
225
  );
197
226
  }
@@ -201,12 +230,13 @@ export const slackProvider: MessagingProvider = {
201
230
  },
202
231
 
203
232
  async getHistory(
204
- connectionOrToken: OAuthConnection | string,
233
+ connection: OAuthConnection | undefined,
205
234
  conversationId: string,
206
235
  options?: HistoryOptions,
207
236
  ): Promise<Message[]> {
237
+ const auth = getSlackAuth(connection);
208
238
  const resp = await slack.conversationHistory(
209
- connectionOrToken,
239
+ auth,
210
240
  conversationId,
211
241
  options?.limit ?? 50,
212
242
  options?.before,
@@ -215,7 +245,7 @@ export const slackProvider: MessagingProvider = {
215
245
 
216
246
  const messages: Message[] = [];
217
247
  for (const msg of resp.messages) {
218
- const name = await resolveUserName(connectionOrToken, msg.user ?? "");
248
+ const name = await resolveUserName(auth, msg.user ?? "");
219
249
  messages.push(mapMessage(msg, conversationId, name));
220
250
  }
221
251
 
@@ -223,15 +253,12 @@ export const slackProvider: MessagingProvider = {
223
253
  },
224
254
 
225
255
  async search(
226
- connectionOrToken: OAuthConnection | string,
256
+ connection: OAuthConnection | undefined,
227
257
  query: string,
228
258
  options?: SearchOptions,
229
259
  ): Promise<SearchResult> {
230
- const resp = await slack.searchMessages(
231
- connectionOrToken,
232
- query,
233
- options?.count ?? 20,
234
- );
260
+ const auth = getSlackAuth(connection);
261
+ const resp = await slack.searchMessages(auth, query, options?.count ?? 20);
235
262
  return {
236
263
  total: resp.messages.total,
237
264
  messages: resp.messages.matches.map(mapSearchMatch),
@@ -240,13 +267,14 @@ export const slackProvider: MessagingProvider = {
240
267
  },
241
268
 
242
269
  async sendMessage(
243
- connectionOrToken: OAuthConnection | string,
270
+ connection: OAuthConnection | undefined,
244
271
  conversationId: string,
245
272
  text: string,
246
273
  options?: SendOptions,
247
274
  ): Promise<SendResult> {
275
+ const auth = getSlackAuth(connection);
248
276
  const resp = await slack.postMessage(
249
- connectionOrToken,
277
+ auth,
250
278
  conversationId,
251
279
  text,
252
280
  options?.threadId,
@@ -259,32 +287,34 @@ export const slackProvider: MessagingProvider = {
259
287
  },
260
288
 
261
289
  async getThreadReplies(
262
- connectionOrToken: OAuthConnection | string,
290
+ connection: OAuthConnection | undefined,
263
291
  conversationId: string,
264
292
  threadId: string,
265
293
  options?: HistoryOptions,
266
294
  ): Promise<Message[]> {
295
+ const auth = getSlackAuth(connection);
267
296
  const resp = await slack.conversationReplies(
268
- connectionOrToken,
297
+ auth,
269
298
  conversationId,
270
299
  threadId,
271
300
  options?.limit ?? 50,
272
301
  );
273
302
  const messages: Message[] = [];
274
303
  for (const msg of resp.messages) {
275
- const name = await resolveUserName(connectionOrToken, msg.user ?? "");
304
+ const name = await resolveUserName(auth, msg.user ?? "");
276
305
  messages.push(mapMessage(msg, conversationId, name));
277
306
  }
278
307
  return messages;
279
308
  },
280
309
 
281
310
  async markRead(
282
- connectionOrToken: OAuthConnection | string,
311
+ connection: OAuthConnection | undefined,
283
312
  conversationId: string,
284
313
  messageId?: string,
285
314
  ): Promise<void> {
315
+ const auth = getSlackAuth(connection);
286
316
  // Slack's conversations.mark requires a timestamp — use the provided one or "now"
287
317
  const ts = messageId ?? String(Date.now() / 1000);
288
- await slack.conversationMark(connectionOrToken, conversationId, ts);
318
+ await slack.conversationMark(auth, conversationId, ts);
289
319
  },
290
320
  };
@@ -6,7 +6,7 @@
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 `connectionOrToken` parameter in MessagingProvider methods is unused
9
+ * The `connection` parameter in MessagingProvider methods is unused
10
10
  * for Telegram because delivery is authenticated via the gateway's bearer
11
11
  * token, not a per-user OAuth token.
12
12
  */
@@ -76,9 +76,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
76
76
  return !!webhookSecret;
77
77
  },
78
78
 
79
- async testConnection(
80
- _connectionOrToken: OAuthConnection | string,
81
- ): Promise<ConnectionInfo> {
79
+ async testConnection(_connection?: OAuthConnection): Promise<ConnectionInfo> {
82
80
  const botToken = await getBotToken();
83
81
  if (!botToken) {
84
82
  return {
@@ -123,7 +121,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
123
121
  },
124
122
 
125
123
  async sendMessage(
126
- _connectionOrToken: OAuthConnection | string,
124
+ _connection: OAuthConnection | undefined,
127
125
  conversationId: string,
128
126
  text: string,
129
127
  _options?: SendOptions,
@@ -161,7 +159,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
161
159
  // interact with chats where users have initiated contact or the bot
162
160
  // has been added to a group.
163
161
  async listConversations(
164
- _connectionOrToken: OAuthConnection | string,
162
+ _connection?: OAuthConnection,
165
163
  _options?: ListOptions,
166
164
  ): Promise<Conversation[]> {
167
165
  return [];
@@ -169,7 +167,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
169
167
 
170
168
  // Telegram Bot API does not provide message history retrieval.
171
169
  async getHistory(
172
- _connectionOrToken: OAuthConnection | string,
170
+ _connection: OAuthConnection | undefined,
173
171
  _conversationId: string,
174
172
  _options?: HistoryOptions,
175
173
  ): Promise<Message[]> {
@@ -178,7 +176,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
178
176
 
179
177
  // Telegram Bot API does not support message search.
180
178
  async search(
181
- _connectionOrToken: OAuthConnection | string,
179
+ _connection: OAuthConnection | undefined,
182
180
  _query: string,
183
181
  _options?: SearchOptions,
184
182
  ): Promise<SearchResult> {
@@ -5,7 +5,7 @@
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 `connectionOrToken` parameter in MessagingProvider methods is unused
8
+ * The `connection` parameter in MessagingProvider methods is unused
9
9
  * for WhatsApp because delivery is authenticated via the gateway's bearer
10
10
  * token, not a per-user OAuth token.
11
11
  */
@@ -66,9 +66,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
66
66
  return hasWhatsAppCredentials();
67
67
  },
68
68
 
69
- async testConnection(
70
- _connectionOrToken: OAuthConnection | string,
71
- ): Promise<ConnectionInfo> {
69
+ async testConnection(_connection?: OAuthConnection): Promise<ConnectionInfo> {
72
70
  if (!(await hasWhatsAppCredentials())) {
73
71
  return {
74
72
  connected: false,
@@ -96,7 +94,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
96
94
  },
97
95
 
98
96
  async sendMessage(
99
- _connectionOrToken: OAuthConnection | string,
97
+ _connection: OAuthConnection | undefined,
100
98
  conversationId: string,
101
99
  text: string,
102
100
  options?: SendOptions,
@@ -140,7 +138,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
140
138
 
141
139
  // WhatsApp does not support listing conversations via this provider.
142
140
  async listConversations(
143
- _connectionOrToken: OAuthConnection | string,
141
+ _connection?: OAuthConnection,
144
142
  _options?: ListOptions,
145
143
  ): Promise<Conversation[]> {
146
144
  return [];
@@ -148,7 +146,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
148
146
 
149
147
  // WhatsApp does not provide message history retrieval via the gateway.
150
148
  async getHistory(
151
- _connectionOrToken: OAuthConnection | string,
149
+ _connection: OAuthConnection | undefined,
152
150
  _conversationId: string,
153
151
  _options?: HistoryOptions,
154
152
  ): Promise<Message[]> {
@@ -157,7 +155,7 @@ export const whatsappMessagingProvider: MessagingProvider = {
157
155
 
158
156
  // WhatsApp does not support message search.
159
157
  async search(
160
- _connectionOrToken: OAuthConnection | string,
158
+ _connection: OAuthConnection | undefined,
161
159
  _query: string,
162
160
  _options?: SearchOptions,
163
161
  ): Promise<SearchResult> {
@@ -5,14 +5,20 @@
5
5
  * Follows the same delivery pattern used by guardian-dispatch: POST to
6
6
  * the gateway's `/deliver/telegram` endpoint with a chat ID and text
7
7
  * payload. The gateway forwards the message to the Telegram Bot API.
8
+ *
9
+ * For access request notifications, inline keyboard buttons ("Approve once",
10
+ * "Reject") are attached via the approval payload so the guardian can act
11
+ * without typing a command. If the rich delivery fails, the adapter falls
12
+ * back to plain text with typed-command instructions.
8
13
  */
9
14
 
10
15
  import { getGatewayInternalBaseUrl } from "../../config/env.js";
11
16
  import { mintDaemonDeliveryToken } from "../../runtime/auth/token-service.js";
17
+ import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
12
18
  import { deliverChannelReply } from "../../runtime/gateway-client.js";
13
19
  import { getLogger } from "../../util/logger.js";
14
20
  import { isConversationSeedSane } from "../conversation-seed-composer.js";
15
- import { nonEmpty } from "../copy-composer.js";
21
+ import { buildAccessRequestContractText, nonEmpty } from "../copy-composer.js";
16
22
  import type {
17
23
  ChannelAdapter,
18
24
  ChannelDeliveryPayload,
@@ -40,6 +46,34 @@ function resolveTelegramMessageText(payload: ChannelDeliveryPayload): string {
40
46
  return payload.sourceEventName.replace(/[._]/g, " ");
41
47
  }
42
48
 
49
+ /**
50
+ * Build an {@link ApprovalUIMetadata} for an access request so the gateway
51
+ * renders inline keyboard buttons in the Telegram message.
52
+ *
53
+ * Returns `undefined` when the context payload is missing the required
54
+ * `requestId`, in which case the caller should fall back to plain text.
55
+ */
56
+ function buildAccessRequestApproval(
57
+ contextPayload: Record<string, unknown>,
58
+ ): ApprovalUIMetadata | undefined {
59
+ const requestId =
60
+ typeof contextPayload.requestId === "string"
61
+ ? contextPayload.requestId
62
+ : undefined;
63
+ if (!requestId) return undefined;
64
+
65
+ const plainTextFallback = buildAccessRequestContractText(contextPayload);
66
+
67
+ return {
68
+ requestId,
69
+ actions: [
70
+ { id: "approve_once", label: "Approve once" },
71
+ { id: "reject", label: "Reject" },
72
+ ],
73
+ plainTextFallback,
74
+ };
75
+ }
76
+
43
77
  export class TelegramAdapter implements ChannelAdapter {
44
78
  readonly channel: NotificationChannel = "telegram";
45
79
 
@@ -66,10 +100,52 @@ export class TelegramAdapter implements ChannelAdapter {
66
100
  // delivery copy when available and avoid deterministic label prefixes.
67
101
  const messageText = resolveTelegramMessageText(payload);
68
102
 
103
+ // For access requests, attach inline keyboard buttons so the guardian
104
+ // can approve/reject with a single tap.
105
+ const isAccessRequest =
106
+ payload.sourceEventName === "ingress.access_request" &&
107
+ payload.contextPayload != null;
108
+
109
+ const approval = isAccessRequest
110
+ ? buildAccessRequestApproval(payload.contextPayload!)
111
+ : undefined;
112
+
69
113
  try {
114
+ if (approval) {
115
+ // Attempt rich delivery with inline keyboard buttons.
116
+ // On failure, fall back to plain text below.
117
+ try {
118
+ await deliverChannelReply(
119
+ deliverUrl,
120
+ { chatId, text: messageText, approval },
121
+ mintDaemonDeliveryToken(),
122
+ );
123
+
124
+ log.info(
125
+ { sourceEventName: payload.sourceEventName, chatId },
126
+ "Telegram access request notification delivered with inline buttons",
127
+ );
128
+
129
+ return { success: true };
130
+ } catch (richErr) {
131
+ log.warn(
132
+ { err: richErr, sourceEventName: payload.sourceEventName, chatId },
133
+ "Rich Telegram delivery failed — falling back to plain text",
134
+ );
135
+ }
136
+ }
137
+
138
+ // When falling back from rich delivery, append the plain-text
139
+ // instructions so the guardian still knows how to approve/reject.
140
+ const fallbackText =
141
+ approval?.plainTextFallback &&
142
+ !messageText.includes(approval.plainTextFallback)
143
+ ? `${messageText}\n\n${approval.plainTextFallback}`
144
+ : messageText;
145
+
70
146
  await deliverChannelReply(
71
147
  deliverUrl,
72
- { chatId, text: messageText },
148
+ { chatId, text: fallbackText },
73
149
  mintDaemonDeliveryToken(),
74
150
  );
75
151