@vellumai/assistant 0.4.34 → 0.4.36

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 (251) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +4 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +91 -43
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +806 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +491 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +133 -242
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +177 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +175 -145
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +12 -1
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
  175. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
  176. package/src/memory/migrations/038-actor-token-records.ts +8 -1
  177. package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
  178. package/src/memory/migrations/110-channel-guardian.ts +27 -6
  179. package/src/memory/migrations/112-assistant-inbox.ts +39 -15
  180. package/src/memory/migrations/114-notifications.ts +37 -15
  181. package/src/memory/migrations/117-conversation-attention.ts +33 -9
  182. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  183. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  184. package/src/memory/migrations/index.ts +2 -0
  185. package/src/memory/migrations/schema-introspection.ts +18 -0
  186. package/src/memory/schema-migration.ts +1 -0
  187. package/src/memory/shared-app-links-store.ts +1 -1
  188. package/src/messaging/registry.ts +27 -0
  189. package/src/notifications/README.md +79 -70
  190. package/src/notifications/broadcaster.ts +2 -1
  191. package/src/notifications/conversation-pairing.ts +147 -13
  192. package/src/notifications/copy-composer.ts +7 -3
  193. package/src/notifications/destination-resolver.ts +14 -1
  194. package/src/notifications/emit-signal.ts +3 -2
  195. package/src/notifications/signal.ts +105 -1
  196. package/src/notifications/types.ts +16 -0
  197. package/src/permissions/checker.ts +29 -3
  198. package/src/permissions/prompter.ts +11 -3
  199. package/src/runtime/access-request-helper.ts +2 -1
  200. package/src/runtime/auth/route-policy.ts +7 -1
  201. package/src/runtime/channel-invite-transport.ts +40 -63
  202. package/src/runtime/channel-invite-transports/email.ts +13 -39
  203. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  204. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  205. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  206. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  207. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  208. package/src/runtime/channel-readiness-service.ts +202 -45
  209. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  210. package/src/runtime/guardian-outbound-actions.ts +8 -5
  211. package/src/runtime/http-server.ts +5 -9
  212. package/src/runtime/http-types.ts +13 -1
  213. package/src/runtime/invite-instruction-generator.ts +178 -0
  214. package/src/runtime/invite-service.ts +22 -25
  215. package/src/runtime/migrations/migration-transport.ts +13 -0
  216. package/src/runtime/routes/app-routes.ts +1 -1
  217. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  218. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  219. package/src/runtime/routes/contact-routes.ts +54 -26
  220. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -1
  221. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  222. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  223. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  224. package/src/runtime/routes/integration-routes.ts +1 -1
  225. package/src/runtime/routes/invite-routes.ts +1 -1
  226. package/src/runtime/routes/secret-routes.ts +31 -7
  227. package/src/runtime/routes/surface-content-routes.ts +104 -0
  228. package/src/runtime/routes/twilio-routes.ts +32 -1
  229. package/src/runtime/routes/usage-routes.ts +114 -0
  230. package/src/runtime/tool-grant-request-helper.ts +2 -1
  231. package/src/security/encrypted-store.ts +9 -5
  232. package/src/security/keychain-broker-client.ts +393 -0
  233. package/src/security/secure-keys.ts +106 -321
  234. package/src/tools/apps/executors.ts +73 -0
  235. package/src/tools/browser/auto-navigate.ts +15 -6
  236. package/src/tools/browser/chrome-cdp.ts +211 -0
  237. package/src/tools/browser/network-recorder.test.ts +83 -0
  238. package/src/tools/browser/network-recorder.ts +8 -7
  239. package/src/tools/browser/x-auto-navigate.ts +12 -6
  240. package/src/tools/credentials/policy-types.ts +24 -0
  241. package/src/tools/credentials/vault.ts +22 -27
  242. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  243. package/src/tools/permission-checker.ts +1 -0
  244. package/src/tools/types.ts +2 -0
  245. package/src/tools/ui-surface/definitions.ts +1 -2
  246. package/src/tools/watch/watch-state.ts +2 -0
  247. package/src/__tests__/key-migration.test.ts +0 -240
  248. package/src/__tests__/keychain.test.ts +0 -286
  249. package/src/cli/core-commands.ts +0 -899
  250. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  251. package/src/security/keychain.ts +0 -490
