@vellumai/assistant 0.4.29 → 0.4.31

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 (237) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/Dockerfile +14 -8
  3. package/README.md +7 -8
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +76 -43
  6. package/package.json +1 -1
  7. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  8. package/scripts/test.sh +1 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  10. package/src/__tests__/actor-token-service.test.ts +4 -3
  11. package/src/__tests__/app-executors.test.ts +7 -17
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  13. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  14. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  15. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  16. package/src/__tests__/channel-approval.test.ts +8 -0
  17. package/src/__tests__/channel-approvals.test.ts +39 -1
  18. package/src/__tests__/channel-guardian.test.ts +15 -5
  19. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  20. package/src/__tests__/config-schema.test.ts +0 -9
  21. package/src/__tests__/conflict-policy.test.ts +76 -0
  22. package/src/__tests__/conflict-store.test.ts +14 -20
  23. package/src/__tests__/contacts-tools.test.ts +8 -61
  24. package/src/__tests__/contradiction-checker.test.ts +5 -1
  25. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  26. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  27. package/src/__tests__/gemini-image-service.test.ts +2 -2
  28. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  29. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  30. package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
  31. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  33. package/src/__tests__/integrations-cli.test.ts +3 -27
  34. package/src/__tests__/intent-routing.test.ts +3 -0
  35. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  36. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  37. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
  39. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  40. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  41. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  42. package/src/__tests__/registry.test.ts +0 -10
  43. package/src/__tests__/relay-server.test.ts +1 -1
  44. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  45. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  46. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  47. package/src/__tests__/session-agent-loop.test.ts +0 -2
  48. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  49. package/src/__tests__/session-media-retry.test.ts +147 -0
  50. package/src/__tests__/session-profile-injection.test.ts +0 -2
  51. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  52. package/src/__tests__/session-skill-tools.test.ts +0 -49
  53. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  54. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  55. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  57. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  58. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  59. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  60. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  61. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  62. package/src/__tests__/slack-skill.test.ts +3 -2
  63. package/src/__tests__/starter-task-flow.test.ts +0 -1
  64. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  65. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  66. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  68. package/src/amazon/client.ts +7 -24
  69. package/src/approvals/guardian-decision-primitive.ts +11 -7
  70. package/src/approvals/guardian-request-resolvers.ts +5 -3
  71. package/src/calls/relay-server.ts +44 -11
  72. package/src/channels/config.ts +1 -1
  73. package/src/cli/integrations.ts +10 -66
  74. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  75. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  76. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  77. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  78. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  79. package/src/config/bundled-skills/contacts/SKILL.md +49 -53
  80. package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
  81. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
  82. package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
  83. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
  84. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  85. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  86. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  87. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  88. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  89. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  90. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  91. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  92. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  93. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  94. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  95. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  96. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  97. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  98. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  99. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  100. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  101. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  102. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  103. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  104. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  105. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  106. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  107. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  108. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  109. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  110. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  111. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  112. package/src/config/bundled-tool-registry.ts +2 -5
  113. package/src/config/channel-permission-profiles.ts +155 -0
  114. package/src/config/env.ts +4 -1
  115. package/src/config/memory-schema.ts +0 -10
  116. package/src/config/system-prompt.ts +6 -0
  117. package/src/contacts/contact-store.ts +221 -56
  118. package/src/contacts/contacts-write.ts +14 -3
  119. package/src/contacts/types.ts +35 -4
  120. package/src/daemon/assistant-attachments.ts +23 -3
  121. package/src/daemon/guardian-verification-intent.ts +7 -4
  122. package/src/daemon/handlers/apps.ts +1 -2
  123. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  124. package/src/daemon/handlers/config-inbox.ts +16 -134
  125. package/src/daemon/handlers/contacts.ts +2 -2
  126. package/src/daemon/handlers/guardian-actions.ts +21 -88
  127. package/src/daemon/handlers/sessions.ts +2 -2
  128. package/src/daemon/ipc-contract/apps.ts +0 -1
  129. package/src/daemon/ipc-contract/contacts.ts +2 -2
  130. package/src/daemon/ipc-contract/inbox.ts +7 -66
  131. package/src/daemon/ipc-contract/sessions.ts +1 -0
  132. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  133. package/src/daemon/ipc-contract-inventory.json +2 -4
  134. package/src/daemon/lifecycle.ts +14 -2
  135. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  136. package/src/daemon/session-agent-loop.ts +2 -45
  137. package/src/daemon/session-attachments.ts +5 -1
  138. package/src/daemon/session-conflict-gate.ts +21 -82
  139. package/src/daemon/session-error.ts +18 -0
  140. package/src/daemon/session-lifecycle.ts +4 -5
  141. package/src/daemon/session-media-retry.ts +15 -1
  142. package/src/daemon/session-memory.ts +7 -52
  143. package/src/daemon/session-process.ts +3 -1
  144. package/src/daemon/session-runtime-assembly.ts +18 -35
  145. package/src/daemon/session-surfaces.ts +0 -1
  146. package/src/daemon/session-tool-setup.ts +7 -4
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/heartbeat/heartbeat-service.ts +5 -1
  149. package/src/home-base/prebuilt/seed.ts +0 -1
  150. package/src/influencer/client.ts +7 -24
  151. package/src/media/gemini-image-service.ts +48 -3
  152. package/src/memory/app-store.ts +0 -4
  153. package/src/memory/conflict-intent.ts +3 -6
  154. package/src/memory/conflict-policy.ts +34 -0
  155. package/src/memory/conflict-store.ts +10 -18
  156. package/src/memory/contradiction-checker.ts +2 -2
  157. package/src/memory/conversation-attention-store.ts +3 -1
  158. package/src/memory/db-init.ts +8 -0
  159. package/src/memory/job-handlers/conflict.ts +0 -7
  160. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  161. package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
  162. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  163. package/src/memory/migrations/index.ts +3 -0
  164. package/src/memory/schema.ts +12 -17
  165. package/src/memory/slack-thread-store.ts +187 -0
  166. package/src/messaging/index.ts +0 -1
  167. package/src/messaging/providers/slack/client.ts +84 -26
  168. package/src/messaging/providers/slack/types.ts +4 -0
  169. package/src/messaging/types.ts +0 -38
  170. package/src/notifications/adapters/slack.ts +90 -0
  171. package/src/notifications/destination-resolver.ts +42 -1
  172. package/src/notifications/emit-signal.ts +17 -1
  173. package/src/oauth/provider-profiles.ts +22 -0
  174. package/src/providers/anthropic/client.ts +3 -0
  175. package/src/providers/openai/client.ts +3 -0
  176. package/src/providers/retry.ts +9 -1
  177. package/src/runtime/actor-trust-resolver.ts +8 -0
  178. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  179. package/src/runtime/auth/route-policy.ts +4 -8
  180. package/src/runtime/channel-approval-types.ts +18 -0
  181. package/src/runtime/channel-approvals.ts +8 -0
  182. package/src/runtime/channel-invite-transport.ts +1 -1
  183. package/src/runtime/channel-reply-delivery.ts +62 -3
  184. package/src/runtime/gateway-client.ts +36 -2
  185. package/src/runtime/gateway-internal-client.ts +86 -0
  186. package/src/runtime/guardian-action-service.ts +128 -0
  187. package/src/runtime/guardian-outbound-actions.ts +3 -3
  188. package/src/runtime/guardian-reply-router.ts +4 -4
  189. package/src/runtime/guardian-verification-templates.ts +16 -1
  190. package/src/runtime/http-server.ts +29 -46
  191. package/src/runtime/invite-redemption-service.ts +1 -1
  192. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  193. package/src/runtime/nl-approval-parser.ts +138 -0
  194. package/src/runtime/routes/approval-routes.ts +1 -40
  195. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  196. package/src/runtime/routes/channel-route-shared.ts +35 -1
  197. package/src/runtime/routes/contact-routes.ts +494 -47
  198. package/src/runtime/routes/conversation-routes.ts +2 -1
  199. package/src/runtime/routes/global-search-routes.ts +2 -2
  200. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  201. package/src/runtime/routes/guardian-approval-interception.ts +78 -1
  202. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
  203. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  204. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
  205. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  206. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  207. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  208. package/src/runtime/routes/migration-routes.ts +17 -17
  209. package/src/runtime/slack-block-formatting.ts +176 -0
  210. package/src/schedule/scheduler.ts +11 -2
  211. package/src/tools/apps/executors.ts +16 -15
  212. package/src/tools/calls/call-end.ts +1 -1
  213. package/src/tools/computer-use/definitions.ts +16 -0
  214. package/src/tools/credentials/vault.ts +86 -2
  215. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  216. package/src/tools/permission-checker.ts +18 -0
  217. package/src/tools/terminal/shell.ts +15 -5
  218. package/src/tools/tool-approval-handler.ts +48 -4
  219. package/src/tools/types.ts +38 -1
  220. package/src/util/errors.ts +5 -1
  221. package/src/util/retry.ts +21 -0
  222. package/src/watcher/providers/slack.ts +33 -3
  223. package/src/workspace/git-service.ts +6 -4
  224. package/src/__tests__/get-weather.test.ts +0 -393
  225. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  226. package/src/autonomy/autonomy-resolver.ts +0 -62
  227. package/src/autonomy/autonomy-store.ts +0 -138
  228. package/src/autonomy/disposition-mapper.ts +0 -31
  229. package/src/autonomy/index.ts +0 -11
  230. package/src/autonomy/types.ts +0 -43
  231. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  232. package/src/config/bundled-skills/weather/TOOLS.json +0 -32
  233. package/src/config/bundled-skills/weather/icon.svg +0 -24
  234. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  235. package/src/messaging/triage-engine.ts +0 -344
  236. package/src/tools/weather/service.ts +0 -712
  237. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -140,6 +140,7 @@ import {
140
140
  handleMergeContacts,
141
141
  handleUpdateContactChannel,
142
142
  handleUpsertContact,
143
+ handleVerifyContactChannel,
143
144
  } from "./routes/contact-routes.js";
