@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
@@ -74,6 +74,15 @@ export class RateLimitProvider implements Provider {
74
74
 
75
75
  if (this.requestTimestamps.length >= limit) {
76
76
  const waitSec = Math.ceil((oldestInWindow + 60_000 - now) / 1000);
77
+ log.warn(
78
+ {
79
+ provider: this.name,
80
+ limit,
81
+ currentCount: this.requestTimestamps.length,
82
+ retryAfterSec: waitSec,
83
+ },
84
+ `Provider rate limit exceeded: ${limit} requests/minute for ${this.name}`,
85
+ );
77
86
  throw new RateLimitError(
78
87
  `Rate limit exceeded: ${limit} requests/minute. Try again in ${waitSec}s.`,
79
88
  );
@@ -85,6 +94,14 @@ export class RateLimitProvider implements Provider {
85
94
  if (limit <= 0) return;
86
95
 
87
96
  if (this.sessionTokens >= limit) {
97
+ log.warn(
98
+ {
99
+ provider: this.name,
100
+ sessionTokens: this.sessionTokens,
101
+ limit,
102
+ },
103
+ `Session token budget exhausted for ${this.name}: ${this.sessionTokens.toLocaleString()}/${limit.toLocaleString()}`,
104
+ );
88
105
  throw new RateLimitError(
89
106
  `Session token budget exhausted: ${this.sessionTokens.toLocaleString()}/${limit.toLocaleString()} tokens used. Start a new session to continue.`,
90
107
  );
@@ -289,8 +289,8 @@ export function initializeProviders(config: ProvidersConfig): void {
289
289
  );
290
290
  routingSources.set("gemini", "user-key");
291
291
  } else {
292
- // No user Gemini key — try managed proxy fallback
293
- const managedBaseUrl = buildManagedBaseUrl("gemini");
292
+ // No user Gemini key — route through Vertex managed proxy
293
+ const managedBaseUrl = buildManagedBaseUrl("vertex");
294
294
  if (managedBaseUrl) {
295
295
  const ctx = resolveManagedProxyContext();
296
296
  const model = resolveModel(config, "gemini");
@@ -43,7 +43,7 @@ Host file allows the assistant to perform file operations (read, write, edit) on
43
43
  - `POST /v1/host-file-result` — `{ requestId, content, isError }`
44
44
  - **Tracking**: Uses the same `pending-interactions` tracker as approvals and host bash, with `kind: "host_file"`. The endpoint validates the interaction kind before resolving.
45
45
 
46
- ### Channel approvals (Telegram, SMS)
46
+ ### Channel approvals (Telegram, Slack)
47
47
 
48
48
  Channel approval flows use `requestId` (not `runId`) as the primary identifier:
49
49
 
@@ -51,6 +51,23 @@ Channel approval flows use `requestId` (not `runId`) as the primary identifier:
51
51
  - Guardian approval records in `channelGuardianApprovalRequests` link via `requestId`.
52
52
  - The conversational approval engine classifies user intent and resolves via `session.handleConfirmationResponse(requestId, decision)`.
53
53
 
54
+ ## Rate Limiting & Diagnostics
55
+
56
+ All `/v1/*` endpoints share a per-client-IP sliding-window rate limiter (`middleware/rate-limiter.ts`):
57
+
58
+ - **Authenticated**: 300 requests/minute
59
+ - **Unauthenticated**: 20 requests/minute
60
+
61
+ When the limit is exceeded, the limiter returns 429 and logs a structured warning (module: `rate-limiter`) with the denied endpoint and a breakdown of which endpoints consumed the budget in the current window. This makes it easy to identify whether the cause is rapid thread switching, polling, or unexpected request volume.
62
+
63
+ Logs are written to `~/.vellum/workspace/data/logs/vellum.log` by default. If `logFile.dir` is configured, logs rotate daily as `assistant-YYYY-MM-DD.log` in that directory. To watch rate limit events in real time:
64
+
65
+ ```bash
66
+ tail -f ~/.vellum/workspace/data/logs/vellum.log | grep rate-limit
67
+ ```
68
+
69
+ The provider-level rate limiter (`providers/ratelimit.ts`) also logs warnings (module: `rate-limit`) when request rate or token budget limits are enforced.
70
+
54
71
  ## HTTP-Only Transport
55
72
 
56
73
  HTTP is the sole transport for client-daemon communication. The runtime HTTP server (`assistant/src/runtime/http-server.ts`) is the canonical API surface. Clients connect via HTTP for request/response operations and SSE (`GET /v1/events`) for streaming server-to-client events.
@@ -403,6 +403,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
403
403
  { endpoint: "schedules/cancel", scopes: ["settings.write"] },
404
404
 
405
405
  // Diagnostics
406
+ { endpoint: "export", scopes: ["settings.read"] },
406
407
  { endpoint: "diagnostics/export", scopes: ["settings.read"] },
407
408
  { endpoint: "diagnostics/env-vars", scopes: ["settings.read"] },
408
409
 
@@ -16,6 +16,7 @@ import {
16
16
  saveRawConfig,
17
17
  setNestedValue,
18
18
  } from "../../config/loader.js";
19
+ import { credentialKey } from "../../security/credential-key.js";
19
20
  import { getSecureKey } from "../../security/secure-keys.js";
20
21
  import { getTelegramBotUsername } from "../../telegram/bot-username.js";
21
22
  import { getLogger } from "../../util/logger.js";
@@ -40,7 +41,7 @@ export async function ensureTelegramBotUsernameResolved(): Promise<void> {
40
41
  return; // Username already cached in config
41
42
  }
42
43
 
43
- const token = getSecureKey("credential:telegram:bot_token");
44
+ const token = getSecureKey(credentialKey("telegram", "bot_token"));
44
45
  if (!token) return;
45
46
 
46
47
  try {
@@ -3,6 +3,7 @@ import { hasTwilioCredentials } from "../calls/twilio-rest.js";
3
3
  import { getChannelInvitePolicy } from "../channels/config.js";
4
4
  import { loadRawConfig } from "../config/loader.js";
5
5
  import { getEmailService } from "../email/service.js";
6
+ import { credentialKey } from "../security/credential-key.js";
6
7
  import { getSecureKey } from "../security/secure-keys.js";
7
8
  import { resolveWhatsAppDisplayNumber } from "./channel-invite-transports/whatsapp.js";
8
9
  import type {
@@ -11,6 +12,7 @@ import type {
11
12
  ChannelProbeContext,
12
13
  ChannelReadinessSnapshot,
13
14
  ReadinessCheckResult,
15
+ SetupStatus,
14
16
  } from "./channel-readiness-types.js";
15
17
  import { probeLocalGatewayHealth } from "./local-gateway-health.js";
16
18
 
@@ -31,55 +33,83 @@ function hasIngressConfigured(): boolean {
31
33
  }
32
34
  }
33
35
 
36
+ // ── Shared check helpers ────────────────────────────────────────────────────
37
+
38
+ /** Build a check result from a boolean condition. */
39
+ function check(
40
+ name: string,
41
+ passed: boolean,
42
+ passMessage: string,
43
+ failMessage: string,
44
+ ): ReadinessCheckResult {
45
+ return { name, passed, message: passed ? passMessage : failMessage };
46
+ }
47
+
48
+ /** Check that a secure credential key exists. */
49
+ function checkCredential(
50
+ name: string,
51
+ service: string,
52
+ field: string,
53
+ label: string,
54
+ ): ReadinessCheckResult {
55
+ const exists = !!getSecureKey(credentialKey(service, field));
56
+ return check(
57
+ name,
58
+ exists,
59
+ `${label} is configured`,
60
+ `${label} is not configured`,
61
+ );
62
+ }
63
+
64
+ /** Check that public ingress is configured and enabled. */
65
+ function checkIngress(): ReadinessCheckResult {
66
+ const has = hasIngressConfigured();
67
+ return check(
68
+ "ingress",
69
+ has,
70
+ "Public ingress URL is configured",
71
+ "Public ingress URL is not configured or disabled",
72
+ );
73
+ }
74
+
34
75
  // ── Voice Probe ─────────────────────────────────────────────────────────────
35
76
 
36
77
  const voiceProbe: ChannelProbe = {
37
78
  channel: "phone",
38
79
  async runLocalChecks(): Promise<ReadinessCheckResult[]> {
39
- const results: ReadinessCheckResult[] = [];
40
-
41
80
  const hasCreds = hasTwilioCredentials();
42
- results.push({
43
- name: "twilio_credentials",
44
- passed: hasCreds,
45
- message: hasCreds
46
- ? "Twilio credentials are configured"
47
- : "Twilio Account SID and Auth Token are not configured",
48
- });
49
-
50
- const resolvedNumber = resolveTwilioPhoneNumber();
51
- const hasPhone = !!resolvedNumber;
52
- results.push({
53
- name: "phone_number",
54
- passed: hasPhone,
55
- message: hasPhone
56
- ? "Phone number is assigned for voice calls"
57
- : "No phone number assigned for voice calls",
58
- });
59
-
60
- const hasIngress = hasIngressConfigured();
61
- results.push({
62
- name: "ingress",
63
- passed: hasIngress,
64
- message: hasIngress
65
- ? "Public ingress URL is configured"
66
- : "Public ingress URL is not configured or disabled",
67
- });
68
-
69
- if (hasIngress) {
70
- const gatewayHealth = await probeLocalGatewayHealth();
71
- results.push({
72
- name: "gateway_health",
73
- passed: gatewayHealth.healthy,
74
- message: gatewayHealth.healthy
75
- ? `Local gateway is serving requests at ${gatewayHealth.target}`
76
- : `Local gateway is not serving requests at ${gatewayHealth.target}${
77
- gatewayHealth.error ? `: ${gatewayHealth.error}` : ""
78
- }`,
79
- });
81
+ const hasPhone = !!resolveTwilioPhoneNumber();
82
+ const ingress = checkIngress();
83
+
84
+ const checks: ReadinessCheckResult[] = [
85
+ check(
86
+ "twilio_credentials",
87
+ hasCreds,
88
+ "Twilio credentials are configured",
89
+ "Twilio Account SID and Auth Token are not configured",
90
+ ),
91
+ check(
92
+ "phone_number",
93
+ hasPhone,
94
+ "Phone number is assigned for voice calls",
95
+ "No phone number assigned for voice calls",
96
+ ),
97
+ ingress,
98
+ ];
99
+
100
+ if (ingress.passed) {
101
+ const gw = await probeLocalGatewayHealth();
102
+ checks.push(
103
+ check(
104
+ "gateway_health",
105
+ gw.healthy,
106
+ `Local gateway is serving requests at ${gw.target}`,
107
+ `Local gateway is not serving requests at ${gw.target}${gw.error ? `: ${gw.error}` : ""}`,
108
+ ),
109
+ );
80
110
  }
81
111
 
82
- return results;
112
+ return checks;
83
113
  },
84
114
  };
85
115
 
@@ -87,41 +117,16 @@ const voiceProbe: ChannelProbe = {
87
117
 
88
118
  const telegramProbe: ChannelProbe = {
89
119
  channel: "telegram",
90
- runLocalChecks(): ReadinessCheckResult[] {
91
- const results: ReadinessCheckResult[] = [];
92
-
93
- const hasBotToken = !!getSecureKey("credential:telegram:bot_token");
94
- results.push({
95
- name: "bot_token",
96
- passed: hasBotToken,
97
- message: hasBotToken
98
- ? "Telegram bot token is configured"
99
- : "Telegram bot token is not configured",
100
- });
101
-
102
- const hasWebhookSecret = !!getSecureKey(
103
- "credential:telegram:webhook_secret",
104
- );
105
- results.push({
106
- name: "webhook_secret",
107
- passed: hasWebhookSecret,
108
- message: hasWebhookSecret
109
- ? "Telegram webhook secret is configured"
110
- : "Telegram webhook secret is not configured",
111
- });
112
-
113
- const hasIngress = hasIngressConfigured();
114
- results.push({
115
- name: "ingress",
116
- passed: hasIngress,
117
- message: hasIngress
118
- ? "Public ingress URL is configured"
119
- : "Public ingress URL is not configured or disabled",
120
- });
121
-
122
- return results;
123
- },
124
- // Telegram has no remote checks currently
120
+ runLocalChecks: () => [
121
+ checkCredential("bot_token", "telegram", "bot_token", "Telegram bot token"),
122
+ checkCredential(
123
+ "webhook_secret",
124
+ "telegram",
125
+ "webhook_secret",
126
+ "Telegram webhook secret",
127
+ ),
128
+ checkIngress(),
129
+ ],
125
130
  };
126
131
 
127
132
  // ── Email Probe ─────────────────────────────────────────────────────────────
@@ -129,43 +134,33 @@ const telegramProbe: ChannelProbe = {
129
134
  const emailProbe: ChannelProbe = {
130
135
  channel: "email",
131
136
  runLocalChecks(): ReadinessCheckResult[] {
132
- const results: ReadinessCheckResult[] = [];
133
-
134
137
  const hasApiKey = !!(
135
- getSecureKey("agentmail") || getSecureKey("credential:agentmail:api_key")
138
+ getSecureKey("agentmail") ||
139
+ getSecureKey(credentialKey("agentmail", "api_key"))
136
140
  );
137
- results.push({
138
- name: "agentmail_api_key",
139
- passed: hasApiKey,
140
- message: hasApiKey
141
- ? "AgentMail API key is configured"
142
- : "AgentMail API key is not configured",
143
- });
144
-
145
141
  const invitePolicy = getChannelInvitePolicy("email");
146
- results.push({
147
- name: "invite_policy",
148
- passed: invitePolicy.codeRedemptionEnabled,
149
- message: invitePolicy.codeRedemptionEnabled
150
- ? "Email invite code redemption is enabled"
151
- : "Email invite code redemption is disabled",
152
- });
153
-
154
- const hasIngress = hasIngressConfigured();
155
- results.push({
156
- name: "ingress",
157
- passed: hasIngress,
158
- message: hasIngress
159
- ? "Public ingress URL is configured"
160
- : "Public ingress URL is not configured or disabled",
161
- });
162
-
163
- return results;
142
+ return [
143
+ check(
144
+ "agentmail_api_key",
145
+ hasApiKey,
146
+ "AgentMail API key is configured",
147
+ "AgentMail API key is not configured",
148
+ ),
149
+ check(
150
+ "invite_policy",
151
+ invitePolicy.codeRedemptionEnabled,
152
+ "Email invite code redemption is enabled",
153
+ "Email invite code redemption is disabled",
154
+ ),
155
+ checkIngress(),
156
+ ];
164
157
  },
158
+ // runRemoteChecks UNCHANGED — keep the existing inbox_configured check as-is
165
159
  async runRemoteChecks(): Promise<ReadinessCheckResult[]> {
166
160
  // Only worth checking if the API key is present
167
161
  const hasApiKey = !!(
168
- getSecureKey("agentmail") || getSecureKey("credential:agentmail:api_key")
162
+ getSecureKey("agentmail") ||
163
+ getSecureKey(credentialKey("agentmail", "api_key"))
169
164
  );
170
165
  if (!hasApiKey) return [];
171
166
 
@@ -199,77 +194,47 @@ const emailProbe: ChannelProbe = {
199
194
  const whatsappProbe: ChannelProbe = {
200
195
  channel: "whatsapp",
201
196
  runLocalChecks(): ReadinessCheckResult[] {
202
- const results: ReadinessCheckResult[] = [];
203
-
204
- const hasPhoneNumberId = !!getSecureKey(
205
- "credential:whatsapp:phone_number_id",
206
- );
207
- results.push({
208
- name: "whatsapp_phone_number_id",
209
- passed: hasPhoneNumberId,
210
- message: hasPhoneNumberId
211
- ? "WhatsApp phone number ID is configured"
212
- : "WhatsApp phone number ID is not configured",
213
- });
214
-
215
- const hasAccessToken = !!getSecureKey("credential:whatsapp:access_token");
216
- results.push({
217
- name: "whatsapp_access_token",
218
- passed: hasAccessToken,
219
- message: hasAccessToken
220
- ? "WhatsApp access token is configured"
221
- : "WhatsApp access token is not configured",
222
- });
223
-
224
- const hasAppSecret = !!getSecureKey("credential:whatsapp:app_secret");
225
- results.push({
226
- name: "whatsapp_app_secret",
227
- passed: hasAppSecret,
228
- message: hasAppSecret
229
- ? "WhatsApp app secret is configured"
230
- : "WhatsApp app secret is not configured",
231
- });
232
-
233
- const hasWebhookVerifyToken = !!getSecureKey(
234
- "credential:whatsapp:webhook_verify_token",
235
- );
236
- results.push({
237
- name: "whatsapp_webhook_verify_token",
238
- passed: hasWebhookVerifyToken,
239
- message: hasWebhookVerifyToken
240
- ? "WhatsApp webhook verify token is configured"
241
- : "WhatsApp webhook verify token is not configured",
242
- });
243
-
244
197
  const displayNumber = resolveWhatsAppDisplayNumber();
245
- const hasDisplayNumber = !!displayNumber;
246
- results.push({
247
- name: "whatsapp_display_phone_number",
248
- passed: hasDisplayNumber,
249
- message: hasDisplayNumber
250
- ? `WhatsApp display phone number is configured (${displayNumber})`
251
- : "WhatsApp display phone number is not configured — set whatsapp.phoneNumber in workspace config",
252
- });
253
-
254
198
  const invitePolicy = getChannelInvitePolicy("whatsapp");
255
- results.push({
256
- name: "invite_policy",
257
- passed: invitePolicy.codeRedemptionEnabled,
258
- message: invitePolicy.codeRedemptionEnabled
259
- ? "WhatsApp invite code redemption is enabled"
260
- : "WhatsApp invite code redemption is disabled",
261
- });
262
-
263
- const hasIngress = hasIngressConfigured();
264
- results.push({
265
- name: "ingress",
266
- passed: hasIngress,
267
- message: hasIngress
268
- ? "Public ingress URL is configured"
269
- : "Public ingress URL is not configured or disabled",
270
- });
271
-
272
- return results;
199
+ return [
200
+ checkCredential(
201
+ "whatsapp_phone_number_id",
202
+ "whatsapp",
203
+ "phone_number_id",
204
+ "WhatsApp phone number ID",
205
+ ),
206
+ checkCredential(
207
+ "whatsapp_access_token",
208
+ "whatsapp",
209
+ "access_token",
210
+ "WhatsApp access token",
211
+ ),
212
+ checkCredential(
213
+ "whatsapp_app_secret",
214
+ "whatsapp",
215
+ "app_secret",
216
+ "WhatsApp app secret",
217
+ ),
218
+ checkCredential(
219
+ "whatsapp_webhook_verify_token",
220
+ "whatsapp",
221
+ "webhook_verify_token",
222
+ "WhatsApp webhook verify token",
223
+ ),
224
+ check(
225
+ "whatsapp_display_phone_number",
226
+ !!displayNumber,
227
+ `WhatsApp display phone number is configured (${displayNumber})`,
228
+ "WhatsApp display phone number is not configured — set whatsapp.phoneNumber in workspace config",
229
+ ),
230
+ check(
231
+ "invite_policy",
232
+ invitePolicy.codeRedemptionEnabled,
233
+ "WhatsApp invite code redemption is enabled",
234
+ "WhatsApp invite code redemption is disabled",
235
+ ),
236
+ checkIngress(),
237
+ ];
273
238
  },
274
239
  };
275
240
 
@@ -277,26 +242,20 @@ const whatsappProbe: ChannelProbe = {
277
242
 
278
243
  const slackProbe: ChannelProbe = {
279
244
  channel: "slack",
280
- runLocalChecks(): ReadinessCheckResult[] {
281
- const hasBotToken = !!getSecureKey("credential:slack_channel:bot_token");
282
- const hasAppToken = !!getSecureKey("credential:slack_channel:app_token");
283
- return [
284
- {
285
- name: "bot_token",
286
- passed: hasBotToken,
287
- message: hasBotToken
288
- ? "Slack bot token is configured"
289
- : "Slack bot token is not configured",
290
- },
291
- {
292
- name: "app_token",
293
- passed: hasAppToken,
294
- message: hasAppToken
295
- ? "Slack app token is configured"
296
- : "Slack app token is not configured",
297
- },
298
- ];
299
- },
245
+ runLocalChecks: () => [
246
+ checkCredential(
247
+ "bot_token",
248
+ "slack_channel",
249
+ "bot_token",
250
+ "Slack bot token",
251
+ ),
252
+ checkCredential(
253
+ "app_token",
254
+ "slack_channel",
255
+ "app_token",
256
+ "Slack app token",
257
+ ),
258
+ ],
300
259
  };
301
260
 
302
261
  // ── Service ─────────────────────────────────────────────────────────────────
@@ -369,6 +328,18 @@ export class ChannelReadinessService {
369
328
  : true;
370
329
  const ready = allLocalPassed && allRemotePassed;
371
330
 
331
+ // setupStatus: considers all checks (credentials + infrastructure)
332
+ const consideredChecks = [
333
+ ...localChecks,
334
+ ...(remoteChecks && remoteChecksAffectReadiness ? remoteChecks : []),
335
+ ];
336
+ const anyCheckPassed = consideredChecks.some((c) => c.passed);
337
+ const setupStatus: SetupStatus = !anyCheckPassed
338
+ ? "not_configured"
339
+ : ready
340
+ ? "ready"
341
+ : "incomplete";
342
+
372
343
  const reasons: Array<{ code: string; text: string }> = [];
373
344
  for (const check of localChecks) {
374
345
  if (!check.passed) {
@@ -386,6 +357,7 @@ export class ChannelReadinessService {
386
357
  const snapshot: ChannelReadinessSnapshot = {
387
358
  channel: ch,
388
359
  ready,
360
+ setupStatus,
389
361
  checkedAt:
390
362
  remoteChecks && cached && !remoteChecksFreshlyFetched
391
363
  ? cached.checkedAt
@@ -422,6 +394,7 @@ export class ChannelReadinessService {
422
394
  return {
423
395
  channel,
424
396
  ready: false,
397
+ setupStatus: "not_configured",
425
398
  checkedAt: Date.now(),
426
399
  stale: false,
427
400
  reasons: [
@@ -4,6 +4,9 @@ import type { ChannelId } from "../channels/types.js";
4
4
 
5
5
  export type { ChannelId };
6
6
 
7
+ /** Setup progress for a channel: not_configured → incomplete → ready. */
8
+ export type SetupStatus = "not_configured" | "incomplete" | "ready";
9
+
7
10
  /** Result of a single readiness check (local or remote). */
8
11
  export interface ReadinessCheckResult {
9
12
  name: string;
@@ -15,6 +18,7 @@ export interface ReadinessCheckResult {
15
18
  export interface ChannelReadinessSnapshot {
16
19
  channel: ChannelId;
17
20
  ready: boolean;
21
+ setupStatus: SetupStatus;
18
22
  checkedAt: number;
19
23
  stale: boolean;
20
24
  reasons: Array<{ code: string; text: string }>;
@@ -2,12 +2,11 @@
2
2
  * Guardian follow-up conversation engine.
3
3
  *
4
4
  * When a guardian replies to a post-timeout follow-up prompt (e.g. "would you
5
- * like to call them back or send a message?"), this engine classifies the
5
+ * like to call them back?"), this engine classifies the
6
6
  * guardian's intent into a structured disposition and produces a natural reply.
7
7
  *
8
8
  * Dispositions:
9
9
  * - call_back: Guardian wants to call the original caller back
10
- * - message_back: Guardian wants to send a text/message to the caller
11
10
  * - decline: Guardian declines to follow up ("never mind", "no thanks")
12
11
  * - keep_pending: Intent is ambiguous — ask for clarification
13
12
  *
@@ -37,7 +36,6 @@ const FALLBACK_RETRY_TEXT = getGuardianActionFallbackMessage({
37
36
 
38
37
  const VALID_DISPOSITIONS: ReadonlySet<string> = new Set([
39
38
  "call_back",
40
- "message_back",
41
39
  "decline",
42
40
  "keep_pending",
43
41
  ]);
@@ -173,7 +173,7 @@ async function executeCallBack(
173
173
 
174
174
  /**
175
175
  * Execute a follow-up action after the conversation engine has classified
176
- * the guardian's intent as call_back or message_back and the follow-up
176
+ * the guardian's intent as call_back and the follow-up
177
177
  * state has been transitioned to `dispatching`.
178
178
  *
179
179
  * On success: finalizes the follow-up to `completed` and returns
@@ -250,7 +250,6 @@ export async function executeFollowupAction(
250
250
  actionResult = await executeCallBack(request, counterparty);
251
251
  } else {
252
252
  // decline is already handled in M5 — should not reach the executor.
253
- // message_back (SMS) is no longer supported.
254
253
  finalizeFollowup(requestId, "failed");
255
254
  const errorText = await composeGuardianActionMessageGenerative(
256
255
  {
@@ -36,8 +36,6 @@ export type GuardianActionMessageScenario =
36
36
  | "guardian_superseded_remap"
37
37
  | "guardian_unknown_code"
38
38
  | "guardian_auto_matched"
39
- | "outbound_message_copy"
40
- | "followup_message_sent"
41
39
  | "followup_call_started"
42
40
  | "followup_action_failed"
43
41
  | "guardian_answer_delivery_failed";
@@ -183,8 +181,8 @@ export function getGuardianActionFallbackMessage(
183
181
 
184
182
  case "guardian_late_answer_followup":
185
183
  return context.callerIdentifier
186
- ? `${context.callerIdentifier} called earlier with a question, but I wasn't able to connect them. Would you like to call them back or send them a message?`
187
- : "Someone called earlier with a question, but I wasn't able to connect them. Would you like to call them back or send them a message?";
184
+ ? `${context.callerIdentifier} called earlier with a question, but I wasn't able to connect them. Would you like to call them back?`
185
+ : "Someone called earlier with a question, but I wasn't able to connect them. Would you like to call them back?";
188
186
 
189
187
  case "guardian_followup_dispatching":
190
188
  return context.followupAction
@@ -205,7 +203,7 @@ export function getGuardianActionFallbackMessage(
205
203
  return "No problem. Let me know if you change your mind or need anything else.";
206
204
 
207
205
  case "guardian_followup_clarification":
208
- return "Sorry, I didn't quite catch that. Would you like to call them back, send them a message, or skip it for now?";
206
+ return "Sorry, I didn't quite catch that. Would you like to call them back or skip it for now?";
209
207
 
210
208
  case "guardian_pending_disambiguation":
211
209
  return listedCodes
@@ -247,24 +245,6 @@ export function getGuardianActionFallbackMessage(
247
245
  ? `Got it! Your answer has been applied to the current active request: "${context.questionText}"`
248
246
  : "Got it! Your answer has been applied to the current active request on the call.";
249
247
 
250
- case "outbound_message_copy":
251
- // This message is sent TO the original caller relaying the guardian's answer.
252
- // When lateAnswerText is available, include it — that's the whole point of message_back.
253
- if (context.lateAnswerText && context.questionText) {
254
- return `Hi! You asked "${context.questionText}" earlier. Here's the answer: ${context.lateAnswerText}`;
255
- }
256
- if (context.lateAnswerText) {
257
- return `Hi! Regarding your earlier question — here's the answer: ${context.lateAnswerText}`;
258
- }
259
- return context.questionText
260
- ? `Hi! You asked "${context.questionText}" earlier. We'll get back to you with an answer soon.`
261
- : "Hi! Thanks for calling earlier. We'll get back to you soon.";
262
-
263
- case "followup_message_sent":
264
- return context.counterpartyPhone
265
- ? `Done! I've sent a text message to ${context.counterpartyPhone} with your answer.`
266
- : "Done! I've sent them a text message with your answer.";
267
-
268
248
  case "followup_call_started":
269
249
  return context.counterpartyPhone
270
250
  ? `Got it! I'm calling ${context.counterpartyPhone} back now to relay your answer.`