@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
@@ -532,11 +532,16 @@ export class RuntimeHttpServer {
532
532
  if (!isHttpAuthDisabled()) {
533
533
  const clientIp = extractClientIp(req, server);
534
534
  const token = extractBearerToken(req);
535
- const result = token
536
- ? apiRateLimiter.check(clientIp)
537
- : ipRateLimiter.check(clientIp);
535
+ const limiter = token ? apiRateLimiter : ipRateLimiter;
536
+ const limiterKind = token ? "authenticated" : "unauthenticated";
537
+ const result = limiter.check(clientIp, path);
538
538
  if (!result.allowed) {
539
- return rateLimitResponse(result);
539
+ return rateLimitResponse(result, {
540
+ clientIp,
541
+ deniedPath: path,
542
+ limiterKind: limiterKind as "authenticated" | "unauthenticated",
543
+ pathCounts: limiter.getRecentPathCounts(clientIp),
544
+ });
540
545
  }
541
546
  const routerResponse = await this.router.dispatch(
542
547
  endpoint,
@@ -83,7 +83,6 @@ export type GuardianActionCopyGenerator = (
83
83
  /** The disposition returned by the guardian follow-up conversation engine. */
84
84
  export type GuardianFollowUpDisposition =
85
85
  | "call_back"
86
- | "message_back"
87
86
  | "decline"
88
87
  | "keep_pending";
89
88
 
@@ -2,10 +2,13 @@
2
2
  // Tracks request counts per key and returns 429 when the limit is exceeded.
3
3
  // Follows the same sliding-window pattern as gateway/src/auth-rate-limiter.ts.
4
4
 
5
+ import { getLogger } from "../../util/logger.js";
5
6
  import type { HttpErrorResponse } from "../http-errors.js";
6
7
  import { isPrivateAddress } from "./auth.js";
7
8
 
8
- const DEFAULT_MAX_REQUESTS = 60;
9
+ const log = getLogger("rate-limiter");
10
+
11
+ const DEFAULT_MAX_REQUESTS = 300;
9
12
  const DEFAULT_WINDOW_MS = 60_000; // 60 seconds
10
13
  const MAX_TRACKED_TOKENS = 10_000;
11
14
 
@@ -14,8 +17,13 @@ const DEFAULT_IP_MAX_REQUESTS = 20;
14
17
  const DEFAULT_IP_WINDOW_MS = 60_000;
15
18
  const MAX_TRACKED_IPS = 50_000;
16
19
 
20
+ interface RequestEntry {
21
+ timestamp: number;
22
+ path: string;
23
+ }
24
+
17
25
  export class TokenRateLimiter {
18
- private requests = new Map<string, number[]>();
26
+ private requests = new Map<string, RequestEntry[]>();
19
27
  private readonly maxRequests: number;
20
28
  private readonly windowMs: number;
21
29
  private readonly maxTrackedKeys: number;
@@ -34,11 +42,11 @@ export class TokenRateLimiter {
34
42
  * Check whether the request should be allowed and record it.
35
43
  * Returns rate limit metadata for response headers.
36
44
  */
37
- check(key: string): RateLimitResult {
45
+ check(key: string, path?: string): RateLimitResult {
38
46
  const now = Date.now();
39
- let timestamps = this.requests.get(key);
47
+ let entries = this.requests.get(key);
40
48
 
41
- if (!timestamps) {
49
+ if (!entries) {
42
50
  if (this.requests.size >= this.maxTrackedKeys) {
43
51
  this.evictStale(now);
44
52
  if (this.requests.size >= this.maxTrackedKeys) {
@@ -46,24 +54,24 @@ export class TokenRateLimiter {
46
54
  if (oldest !== undefined) this.requests.delete(oldest);
47
55
  }
48
56
  }
49
- timestamps = [];
50
- this.requests.set(key, timestamps);
57
+ entries = [];
58
+ this.requests.set(key, entries);
51
59
  }
52
60
 
53
61
  const cutoff = now - this.windowMs;
54
62
 
55
- // Remove expired timestamps from the front
56
- while (timestamps.length > 0 && timestamps[0] <= cutoff) {
57
- timestamps.shift();
63
+ // Remove expired entries from the front
64
+ while (entries.length > 0 && entries[0].timestamp <= cutoff) {
65
+ entries.shift();
58
66
  }
59
67
 
60
- const remaining = Math.max(0, this.maxRequests - timestamps.length);
68
+ const remaining = Math.max(0, this.maxRequests - entries.length);
61
69
  const resetAt =
62
- timestamps.length > 0
63
- ? Math.ceil((timestamps[0] + this.windowMs) / 1000)
70
+ entries.length > 0
71
+ ? Math.ceil((entries[0].timestamp + this.windowMs) / 1000)
64
72
  : Math.ceil((now + this.windowMs) / 1000);
65
73
 
66
- if (timestamps.length >= this.maxRequests) {
74
+ if (entries.length >= this.maxRequests) {
67
75
  return {
68
76
  allowed: false,
69
77
  limit: this.maxRequests,
@@ -72,7 +80,7 @@ export class TokenRateLimiter {
72
80
  };
73
81
  }
74
82
 
75
- timestamps.push(now);
83
+ entries.push({ timestamp: now, path: path ?? "unknown" });
76
84
 
77
85
  return {
78
86
  allowed: true,
@@ -82,13 +90,36 @@ export class TokenRateLimiter {
82
90
  };
83
91
  }
84
92
 
93
+ /**
94
+ * Return a count of recent requests grouped by path for the given key.
95
+ * Sorted descending by count. Useful for diagnosing which endpoints
96
+ * are consuming the rate limit budget.
97
+ */
98
+ getRecentPathCounts(key: string): Array<{ path: string; count: number }> {
99
+ const entries = this.requests.get(key);
100
+ if (!entries || entries.length === 0) return [];
101
+
102
+ const now = Date.now();
103
+ const cutoff = now - this.windowMs;
104
+ const counts = new Map<string, number>();
105
+ for (const entry of entries) {
106
+ if (entry.timestamp > cutoff) {
107
+ counts.set(entry.path, (counts.get(entry.path) ?? 0) + 1);
108
+ }
109
+ }
110
+
111
+ return Array.from(counts.entries())
112
+ .map(([path, count]) => ({ path, count }))
113
+ .sort((a, b) => b.count - a.count);
114
+ }
115
+
85
116
  private evictStale(now: number): void {
86
117
  const cutoff = now - this.windowMs;
87
- for (const [key, timestamps] of this.requests) {
88
- while (timestamps.length > 0 && timestamps[0] <= cutoff) {
89
- timestamps.shift();
118
+ for (const [key, entries] of this.requests) {
119
+ while (entries.length > 0 && entries[0].timestamp <= cutoff) {
120
+ entries.shift();
90
121
  }
91
- if (timestamps.length === 0) {
122
+ if (entries.length === 0) {
92
123
  this.requests.delete(key);
93
124
  }
94
125
  }
@@ -115,8 +146,31 @@ export function rateLimitHeaders(
115
146
  }
116
147
 
117
148
  /** Return a 429 response with rate limit headers and a Retry-After hint. */
118
- export function rateLimitResponse(result: RateLimitResult): Response {
149
+ export function rateLimitResponse(
150
+ result: RateLimitResult,
151
+ diagnostics?: {
152
+ clientIp: string;
153
+ deniedPath: string;
154
+ limiterKind: "authenticated" | "unauthenticated";
155
+ pathCounts: Array<{ path: string; count: number }>;
156
+ },
157
+ ): Response {
119
158
  const retryAfter = Math.max(1, result.resetAt - Math.ceil(Date.now() / 1000));
159
+
160
+ if (diagnostics) {
161
+ log.warn(
162
+ {
163
+ clientIp: diagnostics.clientIp,
164
+ deniedPath: diagnostics.deniedPath,
165
+ limiterKind: diagnostics.limiterKind,
166
+ limit: result.limit,
167
+ retryAfterSec: retryAfter,
168
+ recentRequests: diagnostics.pathCounts,
169
+ },
170
+ `Rate limited ${diagnostics.limiterKind} request: ${diagnostics.deniedPath} (${result.limit} req/min exceeded)`,
171
+ );
172
+ }
173
+
120
174
  const body: HttpErrorResponse = {
121
175
  error: { code: "RATE_LIMITED", message: "Too Many Requests" },
122
176
  };
@@ -29,12 +29,11 @@ export const GATEWAY_SUBPATH_MAP: Record<string, string> = {
29
29
  voice: "voice-webhook",
30
30
  status: "status",
31
31
  "connect-action": "connect-action",
32
- sms: "sms",
33
32
  };
34
33
 
35
34
  /**
36
35
  * Direct Twilio webhook subpaths that are blocked in gateway_only mode.
37
- * Includes all public-facing webhook paths (voice, status, connect-action, sms)
36
+ * Includes all public-facing webhook paths (voice, status, connect-action)
38
37
  * because the runtime must never serve as a direct ingress for external webhooks.
39
38
  * Internal forwarding endpoints (gateway->runtime) are unaffected.
40
39
  */
@@ -42,7 +41,6 @@ export const GATEWAY_ONLY_BLOCKED_SUBPATHS = new Set([
42
41
  "voice-webhook",
43
42
  "status",
44
43
  "connect-action",
45
- "sms",
46
44
  ]);
47
45
 
48
46
  /**
@@ -38,6 +38,7 @@ export async function handleGetChannelReadiness(url: URL): Promise<Response> {
38
38
  return {
39
39
  channel: s.channel,
40
40
  ready: s.ready,
41
+ setupStatus: s.setupStatus,
41
42
  checkedAt: s.checkedAt,
42
43
  stale: s.stale,
43
44
  reasons: s.reasons,
@@ -91,6 +92,7 @@ export async function handleRefreshChannelReadiness(
91
92
  return {
92
93
  channel: s.channel,
93
94
  ready: s.ready,
95
+ setupStatus: s.setupStatus,
94
96
  checkedAt: s.checkedAt,
95
97
  stale: s.stale,
96
98
  reasons: s.reasons,
@@ -186,6 +186,7 @@ async function handleDiagnosticsExport(body: {
186
186
  // been persisted — the user message and in-flight tool/usage data are
187
187
  // still captured.
188
188
  let anchorMessage;
189
+ let anchorIsFallback = false;
189
190
  if (anchorMessageId) {
190
191
  anchorMessage = db
191
192
  .select()
@@ -220,6 +221,7 @@ async function handleDiagnosticsExport(body: {
220
221
  .orderBy(desc(messages.createdAt))
221
222
  .limit(1)
222
223
  .get();
224
+ anchorIsFallback = true;
223
225
  }
224
226
 
225
227
  // 2. Compute the export time range.
@@ -250,15 +252,15 @@ async function handleDiagnosticsExport(body: {
250
252
  rangeStart =
251
253
  precedingUserMessage?.createdAt ?? anchorMessage.createdAt - 2000;
252
254
 
253
- // When the anchor is not an assistant message (e.g. the fallback "any
254
- // message" path hit because the assistant reply hasn't been persisted
255
- // yet), extend the range to the current time so in-flight tool
256
- // invocations and usage recorded after the user message are captured.
257
- const anchorIsAssistant = anchorMessage.role === "assistant";
258
- rangeEnd = anchorIsAssistant ? anchorMessage.createdAt : now;
259
- usageRangeEnd = anchorIsAssistant
260
- ? anchorMessage.createdAt + 5000
261
- : now + 5000;
255
+ // When the anchor was selected via the fallback "any message" path
256
+ // (because the assistant reply hasn't been persisted yet), extend the
257
+ // range to the current time so in-flight tool invocations and usage
258
+ // recorded after the user message are captured. An explicit anchor to a
259
+ // non-assistant message uses the message's own timestamp.
260
+ rangeEnd = anchorIsFallback ? now : anchorMessage.createdAt;
261
+ usageRangeEnd = anchorIsFallback
262
+ ? now + 5000
263
+ : anchorMessage.createdAt + 5000;
262
264
  } else {
263
265
  // No messages at all — use the current time so we capture any
264
266
  // in-flight LLM usage or tool invocations.
@@ -10,6 +10,7 @@ import { applyGuardianDecision } from "../../approvals/guardian-decision-primiti
10
10
  import type { ChannelId } from "../../channels/types.js";
11
11
  import type { TrustContext } from "../../daemon/session-runtime-assembly.js";
12
12
  import {
13
+ getAllPendingApprovalsByGuardianChat,
13
14
  getPendingApprovalForRequest,
14
15
  getUnresolvedApprovalForRequest,
15
16
  updateApprovalDecision,
@@ -123,7 +124,7 @@ export async function handleApprovalInterception(
123
124
  // Only guardians can approve via reaction — non-guardian reactions are
124
125
  // silently ignored to prevent self-approval.
125
126
  if (callbackData?.startsWith("reaction:")) {
126
- if (trustCtx.trustClass !== "guardian") {
127
+ if (trustCtx.trustClass !== "guardian" || !actorExternalId) {
127
128
  return { handled: true, type: "stale_ignored" };
128
129
  }
129
130
  const reactionDecision = parseReactionCallbackData(callbackData);
@@ -131,13 +132,27 @@ export async function handleApprovalInterception(
131
132
  // Unknown emoji — ignore silently
132
133
  return { handled: true, type: "stale_ignored" };
133
134
  }
134
- const pending = getApprovalInfoByConversation(conversationId);
135
- if (pending.length === 0) {
135
+
136
+ const allPending = getAllPendingApprovalsByGuardianChat(
137
+ sourceChannel,
138
+ conversationExternalId,
139
+ );
140
+ const guardianPending = allPending.filter(
141
+ (approval) => approval.guardianExternalUserId === actorExternalId,
142
+ );
143
+ if (guardianPending.length !== 1) {
136
144
  return { handled: true, type: "stale_ignored" };
137
145
  }
138
- const result = handleChannelDecision(conversationId, reactionDecision);
146
+
147
+ const result = applyGuardianDecision({
148
+ approval: guardianPending[0],
149
+ decision: reactionDecision,
150
+ actorPrincipalId: undefined,
151
+ actorExternalUserId: actorExternalId,
152
+ actorChannel: sourceChannel,
153
+ });
139
154
  if (result.applied) {
140
- return { handled: true, type: "decision_applied" };
155
+ return { handled: true, type: "guardian_decision_applied" };
141
156
  }
142
157
  return { handled: true, type: "stale_ignored" };
143
158
  }
@@ -314,21 +314,35 @@ export async function enforceIngressAcl(
314
314
  );
315
315
  }
316
316
 
317
+ // DM the requester so they have a private channel to reply with
318
+ // the verification code. Sending to the Slack user ID (not
319
+ // conversationExternalId) auto-opens a DM conversation.
317
320
  if (replyCallbackUrl) {
321
+ const senderUserId = (canonicalSenderId ?? rawSenderId)!;
322
+ // Strip threadTs from the callback URL — it belongs to the
323
+ // originating channel thread and would cause errors in the DM.
324
+ let dmCallbackUrl = replyCallbackUrl;
325
+ try {
326
+ const url = new URL(replyCallbackUrl);
327
+ url.searchParams.delete("threadTs");
328
+ dmCallbackUrl = url.toString();
329
+ } catch {
330
+ // Malformed URL — use as-is
331
+ }
318
332
  try {
319
333
  await deliverChannelReply(
320
- replyCallbackUrl,
334
+ dmCallbackUrl,
321
335
  {
322
- chatId: conversationExternalId,
323
- text: "I've notified the owner. They'll share a verification code with you if they approve access.",
336
+ chatId: senderUserId,
337
+ text: "I've notified the owner. They'll share a verification code with you if they approve access. You can reply with the code here.",
324
338
  assistantId,
325
339
  },
326
340
  mintBearerToken(),
327
341
  );
328
342
  } catch (err) {
329
343
  log.error(
330
- { err, conversationExternalId },
331
- "Failed to deliver Slack verification prompt reply",
344
+ { err, senderUserId },
345
+ "Failed to deliver Slack verification DM to requester",
332
346
  );
333
347
  }
334
348
  }
@@ -371,14 +385,20 @@ export async function enforceIngressAcl(
371
385
  const replyText = guardianNotified
372
386
  ? "Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."
373
387
  : "Sorry, you haven't been approved to message this assistant.";
388
+ const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
389
+ chatId: conversationExternalId,
390
+ text: replyText,
391
+ assistantId,
392
+ };
393
+ // On Slack, send as ephemeral so only the requester sees the rejection
394
+ if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
395
+ replyPayload.ephemeral = true;
396
+ replyPayload.user = (canonicalSenderId ?? rawSenderId)!;
397
+ }
374
398
  try {
375
399
  await deliverChannelReply(
376
400
  replyCallbackUrl,
377
- {
378
- chatId: conversationExternalId,
379
- text: replyText,
380
- assistantId,
381
- },
401
+ replyPayload,
382
402
  mintBearerToken(),
383
403
  );
384
404
  } catch (err) {
@@ -543,21 +563,31 @@ export async function enforceIngressAcl(
543
563
  );
544
564
  }
545
565
 
566
+ // DM the requester (same as non-member path)
546
567
  if (replyCallbackUrl) {
568
+ const senderUserId = (canonicalSenderId ?? rawSenderId)!;
569
+ let dmCallbackUrl = replyCallbackUrl;
570
+ try {
571
+ const url = new URL(replyCallbackUrl);
572
+ url.searchParams.delete("threadTs");
573
+ dmCallbackUrl = url.toString();
574
+ } catch {
575
+ // Malformed URL — use as-is
576
+ }
547
577
  try {
548
578
  await deliverChannelReply(
549
- replyCallbackUrl,
579
+ dmCallbackUrl,
550
580
  {
551
- chatId: conversationExternalId,
552
- text: "I've notified the owner. They'll share a verification code with you if they approve access.",
581
+ chatId: senderUserId,
582
+ text: "I've notified the owner. They'll share a verification code with you if they approve access. You can reply with the code here.",
553
583
  assistantId,
554
584
  },
555
585
  mintBearerToken(),
556
586
  );
557
587
  } catch (err) {
558
588
  log.error(
559
- { err, conversationExternalId },
560
- "Failed to deliver Slack verification prompt reply (inactive member)",
589
+ { err, senderUserId },
590
+ "Failed to deliver Slack verification DM to requester (inactive member)",
561
591
  );
562
592
  }
563
593
  }
@@ -605,14 +635,25 @@ export async function enforceIngressAcl(
605
635
  const replyText = guardianNotified
606
636
  ? "Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."
607
637
  : "Sorry, you haven't been approved to message this assistant.";
638
+ const inactiveReplyPayload: Parameters<
639
+ typeof deliverChannelReply
640
+ >[1] = {
641
+ chatId: conversationExternalId,
642
+ text: replyText,
643
+ assistantId,
644
+ };
645
+ // On Slack, send as ephemeral so only the requester sees the rejection
646
+ if (
647
+ sourceChannel === "slack" &&
648
+ (canonicalSenderId ?? rawSenderId)
649
+ ) {
650
+ inactiveReplyPayload.ephemeral = true;
651
+ inactiveReplyPayload.user = (canonicalSenderId ?? rawSenderId)!;
652
+ }
608
653
  try {
609
654
  await deliverChannelReply(
610
655
  replyCallbackUrl,
611
- {
612
- chatId: conversationExternalId,
613
- text: replyText,
614
- assistantId,
615
- },
656
+ inactiveReplyPayload,
616
657
  mintBearerToken(),
617
658
  );
618
659
  } catch (err) {
@@ -640,14 +681,19 @@ export async function enforceIngressAcl(
640
681
  "Ingress ACL: member policy deny",
641
682
  );
642
683
  if (replyCallbackUrl) {
684
+ const denyPayload: Parameters<typeof deliverChannelReply>[1] = {
685
+ chatId: conversationExternalId,
686
+ text: "Sorry, you haven't been approved to message this assistant.",
687
+ assistantId,
688
+ };
689
+ if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
690
+ denyPayload.ephemeral = true;
691
+ denyPayload.user = (canonicalSenderId ?? rawSenderId)!;
692
+ }
643
693
  try {
644
694
  await deliverChannelReply(
645
695
  replyCallbackUrl,
646
- {
647
- chatId: conversationExternalId,
648
- text: "Sorry, you haven't been approved to message this assistant.",
649
- assistantId,
650
- },
696
+ denyPayload,
651
697
  mintBearerToken(),
652
698
  );
653
699
  } catch (err) {
@@ -149,14 +149,21 @@ export async function handleGuardianReplyIntercept(
149
149
  if (routerResult.consumed) {
150
150
  // Deliver reply text if the router produced one
151
151
  if (routerResult.replyText) {
152
+ const routerReplyPayload: Parameters<typeof deliverChannelReply>[1] = {
153
+ chatId: conversationExternalId,
154
+ text: routerResult.replyText,
155
+ assistantId: canonicalAssistantId,
156
+ };
157
+ // On Slack, send guardian management replies (disambiguation, pending
158
+ // request lists, etc.) as ephemeral so only the guardian sees them.
159
+ if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
160
+ routerReplyPayload.ephemeral = true;
161
+ routerReplyPayload.user = (canonicalSenderId ?? rawSenderId)!;
162
+ }
152
163
  try {
153
164
  await deliverChannelReply(
154
165
  replyCallbackUrl,
155
- {
156
- chatId: conversationExternalId,
157
- text: routerResult.replyText,
158
- assistantId: canonicalAssistantId,
159
- },
166
+ routerReplyPayload,
160
167
  mintBearerToken(),
161
168
  );
162
169
  } catch (err) {
@@ -12,6 +12,7 @@ import {
12
12
  userInfo,
13
13
  } from "../../../../messaging/providers/slack/client.js";
14
14
  import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
15
+ import { credentialKey } from "../../../../security/credential-key.js";
15
16
  import { getSecureKey } from "../../../../security/secure-keys.js";
16
17
  import { getLogger } from "../../../../util/logger.js";
17
18
  import { httpError } from "../../../http-errors.js";
@@ -29,8 +30,8 @@ const log = getLogger("slack-share");
29
30
  */
30
31
  function resolveSlackToken(): string | undefined {
31
32
  return (
32
- getSecureKey("credential:integration:slack:access_token") ??
33
- getSecureKey("credential:slack_channel:bot_token")
33
+ getSecureKey(credentialKey("integration:slack", "access_token")) ??
34
+ getSecureKey(credentialKey("slack_channel", "bot_token"))
34
35
  );
35
36
  }
36
37
 
@@ -21,6 +21,7 @@ import {
21
21
  import { loadRawConfig, saveRawConfig } from "../../../config/loader.js";
22
22
  import { syncTwilioWebhooks } from "../../../daemon/handlers/config-ingress.js";
23
23
  import type { IngressConfig } from "../../../inbound/public-ingress-urls.js";
24
+ import { credentialKey } from "../../../security/credential-key.js";
24
25
  import {
25
26
  deleteSecureKeyAsync,
26
27
  setSecureKeyAsync,
@@ -136,7 +137,7 @@ export async function handleSetTwilioCredentials(
136
137
  // validation (gateway/src/credential-reader.ts), while the assistant reads
137
138
  // from config via resolveAccountSid(). Both stores must stay in sync.
138
139
  const sidStored = await setSecureKeyAsync(
139
- "credential:twilio:account_sid",
140
+ credentialKey("twilio", "account_sid"),
140
141
  body.accountSid,
141
142
  );
142
143
  if (!sidStored) {
@@ -148,11 +149,11 @@ export async function handleSetTwilioCredentials(
148
149
  }
149
150
 
150
151
  const tokenStored = await setSecureKeyAsync(
151
- "credential:twilio:auth_token",
152
+ credentialKey("twilio", "auth_token"),
152
153
  body.authToken,
153
154
  );
154
155
  if (!tokenStored) {
155
- await deleteSecureKeyAsync("credential:twilio:account_sid");
156
+ await deleteSecureKeyAsync(credentialKey("twilio", "account_sid"));
156
157
  return Response.json({
157
158
  success: false,
158
159
  hasCredentials: false,
@@ -202,8 +203,8 @@ export async function handleSetTwilioCredentials(
202
203
  * DELETE /v1/integrations/twilio/credentials
203
204
  */
204
205
  export async function handleClearTwilioCredentials(): Promise<Response> {
205
- const r1 = await deleteSecureKeyAsync("credential:twilio:account_sid");
206
- const r2 = await deleteSecureKeyAsync("credential:twilio:auth_token");
206
+ const r1 = await deleteSecureKeyAsync(credentialKey("twilio", "account_sid"));
207
+ const r2 = await deleteSecureKeyAsync(credentialKey("twilio", "auth_token"));
207
208
 
208
209
  if (r1 === "error" || r2 === "error") {
209
210
  return Response.json(
@@ -4,6 +4,7 @@ import {
4
4
  invalidateConfigCache,
5
5
  } from "../../config/loader.js";
6
6
  import { initializeProviders } from "../../providers/registry.js";
7
+ import { credentialKey } from "../../security/credential-key.js";
7
8
  import {
8
9
  deleteSecureKeyAsync,
9
10
  getSecureKeyAsync,
@@ -78,7 +79,7 @@ export async function handleAddSecret(req: Request): Promise<Response> {
78
79
  assertMetadataWritable();
79
80
  const service = name.slice(0, colonIdx);
80
81
  const field = name.slice(colonIdx + 1);
81
- const key = `credential:${service}:${field}`;
82
+ const key = credentialKey(service, field);
82
83
  const stored = await setSecureKeyAsync(key, value);
83
84
  if (!stored) {
84
85
  return httpError(
@@ -164,7 +165,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
164
165
  const service = name.slice(0, colonIdx);
165
166
  const field = name.slice(colonIdx + 1);
166
167
  assertMetadataWritable();
167
- const key = `credential:${service}:${field}`;
168
+ const key = credentialKey(service, field);
168
169
  // Check existence first — the broker always returns "deleted" even
169
170
  // for keys that don't exist, so we need a pre-check for 404 semantics.
170
171
  const existing = await getSecureKeyAsync(key);