@@ -66,12 +66,19 @@ export function resolveDestinations(
66
66
  case "sms": {
67
67
  const guardianResult = findGuardianForChannel(channel);
68
68
  if (guardianResult && guardianResult.channel.externalChatId) {
69
+ const externalChatId = guardianResult.channel.externalChatId;
69
70
  result.set(channel as NotificationChannel, {
70
71
  channel: channel as NotificationChannel,
71
- endpoint: guardianResult.channel.externalChatId ?? undefined,
72
+ endpoint: externalChatId,
72
73
  metadata: {
73
74
  externalUserId: guardianResult.channel.externalUserId,
74
75
  },
76
+ bindingContext: {
77
+ sourceChannel: channel as NotificationChannel,
78
+ externalChatId,
79
+ externalUserId:
80
+ guardianResult.channel.externalUserId ?? undefined,
81
+ },
75
82
  });
76
83
  }
77
84
  log.debug(
@@ -97,6 +104,12 @@ export function resolveDestinations(
97
104
  metadata: {
98
105
  externalUserId: guardianResult.channel.externalUserId,
99
106
  },
107
+ bindingContext: {
108
+ sourceChannel: "slack",
109
+ externalChatId: chatId,
110
+ externalUserId:
111
+ guardianResult.channel.externalUserId ?? undefined,
112
+ },
100
113
  });
101
114
  } else if (guardianResult && chatId) {
102
115
  log.warn(
@@ -34,6 +34,7 @@ import type {
34
34
  AttentionHints,
35
35
  NotificationContextPayload,
36
36
  NotificationSignal,
37
+ NotificationSourceChannel,
37
38
  RoutingIntent,
38
39
  } from "./signal.js";
39
40
  import type {
@@ -151,8 +152,8 @@ function getConnectedChannels(): NotificationChannel[] {
151
152
  export interface EmitSignalParams<TEventName extends string = string> {
152
153
  /** Free-form event name, e.g. 'reminder.fired', 'schedule.complete'. */
153
154
  sourceEventName: TEventName;
154
- /** Source channel that produced the event. */
155
- sourceChannel: string;
155
+ /** Source channel that produced the event — must be a registered channel. */
156
+ sourceChannel: NotificationSourceChannel;
156
157
  /** Session or conversation ID from the source context. */
157
158
  sourceSessionId: string;
158
159
  /** Attention hints for the decision engine. */
@@ -6,6 +6,110 @@
6
6
 
7
7
  import type { GuardianQuestionPayload } from "./guardian-question-mode.js";
8
8
 
9
+ // ── Source channel registry ────────────────────────────────────────────
10
+
11
+ export const NOTIFICATION_SOURCE_CHANNELS = [
12
+ { id: "assistant_tool", description: "Assistant skill/tool invocation" },
13
+ { id: "vellum", description: "Vellum native client (macOS/iOS)" },
14
+ { id: "voice", description: "Voice call pipeline" },
15
+ { id: "telegram", description: "Telegram channel" },
16
+ { id: "sms", description: "SMS channel" },
17
+ { id: "slack", description: "Slack channel" },
18
+ { id: "scheduler", description: "Scheduled task runner (reminders, cron)" },
19
+ { id: "watcher", description: "File/event watcher subsystem" },
20
+ ] as const;
21
+
22
+ export type NotificationSourceChannel =
23
+ (typeof NOTIFICATION_SOURCE_CHANNELS)[number]["id"];
24
+
25
+ export function isNotificationSourceChannel(
26
+ value: unknown,
27
+ ): value is NotificationSourceChannel {
28
+ return (
29
+ typeof value === "string" &&
30
+ NOTIFICATION_SOURCE_CHANNELS.some((c) => c.id === value)
31
+ );
32
+ }
33
+
34
+ // ── Source event name registry ─────────────────────────────────────────
35
+
36
+ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
37
+ {
38
+ id: "user.send_notification",
39
+ description: "User-initiated notification via assistant tool",
40
+ },
41
+ { id: "reminder.fired", description: "Scheduled reminder triggered" },
42
+ { id: "schedule.complete", description: "Scheduled task finished running" },
43
+ {
44
+ id: "guardian.question",
45
+ description: "Guardian approval question requiring response",
46
+ },
47
+ { id: "ingress.access_request", description: "Non-member requesting access" },
48
+ {
49
+ id: "ingress.access_request.callback_handoff",
50
+ description: "Caller requested callback while unreachable",
51
+ },
52
+ {
53
+ id: "ingress.escalation",
54
+ description: "Incoming message escalated for attention",
55
+ },
56
+ {
57
+ id: "ingress.trusted_contact.guardian_decision",
58
+ description: "Guardian decided on trusted contact request",
59
+ },
60
+ {
61
+ id: "ingress.trusted_contact.denied",
62
+ description: "Trusted contact request denied",
63
+ },
64
+ {
65
+ id: "ingress.trusted_contact.verification_sent",
66
+ description: "Verification sent to trusted contact",
67
+ },
68
+ {
69
+ id: "ingress.trusted_contact.activated",
70
+ description: "Trusted contact activated",
71
+ },
72
+ {
73
+ id: "watcher.notification",
74
+ description: "Watcher detected a notable event",
75
+ },
76
+ {
77
+ id: "watcher.escalation",
78
+ description: "Watcher event requiring immediate attention",
79
+ },
80
+ {
81
+ id: "tool_confirmation.required_action",
82
+ description: "Tool requires user confirmation before executing",
83
+ },
84
+ { id: "activity.complete", description: "Background activity finished" },
85
+ {
86
+ id: "quick_chat.response_ready",
87
+ description: "Quick chat response ready for review",
88
+ },
89
+ {
90
+ id: "voice.response_ready",
91
+ description: "Voice response ready for playback",
92
+ },
93
+ {
94
+ id: "ride_shotgun.invitation",
95
+ description: "Invitation to ride shotgun on a session",
96
+ },
97
+ ] as const;
98
+
99
+ export type NotificationSourceEventName =
100
+ (typeof NOTIFICATION_SOURCE_EVENT_NAMES)[number]["id"];
101
+
102
+ export function isNotificationSourceEventName(
103
+ value: unknown,
104
+ ): value is NotificationSourceEventName {
105
+ return (
106
+ typeof value === "string" &&
107
+ NOTIFICATION_SOURCE_EVENT_NAMES.some((e) => e.id === value)
108
+ );
109
+ }
110
+
111
+ // ── Attention hints & routing ──────────────────────────────────────────
112
+
9
113
  export interface AttentionHints {
10
114
  requiresAction: boolean;
11
115
  urgency: "low" | "medium" | "high";
@@ -28,7 +132,7 @@ export type NotificationContextPayload<TEventName extends string = string> =
28
132
  export interface NotificationSignal<TEventName extends string = string> {
29
133
  signalId: string;
30
134
  createdAt: number; // epoch ms
31
- sourceChannel: string; // free-form: 'vellum', 'telegram', 'voice', 'scheduler', etc.
135
+ sourceChannel: NotificationSourceChannel; // see NOTIFICATION_SOURCE_CHANNELS registry
32
136
  sourceSessionId: string;
33
137
  sourceEventName: TEventName; // free-form: 'reminder_fired', 'schedule_complete', 'guardian_question', etc.
34
138
  contextPayload: NotificationContextPayload<TEventName>;
@@ -51,6 +51,22 @@ export interface ChannelDestination {
51
51
  channel: NotificationChannel;
52
52
  endpoint?: string;
53
53
  metadata?: Record<string, unknown>;
54
+ /** Stable binding data for channel-scoped conversation continuation. */
55
+ bindingContext?: DestinationBindingContext;
56
+ }
57
+
58
+ /**
59
+ * Binding data that identifies a specific external chat for a channel.
60
+ * Used by conversation pairing to look up or create channel-scoped
61
+ * conversations keyed by (sourceChannel, externalChatId).
62
+ */
63
+ export interface DestinationBindingContext {
64
+ /** The channel this binding belongs to (e.g. "telegram", "sms", "slack"). */
65
+ sourceChannel: NotificationChannel;
66
+ /** The channel-specific chat/thread identifier (e.g. Telegram chat ID, phone number). */
67
+ externalChatId: string;
68
+ /** Optional external user identifier within the chat. */
69
+ externalUserId?: string;
54
70
  }
55
71
 
56
72
  /**
@@ -40,6 +40,7 @@ import { isWorkspaceScopedInvocation } from "./workspace-policy.js";
40
40
  // risk logic is input-deterministic.
41
41
  const RISK_CACHE_MAX = 256;
42
42
  const riskCache = new Map<string, RiskLevel>();
43
+ let riskCacheInvalidationHookRegistered = false;
43
44
 
44
45
  function riskCacheKey(
45
46
  toolName: string,
@@ -63,9 +64,13 @@ export function clearRiskCache(): void {
63
64
  riskCache.clear();
64
65
  }
65
66
 
66
- // Invalidate risk cache whenever trust rules change so that risk decisions
67
- // referencing config-dependent checks (e.g. skill source paths) stay fresh.
68
- onRulesChanged(clearRiskCache);
67
+ function ensureRiskCacheInvalidationHook(): void {
68
+ if (riskCacheInvalidationHookRegistered) return;
69
+ // Register lazily to avoid an ESM initialization cycle between checker and
70
+ // trust-store when a higher-level module imports both during startup.
71
+ riskCacheInvalidationHookRegistered = true;
72
+ onRulesChanged(clearRiskCache);
73
+ }
69
74
 
70
75
  // Low-risk shell programs that are read-only / informational
71
76
  const LOW_RISK_PROGRAMS = new Set([
@@ -462,6 +467,7 @@ export async function classifyRisk(
462
467
  signal?: AbortSignal,
463
468
  ): Promise<RiskLevel> {
464
469
  signal?.throwIfAborted();
470
+ ensureRiskCacheInvalidationHook();
465
471
 
466
472
  // Check cache first (skip when preParsed is provided since caller already
467
473
  // parsed and we'd just be duplicating the key computation cost).
@@ -806,6 +812,26 @@ export async function check(
806
812
  }
807
813
  }
808
814
 
815
+ // Any unrecognized mode (including raw "legacy" that somehow bypassed loader
816
+ // migration) is treated as workspace mode — fail-closed relative to the old
817
+ // risk-only fallthrough that would auto-allow low-risk operations everywhere.
818
+ if (
819
+ permissionsMode !== "strict" &&
820
+ permissionsMode !== "workspace" &&
821
+ !matchedRule &&
822
+ risk !== RiskLevel.High
823
+ ) {
824
+ if (toolName === "bash" && !getConfig().sandbox.enabled) {
825
+ // Fall through to risk-based policy below
826
+ } else if (isWorkspaceScopedInvocation(toolName, input, workingDir)) {
827
+ return {
828
+ decision: "allow",
829
+ reason:
830
+ "Workspace mode (normalized): workspace-scoped operation auto-allowed",
831
+ };
832
+ }
833
+ }
834
+
809
835
  // Auto-allow low-risk bundled skill tools even without explicit trust rules.
810
836
  // These are first-party tools with a vetted risk declaration — applying the
811
837
  // same policy as the per-tool default allow rules for browser tools, but
@@ -19,12 +19,14 @@ interface PendingPrompt {
19
19
  }) => void;
20
20
  reject: (reason: Error) => void;
21
21
  timer: ReturnType<typeof setTimeout>;
22
+ toolUseId?: string;
22
23
  }
23
24
 
24
25
  export type ConfirmationStateCallback = (
25
26
  requestId: string,
26
27
  state: "pending" | "approved" | "denied" | "timed_out" | "resolved_stale",
27
28
  source: "button" | "inline_nl" | "auto_deny" | "timeout" | "system",
29
+ toolUseId?: string,
28
30
  ) => void;
29
31
 
30
32
  export class PermissionPrompter {
@@ -62,6 +64,7 @@ export class PermissionPrompter {
62
64
  persistentDecisionsAllowed?: boolean,
63
65
  signal?: AbortSignal,
64
66
  temporaryOptionsAvailable?: Array<"allow_10m" | "allow_thread">,
67
+ toolUseId?: string,
65
68
  ): Promise<{
66
69
  decision: UserDecision;
67
70
  selectedPattern?: string;
@@ -80,11 +83,11 @@ export class PermissionPrompter {
80
83
  { requestId, toolName },
81
84
  "Permission prompt timed out, defaulting to deny",
82
85
  );
83
- this.onStateChanged?.(requestId, "timed_out", "timeout");
86
+ this.onStateChanged?.(requestId, "timed_out", "timeout", toolUseId);
84
87
  resolve({ decision: "deny" });
85
88
  }, timeoutMs);
86
89
 
87
- this.pending.set(requestId, { resolve, reject, timer });
90
+ this.pending.set(requestId, { resolve, reject, timer, toolUseId });
88
91
 
89
92
  if (signal) {
90
93
  const onAbort = () => {
@@ -120,7 +123,7 @@ export class PermissionPrompter {
120
123
  temporaryOptionsAvailable,
121
124
  });
122
125
 
123
- this.onStateChanged?.(requestId, "pending", "system");
126
+ this.onStateChanged?.(requestId, "pending", "system", toolUseId);
124
127
  });
125
128
  }
126
129
 
@@ -128,6 +131,11 @@ export class PermissionPrompter {
128
131
  return this.pending.has(requestId);
129
132
  }
130
133
 
134
+ /** Returns the toolUseId associated with a pending request, if any. */
135
+ getToolUseId(requestId: string): string | undefined {
136
+ return this.pending.get(requestId)?.toolUseId;
137
+ }
138
+
131
139
  resolveConfirmation(
132
140
  requestId: string,
133
141
  decision: UserDecision,
@@ -26,6 +26,7 @@ import {
26
26
  updateCanonicalGuardianDelivery,
27
27
  } from "../memory/canonical-guardian-store.js";
28
28
  import { emitNotificationSignal } from "../notifications/emit-signal.js";
29
+ import type { NotificationSourceChannel } from "../notifications/signal.js";
29
30
  import type { NotificationDeliveryResult } from "../notifications/types.js";
30
31
  import { getLogger } from "../util/logger.js";
31
32
  import { ensureVellumGuardianBinding } from "./guardian-vellum-migration.js";
@@ -216,7 +217,7 @@ export function notifyGuardianOfAccessRequest(
216
217
  let vellumDeliveryId: string | null = null;
217
218
  void emitNotificationSignal({
218
219
  sourceEventName: "ingress.access_request",
219
- sourceChannel,
220
+ sourceChannel: sourceChannel as NotificationSourceChannel,
220
221
  sourceSessionId: `access-req-${sourceChannel}-${actorExternalId}`,
221
222
  attentionHints: {
222
223
  requiresAction: true,
@@ -166,9 +166,10 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
166
166
  { endpoint: "home-base-ui", scopes: ["settings.read"] },
167
167
  { endpoint: "contacts", scopes: ["settings.read"] },
168
168
  { endpoint: "contacts:POST", scopes: ["settings.write"] },
169
+ { endpoint: "contacts:DELETE", scopes: ["settings.write"] },
169
170
  { endpoint: "contacts/merge", scopes: ["settings.write"] },
170
171
  { endpoint: "contacts:GET", scopes: ["settings.read"] },
171
- { endpoint: "contacts/channels", scopes: ["settings.write"] },
172
+ { endpoint: "contact-channels", scopes: ["settings.write"] },
172
173
  { endpoint: "contacts/invites", scopes: ["settings.read"] },
173
174
  { endpoint: "contacts/invites:POST", scopes: ["settings.write"] },
174
175
  { endpoint: "contacts/invites/redeem", scopes: ["settings.write"] },
@@ -263,6 +264,11 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
263
264
  { endpoint: "apps/shared:DELETE", scopes: ["settings.write"] },
264
265
  { endpoint: "apps/shared/metadata", scopes: ["settings.read"] },
265
266
 
267
+ // Usage / cost telemetry
268
+ { endpoint: "usage/totals", scopes: ["settings.read"] },
269
+ { endpoint: "usage/daily", scopes: ["settings.read"] },
270
+ { endpoint: "usage/breakdown", scopes: ["settings.read"] },
271
+
266
272
  // Debug
267
273
  { endpoint: "debug", scopes: ["settings.read"] },
268
274
 
@@ -2,15 +2,14 @@
2
2
  * Channel invite adapter abstraction.
3
3
  *
4
4
  * Defines an adapter interface for building shareable invite links,
5
- * extracting inbound invite tokens, and generating guardian instructions
5
+ * extracting inbound invite tokens, and resolving channel handles
6
6
  * from channel-specific payloads. Each channel (Telegram, voice, etc.)
7
7
  * registers an adapter that knows how to handle invite flows for that
8
8
  * channel.
9
9
  *
10
- * All methods are optional: a channel that only provides
11
- * `buildGuardianInstruction` (e.g. SMS) is a valid adapter. The adapter
12
- * layer is intentionally thin — redemption logic lives in
13
- * `invite-redemption-service.ts`.
10
+ * All methods are optional the adapter layer is intentionally thin.
11
+ * Redemption logic lives in `invite-redemption-service.ts` and invite
12
+ * instruction generation lives in `invite-instruction-generator.ts`.
14
13
  */
15
14
 
16
15
  import type { ChannelId } from "../channels/types.js";
@@ -26,13 +25,6 @@ export interface InviteShareLink {
26
25
  displayText: string;
27
26
  }
28
27
 
29
- export interface GuardianInstruction {
30
- /** Human-readable instruction text for the guardian. */
31
- instruction: string;
32
- /** Channel-specific handle to reach the assistant (e.g. "@botName", "+15551234567", "hello@domain.agentmail.to"). */
33
- channelHandle?: string;
34
- }
35
-
36
28
  export interface ChannelInviteAdapter {
37
29
  /** The channel this adapter handles. */
38
30
  channel: ChannelId;
@@ -57,16 +49,6 @@ export interface ChannelInviteAdapter {
57
49
  sourceMetadata?: Record<string, unknown>;
58
50
  }): string | undefined;
59
51
 
60
- /**
61
- * Build guardian instruction for this channel. Returns structured data
62
- * with the instruction text and an optional channel-specific handle.
63
- * Optional — falls back to generic instruction if not implemented.
64
- */
65
- buildGuardianInstruction?(params: {
66
- inviteCode: string;
67
- contactName?: string;
68
- }): GuardianInstruction;
69
-
70
52
  /**
71
53
  * Resolve the channel-specific handle to reach the assistant (e.g.
72
54
  * "@botName", "+15551234567", "hello@domain.agentmail.to").
@@ -74,17 +56,14 @@ export interface ChannelInviteAdapter {
74
56
  * credentials not yet configured).
75
57
  */
76
58
  resolveChannelHandle?(): string | undefined;
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // Backward-compatible type aliases
81
- // ---------------------------------------------------------------------------
82
59
 
83
- /** @deprecated Use `ChannelInviteAdapter` instead. */
84
- export type ChannelInviteTransport = ChannelInviteAdapter;
85
-
86
- /** @deprecated Use `InviteShareLink` instead. */
87
- export type InviteSharePayload = InviteShareLink;
60
+ /**
61
+ * Async variant of `resolveChannelHandle` for adapters that need to
62
+ * perform I/O (e.g. querying a provider API for the assigned address).
63
+ * When both are present, `resolveAdapterHandle()` prefers this method.
64
+ */
65
+ resolveChannelHandleAsync?(): Promise<string | undefined>;
66
+ }
88
67
 
89
68
  // ---------------------------------------------------------------------------
90
69
  // Registry
@@ -124,7 +103,32 @@ export class InviteAdapterRegistry {
124
103
  }
125
104
 
126
105
  // ---------------------------------------------------------------------------
127
- // Singleton registry + backward-compatible free functions
106
+ // Handle resolution helper
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Resolve the channel handle for an adapter, preferring the async path
111
+ * when available and falling back to the sync path. Returns `undefined`
112
+ * when the adapter has no handle resolution method or the handle cannot
113
+ * be determined.
114
+ */
115
+ export async function resolveAdapterHandle(
116
+ adapter: ChannelInviteAdapter,
117
+ ): Promise<string | undefined> {
118
+ try {
119
+ if (adapter.resolveChannelHandleAsync) {
120
+ return await adapter.resolveChannelHandleAsync();
121
+ }
122
+ return adapter.resolveChannelHandle?.();
123
+ } catch {
124
+ // Handle resolution is optional metadata — degrade gracefully so
125
+ // callers (e.g. readiness endpoints) don't fail on transient errors.
126
+ return undefined;
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Singleton registry
128
132
  // ---------------------------------------------------------------------------
129
133
 
130
134
  import { emailInviteAdapter } from "./channel-invite-transports/email.js";
@@ -132,6 +136,7 @@ import { slackInviteAdapter } from "./channel-invite-transports/slack.js";
132
136
  import { smsInviteAdapter } from "./channel-invite-transports/sms.js";
133
137
  import { telegramInviteAdapter } from "./channel-invite-transports/telegram.js";
134
138
  import { voiceInviteAdapter } from "./channel-invite-transports/voice.js";
139
+ import { whatsappInviteAdapter } from "./channel-invite-transports/whatsapp.js";
135
140
 
136
141
  /** Create a registry instance with built-in adapters registered. */
137
142
  export function createInviteAdapterRegistry(): InviteAdapterRegistry {
@@ -141,42 +146,14 @@ export function createInviteAdapterRegistry(): InviteAdapterRegistry {
141
146
  registry.register(smsInviteAdapter);
142
147
  registry.register(telegramInviteAdapter);
143
148
  registry.register(voiceInviteAdapter);
149
+ registry.register(whatsappInviteAdapter);
144
150
  return registry;
145
151
  }
146
152
 
147
- /**
148
- * Module-level singleton registry, created eagerly so callers that
149
- * import the free functions continue to work without changes.
150
- */
153
+ /** Module-level singleton registry, created eagerly at import time. */
151
154
  const defaultRegistry = createInviteAdapterRegistry();
152
155
 
153
156
  /** Return the module-level singleton registry. */
154
157
  export function getInviteAdapterRegistry(): InviteAdapterRegistry {
155
158
  return defaultRegistry;
156
159
  }
157
-
158
- /**
159
- * Register a channel invite adapter on the default registry.
160
- * @deprecated Prefer `getInviteAdapterRegistry().register(adapter)`.
161
- */
162
- export function registerTransport(transport: ChannelInviteAdapter): void {
163
- defaultRegistry.register(transport);
164
- }
165
-
166
- /**
167
- * Look up the registered adapter for a channel on the default registry.
168
- * @deprecated Prefer `getInviteAdapterRegistry().get(channel)`.
169
- */
170
- export function getTransport(
171
- channel: ChannelId,
172
- ): ChannelInviteAdapter | undefined {
173
- return defaultRegistry.get(channel);
174
- }
175
-
176
- /**
177
- * Reset the default registry. Intended for tests only.
178
- * @internal
179
- */
180
- export function _resetRegistry(): void {
181
- defaultRegistry._reset();
182
- }
@@ -1,28 +1,19 @@
1
1
  /**
2
2
  * Email channel invite adapter.
3
3
  *
4
- * Provides guardian instruction text for email-based invites. Email invites
5
- * use the universal 6-digit code path for redemption, so this adapter only
6
- * implements `buildGuardianInstruction` no `buildShareLink` or
7
- * `extractInboundToken` needed.
4
+ * Resolves the assistant's email address for use in invite instructions.
5
+ * Uses the EmailService's cached primary inbox lookup so the real address
6
+ * is returned when an inbox is configured. Falls back to `undefined` when
7
+ * no inbox exists, which causes the invite instruction generator to emit
8
+ * generic "on Email" wording instead of a misleading stub address.
9
+ *
10
+ * Email invites use the universal 6-digit code path for redemption, so
11
+ * this adapter only implements `resolveChannelHandleAsync` — no
12
+ * `buildShareLink` or `extractInboundToken` needed.
8
13
  */
9
14
 
10
- import type {
11
- ChannelInviteAdapter,
12
- GuardianInstruction,
13
- } from "../channel-invite-transport.js";
14
-
15
- // ---------------------------------------------------------------------------
16
- // Email address resolution
17
- // ---------------------------------------------------------------------------
18
-
19
- // TODO: resolve from AgentMail provider (async — needs caching or pre-resolution)
20
- // The real implementation requires async inbox lookup via
21
- // `getActiveEmailProvider().health()` which doesn't fit the sync adapter
22
- // interface.
23
- function resolveAssistantEmailAddress(): string | undefined {
24
- return undefined;
25
- }
15
+ import { getEmailService } from "../../email/service.js";
16
+ import type { ChannelInviteAdapter } from "../channel-invite-transport.js";
26
17
 
27
18
  // ---------------------------------------------------------------------------
28
19
  // Adapter implementation
@@ -31,24 +22,7 @@ function resolveAssistantEmailAddress(): string | undefined {
31
22
  export const emailInviteAdapter: ChannelInviteAdapter = {
32
23
  channel: "email",
33
24
 
34
- buildGuardianInstruction(params: {
35
- inviteCode: string;
36
- contactName?: string;
37
- }): GuardianInstruction {
38
- const address = resolveAssistantEmailAddress();
39
- const contactLabel = params.contactName || "the contact";
40
- if (!address) {
41
- return {
42
- instruction: `Tell ${contactLabel} to email the assistant and include the code ${params.inviteCode} in the message.`,
43
- };
44
- }
45
- return {
46
- instruction: `Tell ${contactLabel} to email ${address} and include the code ${params.inviteCode} in the message.`,
47
- channelHandle: address,
48
- };
49
- },
50
-
51
- resolveChannelHandle(): string | undefined {
52
- return resolveAssistantEmailAddress();
25
+ async resolveChannelHandleAsync(): Promise<string | undefined> {
26
+ return getEmailService().getPrimaryInboxAddress();
53
27
  },
54
28
  };
@@ -1,19 +1,15 @@
1
1
  /**
2
2
  * Slack channel invite adapter.
3
3
  *
4
- * Provides guardian instruction text that includes the assistant's Slack
5
- * bot @username and workspace name when available. Slack invites use the
6
- * universal 6-digit code path for redemption, so this adapter only
7
- * implements `buildGuardianInstruction` no `buildShareLink` or
8
- * `extractInboundToken` needed.
4
+ * Resolves the assistant's Slack bot @username for use in invite
5
+ * instructions. Slack invites use the universal 6-digit code path for
6
+ * redemption, so this adapter only implements `resolveChannelHandle` —
7
+ * no `buildShareLink` or `extractInboundToken` needed.
9
8
  */
10
9
 
11
10
  import type { ChannelId } from "../../channels/types.js";
12
11
  import { getCredentialMetadata } from "../../tools/credentials/metadata-store.js";
13
- import type {
14
- ChannelInviteAdapter,
15
- GuardianInstruction,
16
- } from "../channel-invite-transport.js";
12
+ import type { ChannelInviteAdapter } from "../channel-invite-transport.js";
17
13
 
18
14
  // ---------------------------------------------------------------------------
19
15
  // Slack bot info resolution
@@ -54,31 +50,6 @@ function resolveSlackBotInfo(): SlackBotInfo | undefined {
54
50
  export const slackInviteAdapter: ChannelInviteAdapter = {
55
51
  channel: "slack" as ChannelId,
56
52
 
57
- buildGuardianInstruction(params: {
58
- inviteCode: string;
59
- contactName?: string;
60
- }): GuardianInstruction {
61
- const botInfo = resolveSlackBotInfo();
62
- const contactLabel = params.contactName || "the contact";
63
-
64
- if (!botInfo) {
65
- return {
66
- instruction: `Tell ${contactLabel} to message the assistant on Slack and provide the code ${params.inviteCode}.`,
67
- };
68
- }
69
-
70
- let instruction = `Tell ${contactLabel} to message @${botInfo.botUsername} on Slack and provide the code ${params.inviteCode}`;
71
- if (botInfo.teamName) {
72
- instruction += ` (workspace: ${botInfo.teamName})`;
73
- }
74
- instruction += ".";
75
-
76
- return {
77
- instruction,
78
- channelHandle: `@${botInfo.botUsername}`,
79
- };
80
- },
81
-
82
53
  resolveChannelHandle(): string | undefined {
83
54
  const botInfo = resolveSlackBotInfo();
84
55
  if (!botInfo) return undefined;