144
145
  import { handleListConversationAttention } from "./routes/conversation-attention-routes.js";
145
146
  // Route handlers — grouped by domain
@@ -159,16 +160,6 @@ import {
159
160
  import { handleGuardianBootstrap } from "./routes/guardian-bootstrap-routes.js";
160
161
  import { handleGuardianRefresh } from "./routes/guardian-refresh-routes.js";
161
162
  import { handleGetIdentity, handleHealth } from "./routes/identity-routes.js";
162
- import {
163
- handleBlockMember,
164
- handleCreateInvite,
165
- handleListInvites,
166
- handleListMembers,
167
- handleRedeemInvite,
168
- handleRevokeInvite,
169
- handleRevokeMember,
170
- handleUpsertMember,
171
- } from "./routes/ingress-routes.js";
172
163
  import {
173
164
  handleCancelOutbound,
174
165
  handleClearSlackChannelConfig,
@@ -185,6 +176,12 @@ import {
185
176
  handleSetupTelegram,
186
177
  handleStartOutbound,
187
178
  } from "./routes/integration-routes.js";
179
+ import {
180
+ handleCreateInvite,
181
+ handleListInvites,
182
+ handleRedeemInvite,
183
+ handleRevokeInvite,
184
+ } from "./routes/invite-routes.js";
188
185
  import {
189
186
  handleMigrationExport,
190
187
  handleMigrationImport,
@@ -1133,64 +1130,50 @@ export class RuntimeHttpServer {
1133
1130
  handleUpdateContactChannel(req, params.id, authContext.assistantId),
1134
1131
  },
1135
1132
  {
1136
- endpoint: "contacts/:id",
1137
- method: "GET",
1138
- policyKey: "contacts",
1139
- handler: ({ params, authContext }) =>
1140
- handleGetContact(params.id, authContext.assistantId),
1141
- },
1142
-
1143
- // ------------------------------------------------------------------
1144
- // Ingress contacts
1145
- // ------------------------------------------------------------------
1146
- {
1147
- endpoint: "ingress/members",
1148
- method: "GET",
1149
- handler: ({ url }) => handleListMembers(url),
1150
- },
1151
- {
1152
- endpoint: "ingress/members",
1153
- method: "POST",
1154
- handler: async ({ req }) => handleUpsertMember(req),
1155
- },
1156
- {
1157
- endpoint: "ingress/members/:id/block",
1133
+ endpoint: "contacts/:contactId/channels/:channelId/verify",
1158
1134
  method: "POST",
1159
- policyKey: "ingress/members/block",
1160
- handler: async ({ req, params }) => handleBlockMember(req, params.id),
1161
- },
1162
- {
1163
- endpoint: "ingress/members/:id",
1164
- method: "DELETE",
1165
- policyKey: "ingress/members",
1166
- handler: async ({ req, params }) => handleRevokeMember(req, params.id),
1135
+ policyKey: "contacts/channels",
1136
+ handler: async ({ params, authContext }) =>
1137
+ handleVerifyContactChannel(
1138
+ params.contactId,
1139
+ params.channelId,
1140
+ authContext.assistantId,
1141
+ ),
1167
1142
  },
1168
1143
 
1169
1144
  // ------------------------------------------------------------------
1170
- // Ingress invites
1145
+ // Contacts invites — must precede contacts/:id to avoid shadowing
1171
1146
  // ------------------------------------------------------------------
1172
1147
  {
1173
- endpoint: "ingress/invites",
1148
+ endpoint: "contacts/invites",
1174
1149
  method: "GET",
1175
1150
  handler: ({ url }) => handleListInvites(url),
1176
1151
  },
1177
1152
  {
1178
- endpoint: "ingress/invites",
1153
+ endpoint: "contacts/invites",
1179
1154
  method: "POST",
1180
1155
  handler: async ({ req }) => handleCreateInvite(req),
1181
1156
  },
1182
1157
  {
1183
- endpoint: "ingress/invites/redeem",
1158
+ endpoint: "contacts/invites/redeem",
1184
1159
  method: "POST",
1185
1160
  handler: async ({ req }) => handleRedeemInvite(req),
1186
1161
  },
1187
1162
  {
1188
- endpoint: "ingress/invites/:id",
1163
+ endpoint: "contacts/invites/:id",
1189
1164
  method: "DELETE",
1190
- policyKey: "ingress/invites",
1165
+ policyKey: "contacts/invites",
1191
1166
  handler: ({ params }) => handleRevokeInvite(params.id),
1192
1167
  },
1193
1168
 
1169
+ {
1170
+ endpoint: "contacts/:id",
1171
+ method: "GET",
1172
+ policyKey: "contacts",
1173
+ handler: ({ params, authContext }) =>
1174
+ handleGetContact(params.id, authContext.assistantId),
1175
+ },
1176
+
1194
1177
  // ------------------------------------------------------------------
1195
1178
  // Integrations — Telegram
1196
1179
  // ------------------------------------------------------------------
@@ -17,7 +17,7 @@ import {
17
17
  hashToken,
18
18
  markInviteExpired,
19
19
  recordInviteUse,
20
- } from "../memory/ingress-invite-store.js";
20
+ } from "../memory/invite-store.js";
21
21
  import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
22
22
  import { hashVoiceCode } from "../util/voice-code.js";
23
23
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
@@ -1,23 +1,14 @@
1
1
  /**
2
- * Shared business logic for ingress contact and invite management.
2
+ * Shared business logic for invite management.
3
3
  *
4
4
  * Extracted from the IPC handlers in daemon/handlers/config-inbox.ts so that
5
5
  * both the HTTP routes and the IPC handlers call the same logic.
6
+ *
7
+ * Member/contact operations have been migrated to the /v1/contacts and
8
+ * /v1/contacts/channels endpoints.
6
9
  */
7
10
 
8
11
  import { isChannelId } from "../channels/types.js";
9
- import { listContacts } from "../contacts/contact-store.js";
10
- import {
11
- blockMember,
12
- revokeMember,
13
- upsertMember,
14
- } from "../contacts/contacts-write.js";
15
- import type {
16
- ContactWithChannels,
17
- ContactWriteResult,
18
- MemberPolicy,
19
- MemberStatus,
20
- } from "../contacts/types.js";
21
12
  import {
22
13
  createInvite,
23
14
  findByTokenHash,
@@ -26,10 +17,9 @@ import {
26
17
  type InviteStatus,
27
18
  listInvites,
28
19
  revokeInvite,
29
- } from "../memory/ingress-invite-store.js";
20
+ } from "../memory/invite-store.js";
30
21
  import { isValidE164 } from "../util/phone.js";
31
22
  import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
32
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
33
23
  import { getTransport } from "./channel-invite-transport.js";
34
24
  import {
35
25
  type InviteRedemptionOutcome,
@@ -67,19 +57,6 @@ export interface InviteResponseData {
67
57
  createdAt: number;
68
58
  }
69
59
 
70
- export interface MemberResponseData {
71
- id: string;
72
- sourceChannel: string;
73
- externalUserId?: string;
74
- externalChatId?: string;
75
- displayName?: string;
76
- username?: string;
77
- status: string;
78
- policy: string;
79
- lastSeenAt?: number;
80
- createdAt: number;
81
- }
82
-
83
60
  // ---------------------------------------------------------------------------
84
61
  // Mappers
85
62
  // ---------------------------------------------------------------------------
@@ -133,41 +110,6 @@ function inviteToResponse(
133
110
  };
134
111
  }
135
112
 
136
- function writeResultToResponse(result: ContactWriteResult): MemberResponseData {
137
- return {
138
- id: `${result.contact.id}:${result.channel.id}`,
139
- sourceChannel: result.channel.type,
140
- externalUserId: result.channel.externalUserId ?? undefined,
141
- externalChatId: result.channel.externalChatId ?? undefined,
142
- displayName: result.contact.displayName,
143
- username: undefined,
144
- status:
145
- result.channel.status === "unverified"
146
- ? "pending"
147
- : result.channel.status,
148
- policy: result.channel.policy,
149
- lastSeenAt: result.channel.lastSeenAt ?? undefined,
150
- createdAt: result.channel.createdAt,
151
- };
152
- }
153
-
154
- function contactToMemberResponse(
155
- contact: ContactWithChannels,
156
- ): MemberResponseData[] {
157
- return contact.channels.map((ch) => ({
158
- id: `${contact.id}:${ch.id}`,
159
- sourceChannel: ch.type,
160
- externalUserId: ch.externalUserId ?? undefined,
161
- externalChatId: ch.externalChatId ?? undefined,
162
- displayName: contact.displayName,
163
- username: undefined,
164
- status: ch.status,
165
- policy: ch.policy,
166
- lastSeenAt: ch.lastSeenAt ?? undefined,
167
- createdAt: ch.createdAt,
168
- }));
169
- }
170
-
171
113
  // ---------------------------------------------------------------------------
172
114
  // Result types
173
115
  // ---------------------------------------------------------------------------
@@ -347,97 +289,3 @@ export function redeemVoiceInviteCode(params: {
347
289
  }): VoiceRedemptionOutcome {
348
290
  return redeemVoiceInviteCodeTyped(params);
349
291
  }
350
-
351
- // ---------------------------------------------------------------------------
352
- // Contact operations
353
- // ---------------------------------------------------------------------------
354
-
355
- export function listIngressContacts(params: {
356
- assistantId?: string;
357
- sourceChannel?: string;
358
- status?: string;
359
- policy?: string;
360
- }): IngressResult<MemberResponseData[]> {
361
- // Use uncapped: true since this internal path needs the full dataset
362
- const allContacts = listContacts(
363
- params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
364
- Number.MAX_SAFE_INTEGER,
365
- "contact",
366
- { uncapped: true },
367
- );
368
- const members = allContacts.flatMap((c) => contactToMemberResponse(c));
369
-
370
- const filtered = members.filter((m) => {
371
- if (params.sourceChannel && m.sourceChannel !== params.sourceChannel)
372
- return false;
373
- if (params.status && m.status !== params.status) return false;
374
- if (params.policy && m.policy !== params.policy) return false;
375
- return true;
376
- });
377
-
378
- return { ok: true, data: filtered };
379
- }
380
-
381
- export function upsertIngressContact(params: {
382
- sourceChannel?: string;
383
- externalUserId?: string;
384
- externalChatId?: string;
385
- displayName?: string;
386
- username?: string;
387
- policy?: string;
388
- status?: string;
389
- assistantId?: string;
390
- }): IngressResult<MemberResponseData> {
391
- if (!params.sourceChannel) {
392
- return { ok: false, error: "sourceChannel is required for upsert" };
393
- }
394
- if (!params.externalUserId && !params.externalChatId) {
395
- return {
396
- ok: false,
397
- error:
398
- "At least one of externalUserId or externalChatId is required for upsert",
399
- };
400
- }
401
- const result = upsertMember({
402
- assistantId: params.assistantId,
403
- sourceChannel: params.sourceChannel,
404
- externalUserId: params.externalUserId,
405
- externalChatId: params.externalChatId,
406
- displayName: params.displayName,
407
- username: params.username,
408
- policy: params.policy as MemberPolicy | undefined,
409
- status: params.status as MemberStatus | undefined,
410
- });
411
- if (!result) {
412
- return { ok: false, error: "Failed to upsert member" };
413
- }
414
- return { ok: true, data: writeResultToResponse(result) };
415
- }
416
-
417
- export function revokeIngressContact(
418
- memberId?: string,
419
- reason?: string,
420
- ): IngressResult<MemberResponseData> {
421
- if (!memberId) {
422
- return { ok: false, error: "memberId is required for revoke" };
423
- }
424
- const result = revokeMember(memberId, reason);
425
- if (!result) {
426
- return { ok: false, error: "Member not found or cannot be revoked" };
427
- }
428
- return { ok: true, data: writeResultToResponse(result) };
429
- }
430
-
431
- export function blockIngressContact(
432
- memberId?: string,
433
- reason?: string,
434
- ): IngressResult<MemberResponseData> {
435
- if (!memberId) {
436
- return { ok: false, error: "memberId is required for block" };
437
- }
438
- const result = blockMember(memberId, reason);
439
- if (!result) {
440
- return { ok: false, error: "Member not found or already blocked" };
441
- }
442
- return { ok: true, data: writeResultToResponse(result) };
443
- }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Natural language approval intent parser.
3
+ *
4
+ * Parses short inbound messages (e.g. from Slack) to determine whether the
5
+ * entire message expresses an approval, rejection, or timed-approval intent.
6
+ * Only matches when the full message is an approval/rejection phrase -- does
7
+ * NOT match partial intent inside longer sentences like "yes but also do X".
8
+ *
9
+ * This parser complements the existing `channel-approval-parser.ts`
10
+ * (deterministic phrase-map) by covering a broader set of colloquial
11
+ * patterns, emoji, and timed-approval variants.
12
+ */
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Public types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export type ApprovalDecision = "approve" | "reject" | "approve_10m";
19
+
20
+ export interface ApprovalIntent {
21
+ decision: ApprovalDecision;
22
+ confidence: number;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Pattern definitions
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Exact-match phrases (after normalization) for approval. */
30
+ const APPROVE_EXACT: ReadonlySet<string> = new Set([
31
+ "yes",
32
+ "yep",
33
+ "yeah",
34
+ "yea",
35
+ "yup",
36
+ "approved",
37
+ "approve",
38
+ "go ahead",
39
+ "do it",
40
+ "sure",
41
+ "ok",
42
+ "okay",
43
+ "k",
44
+ "lgtm",
45
+ "sounds good",
46
+ "go for it",
47
+ "please",
48
+ "pls",
49
+ "y",
50
+ "\u{1F44D}", // 👍
51
+ ]);
52
+
53
+ /** Exact-match phrases for rejection. */
54
+ const REJECT_EXACT: ReadonlySet<string> = new Set([
55
+ "no",
56
+ "nope",
57
+ "nah",
58
+ "reject",
59
+ "rejected",
60
+ "denied",
61
+ "deny",
62
+ "don't",
63
+ "dont",
64
+ "cancel",
65
+ "stop",
66
+ "n",
67
+ "\u{1F44E}", // 👎
68
+ ]);
69
+
70
+ /**
71
+ * Patterns for timed approval (e.g. "approve for 10 minutes", "yes for now").
72
+ * Matched after normalization.
73
+ */
74
+ const TIMED_PATTERNS: readonly RegExp[] = [
75
+ /^(?:approve|yes|ok|okay|sure|yep|yeah|go ahead)\s+for\s+10\s*(?:min(?:utes?)?|m)$/,
76
+ /^(?:approve|yes|ok|okay|sure|yep|yeah)\s+for\s+now$/,
77
+ /^approve\s+10\s*(?:min(?:utes?)?|m)$/,
78
+ ];
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Normalization
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Normalize input: lowercase, trim, strip `[ref:...]` disambiguation tags,
86
+ * strip trailing punctuation (except emoji), collapse internal whitespace.
87
+ */
88
+ function normalize(text: string): string {
89
+ return text
90
+ .trim()
91
+ .toLowerCase()
92
+ .replace(/\[ref:[^\]]*\]/g, "")
93
+ .replace(/\s+/g, " ")
94
+ .replace(/[.!?,;:]+$/, "")
95
+ .trim();
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Public API
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Parse a message for approval/rejection intent.
104
+ *
105
+ * Returns an `ApprovalIntent` when the entire message clearly expresses a
106
+ * single approval or rejection decision. Returns `null` when no intent is
107
+ * detected or when the message contains additional content beyond the
108
+ * decision phrase.
109
+ */
110
+ export function parseApprovalIntent(text: string): ApprovalIntent | null {
111
+ const normalized = normalize(text);
112
+
113
+ if (normalized.length === 0) return null;
114
+
115
+ // Reject messages that are too long to be a simple decision phrase.
116
+ // This prevents matching approval words buried inside longer messages.
117
+ // The longest timed-approval phrase is ~30 chars; allow some padding.
118
+ if (normalized.length > 40) return null;
119
+
120
+ // Check timed approval patterns first (more specific).
121
+ for (const pattern of TIMED_PATTERNS) {
122
+ if (pattern.test(normalized)) {
123
+ return { decision: "approve_10m", confidence: 0.95 };
124
+ }
125
+ }
126
+
127
+ // Exact approval match.
128
+ if (APPROVE_EXACT.has(normalized)) {
129
+ return { decision: "approve", confidence: 0.95 };
130
+ }
131
+
132
+ // Exact rejection match.
133
+ if (REJECT_EXACT.has(normalized)) {
134
+ return { decision: "reject", confidence: 0.95 };
135
+ }
136
+
137
+ return null;
138
+ }
@@ -8,57 +8,18 @@
8
8
  * header. Guardian decisions additionally verify that the actor is the
9
9
  * bound guardian.
10
10
  */
11
- import { isHttpAuthDisabled } from "../../config/env.js";
12
- import { findGuardianForChannel } from "../../contacts/contact-store.js";
13
11
  import { getConversationByKey } from "../../memory/conversation-key-store.js";
14
12
  import { addRule } from "../../permissions/trust-store.js";
15
13
  import type { UserDecision } from "../../permissions/types.js";
16
14
  import { getTool } from "../../tools/registry.js";
17
15
  import { getLogger } from "../../util/logger.js";
16
+ import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
18
17
  import type { AuthContext } from "../auth/types.js";
19
18
  import { httpError } from "../http-errors.js";
20
19
  import * as pendingInteractions from "../pending-interactions.js";
21
20
 
22
21
  const log = getLogger("approval-routes");
23
22
 
24
- /**
25
- * Verify the actor from AuthContext is the bound guardian for the vellum channel.
26
- * Returns an error Response if not, or null if allowed.
27
- */
28
- function requireBoundGuardian(authContext: AuthContext): Response | null {
29
- // Dev bypass: when auth is disabled, skip guardian binding check
30
- // (mirrors enforcePolicy dev bypass in route-policy.ts)
31
- if (isHttpAuthDisabled()) {
32
- return null;
33
- }
34
- if (!authContext.actorPrincipalId) {
35
- return httpError(
36
- "FORBIDDEN",
37
- "Actor is not the bound guardian for this channel",
38
- 403,
39
- );
40
- }
41
- const guardianResult = findGuardianForChannel(
42
- "vellum",
43
- authContext.assistantId,
44
- );
45
- if (!guardianResult) {
46
- // No guardian yet — in pre-bootstrap state, allow through
47
- return null;
48
- }
49
- if (
50
- (guardianResult.channel.externalUserId ??
51
- guardianResult.contact.principalId) !== authContext.actorPrincipalId
52
- ) {
53
- return httpError(
54
- "FORBIDDEN",
55
- "Actor is not the bound guardian for this channel",
56
- 403,
57
- );
58
- }
59
- return null;
60
- }
61
-
62
23
  /**
63
24
  * POST /v1/confirm — resolve a pending confirmation by requestId.
64
25
  * Requires AuthContext with guardian-bound actor.
@@ -297,7 +297,8 @@ async function handleCallbackDecision(params: {
297
297
  const result = applyGuardianDecision({
298
298
  approval: guardianApproval,
299
299
  decision: callbackDecision,
300
- actorExternalUserId: actorExternalId,
300
+ actorPrincipalId: undefined, // Callback path — principal not available at this layer
301
+ actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
301
302
  actorChannel: sourceChannel,
302
303
  });
303
304
 
@@ -491,7 +492,8 @@ async function handleConversationalDecision(params: {
491
492
  const result = applyGuardianDecision({
492
493
  approval: targetApproval,
493
494
  decision: engineDecision,
494
- actorExternalUserId: actorExternalId,
495
+ actorPrincipalId: undefined, // Callback path — principal not available at this layer
496
+ actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
495
497
  actorChannel: sourceChannel,
496
498
  });
497
499
 
@@ -675,7 +677,8 @@ async function handleLegacyDecision(params: {
675
677
  const result = applyGuardianDecision({
676
678
  approval: targetLegacyApproval,
677
679
  decision: legacyGuardianDecision,
678
- actorExternalUserId: actorExternalId,
680
+ actorPrincipalId: undefined, // Callback path — principal not available at this layer
681
+ actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
679
682
  actorChannel: sourceChannel,
680
683
  });
681
684
 
@@ -64,10 +64,44 @@ export function parseCallbackData(
64
64
  const source =
65
65
  sourceChannel === "whatsapp"
66
66
  ? ("whatsapp_button" as const)
67
- : ("telegram_button" as const);
67
+ : sourceChannel === "slack"
68
+ ? ("slack_button" as const)
69
+ : ("telegram_button" as const);
68
70
  return { action: action as ApprovalAction, source, requestId };
69
71
  }
70
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // Reaction callback data parser — format: "reaction:<emoji_name>"
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Map of Slack emoji names to approval actions. Multiple emoji names can
79
+ * map to the same action to handle Slack's aliasing (e.g. `+1` and `thumbsup`
80
+ * both represent the thumbs-up emoji).
81
+ */
82
+ const REACTION_EMOJI_MAP: ReadonlyMap<string, ApprovalAction> = new Map([
83
+ ["+1", "approve_once"],
84
+ ["thumbsup", "approve_once"],
85
+ ["-1", "reject"],
86
+ ["thumbsdown", "reject"],
87
+ ["alarm_clock", "approve_10m"],
88
+ ["white_check_mark", "approve_always"],
89
+ ]);
90
+
91
+ /**
92
+ * Parse a `reaction:<emoji_name>` callback data string into an approval
93
+ * decision. Returns null if the emoji is not mapped to any action.
94
+ */
95
+ export function parseReactionCallbackData(
96
+ data: string,
97
+ ): ApprovalDecisionResult | null {
98
+ if (!data.startsWith("reaction:")) return null;
99
+ const emoji = data.slice("reaction:".length);
100
+ const action = REACTION_EMOJI_MAP.get(emoji);
101
+ if (!action) return null;
102
+ return { action, source: "slack_reaction" };
103
+ }
104
+
71
105
  // ---------------------------------------------------------------------------
72
106
  // Context builders
73
107
  // ---------------------------------------------------------------------------