@vellumai/assistant 0.3.15 → 0.3.16

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 (290) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +1 -1
  3. package/README.md +5 -5
  4. package/docs/architecture/http-token-refresh.md +252 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +331 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +16 -14
  22. package/src/__tests__/checker.test.ts +24 -0
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +358 -0
  25. package/src/__tests__/conversation-pairing.test.ts +24 -24
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  39. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  40. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  41. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  43. package/src/__tests__/hooks-runner.test.ts +13 -4
  44. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  45. package/src/__tests__/intent-routing.test.ts +14 -0
  46. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  47. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  48. package/src/__tests__/memory-regressions.test.ts +16 -12
  49. package/src/__tests__/non-member-access-request.test.ts +282 -0
  50. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  51. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  52. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  53. package/src/__tests__/recording-intent.test.ts +1 -0
  54. package/src/__tests__/recording-state-machine.test.ts +328 -17
  55. package/src/__tests__/registry.test.ts +17 -8
  56. package/src/__tests__/relay-server.test.ts +105 -0
  57. package/src/__tests__/reminder.test.ts +13 -0
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  59. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  60. package/src/__tests__/server-history-render.test.ts +8 -8
  61. package/src/__tests__/session-agent-loop.test.ts +1 -0
  62. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  63. package/src/__tests__/session-skill-tools.test.ts +1 -0
  64. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  65. package/src/__tests__/slack-channel-config.test.ts +230 -0
  66. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  67. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  68. package/src/__tests__/system-prompt.test.ts +43 -0
  69. package/src/__tests__/task-management-tools.test.ts +3 -3
  70. package/src/__tests__/task-tools.test.ts +3 -3
  71. package/src/__tests__/trust-store.test.ts +17 -1
  72. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  73. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  74. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  75. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  76. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  77. package/src/__tests__/update-bulletin.test.ts +260 -0
  78. package/src/__tests__/update-template-contract.test.ts +29 -0
  79. package/src/agent/loop.ts +2 -2
  80. package/src/amazon/client.ts +2 -3
  81. package/src/calls/call-controller.ts +115 -34
  82. package/src/calls/call-conversation-messages.ts +2 -2
  83. package/src/calls/call-domain.ts +10 -3
  84. package/src/calls/call-pointer-messages.ts +17 -5
  85. package/src/calls/guardian-action-sweep.ts +77 -36
  86. package/src/calls/relay-server.ts +51 -12
  87. package/src/calls/twilio-routes.ts +3 -1
  88. package/src/calls/types.ts +1 -1
  89. package/src/calls/voice-session-bridge.ts +4 -4
  90. package/src/cli/core-commands.ts +3 -3
  91. package/src/cli/map.ts +8 -5
  92. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  93. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  94. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  95. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  96. package/src/config/computer-use-prompt.ts +1 -0
  97. package/src/config/core-schema.ts +16 -0
  98. package/src/config/env-registry.ts +1 -0
  99. package/src/config/env.ts +16 -1
  100. package/src/config/memory-schema.ts +5 -0
  101. package/src/config/schema.ts +4 -0
  102. package/src/config/system-prompt.ts +69 -2
  103. package/src/config/templates/BOOTSTRAP.md +1 -1
  104. package/src/config/templates/IDENTITY.md +8 -4
  105. package/src/config/templates/SOUL.md +14 -0
  106. package/src/config/templates/UPDATES.md +16 -0
  107. package/src/config/templates/USER.md +5 -1
  108. package/src/config/types.ts +1 -0
  109. package/src/config/update-bulletin-format.ts +52 -0
  110. package/src/config/update-bulletin-state.ts +49 -0
  111. package/src/config/update-bulletin.ts +82 -0
  112. package/src/config/vellum-skills/catalog.json +6 -0
  113. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  114. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  115. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  116. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  117. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  118. package/src/context/window-manager.ts +43 -3
  119. package/src/daemon/config-watcher.ts +1 -0
  120. package/src/daemon/connection-policy.ts +21 -1
  121. package/src/daemon/daemon-control.ts +164 -7
  122. package/src/daemon/date-context.ts +174 -1
  123. package/src/daemon/guardian-action-generators.ts +175 -0
  124. package/src/daemon/guardian-verification-intent.ts +120 -0
  125. package/src/daemon/handlers/apps.ts +1 -3
  126. package/src/daemon/handlers/config-channels.ts +2 -2
  127. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  128. package/src/daemon/handlers/config-inbox.ts +55 -159
  129. package/src/daemon/handlers/config-ingress.ts +1 -1
  130. package/src/daemon/handlers/config-integrations.ts +1 -1
  131. package/src/daemon/handlers/config-platform.ts +1 -1
  132. package/src/daemon/handlers/config-scheduling.ts +2 -2
  133. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  134. package/src/daemon/handlers/config-telegram.ts +1 -1
  135. package/src/daemon/handlers/config-twilio.ts +1 -1
  136. package/src/daemon/handlers/config-voice.ts +100 -0
  137. package/src/daemon/handlers/config.ts +3 -0
  138. package/src/daemon/handlers/misc.ts +83 -5
  139. package/src/daemon/handlers/navigate-settings.ts +27 -0
  140. package/src/daemon/handlers/recording.ts +270 -144
  141. package/src/daemon/handlers/sessions.ts +100 -17
  142. package/src/daemon/handlers/subagents.ts +3 -3
  143. package/src/daemon/handlers/work-items.ts +10 -7
  144. package/src/daemon/ipc-contract/integrations.ts +9 -1
  145. package/src/daemon/ipc-contract/messages.ts +4 -0
  146. package/src/daemon/ipc-contract/sessions.ts +1 -1
  147. package/src/daemon/ipc-contract/settings.ts +26 -0
  148. package/src/daemon/ipc-contract/shared.ts +2 -0
  149. package/src/daemon/ipc-contract/work-items.ts +1 -7
  150. package/src/daemon/ipc-contract-inventory.json +5 -1
  151. package/src/daemon/ipc-contract.ts +5 -1
  152. package/src/daemon/lifecycle.ts +306 -266
  153. package/src/daemon/recording-intent.ts +0 -41
  154. package/src/daemon/response-tier.ts +2 -2
  155. package/src/daemon/server.ts +6 -6
  156. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  157. package/src/daemon/session-agent-loop.ts +15 -8
  158. package/src/daemon/session-history.ts +3 -2
  159. package/src/daemon/session-media-retry.ts +3 -0
  160. package/src/daemon/session-messaging.ts +38 -4
  161. package/src/daemon/session-notifiers.ts +2 -2
  162. package/src/daemon/session-process.ts +256 -23
  163. package/src/daemon/session-queue-manager.ts +2 -0
  164. package/src/daemon/session-runtime-assembly.ts +39 -0
  165. package/src/daemon/session-skill-tools.ts +13 -4
  166. package/src/daemon/session-tool-setup.ts +5 -6
  167. package/src/daemon/session.ts +19 -8
  168. package/src/daemon/tls-certs.ts +55 -13
  169. package/src/daemon/tool-side-effects.ts +13 -5
  170. package/src/gallery/default-gallery.ts +32 -9
  171. package/src/influencer/client.ts +2 -1
  172. package/src/memory/channel-delivery-store.ts +37 -567
  173. package/src/memory/channel-guardian-store.ts +66 -1317
  174. package/src/memory/conflict-store.ts +4 -4
  175. package/src/memory/conversation-attention-store.ts +0 -3
  176. package/src/memory/conversation-crud.ts +668 -0
  177. package/src/memory/conversation-queries.ts +361 -0
  178. package/src/memory/conversation-store.ts +45 -983
  179. package/src/memory/db-connection.ts +3 -0
  180. package/src/memory/db-init.ts +25 -0
  181. package/src/memory/delivery-channels.ts +175 -0
  182. package/src/memory/delivery-crud.ts +211 -0
  183. package/src/memory/delivery-status.ts +199 -0
  184. package/src/memory/embedding-backend.ts +70 -4
  185. package/src/memory/embedding-local.ts +12 -2
  186. package/src/memory/entity-extractor.ts +3 -8
  187. package/src/memory/fts-reconciler.ts +121 -0
  188. package/src/memory/guardian-action-store.ts +366 -3
  189. package/src/memory/guardian-approvals.ts +569 -0
  190. package/src/memory/guardian-bindings.ts +130 -0
  191. package/src/memory/guardian-rate-limits.ts +196 -0
  192. package/src/memory/guardian-verification.ts +520 -0
  193. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  194. package/src/memory/job-utils.ts +8 -5
  195. package/src/memory/jobs-store.ts +66 -6
  196. package/src/memory/jobs-worker.ts +23 -1
  197. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  198. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  199. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  200. package/src/memory/migrations/100-core-tables.ts +1 -1
  201. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  202. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  203. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  204. package/src/memory/migrations/113-late-migrations.ts +1 -1
  205. package/src/memory/migrations/116-messages-fts.ts +13 -0
  206. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  207. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  208. package/src/memory/migrations/index.ts +8 -3
  209. package/src/memory/migrations/validate-migration-state.ts +114 -15
  210. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  211. package/src/memory/retriever.ts +46 -13
  212. package/src/memory/schema-migration.ts +3 -0
  213. package/src/memory/schema.ts +25 -7
  214. package/src/memory/search/semantic.ts +8 -90
  215. package/src/notifications/README.md +1 -1
  216. package/src/notifications/broadcaster.ts +20 -2
  217. package/src/notifications/conversation-pairing.ts +3 -3
  218. package/src/notifications/decision-engine.ts +173 -8
  219. package/src/notifications/deliveries-store.ts +27 -8
  220. package/src/notifications/preferences-store.ts +7 -7
  221. package/src/notifications/thread-candidates.ts +234 -0
  222. package/src/notifications/types.ts +18 -0
  223. package/src/permissions/defaults.ts +11 -1
  224. package/src/permissions/prompter.ts +17 -0
  225. package/src/permissions/trust-store.ts +2 -0
  226. package/src/providers/failover.ts +19 -0
  227. package/src/providers/registry.ts +46 -1
  228. package/src/runtime/approval-message-composer.ts +1 -1
  229. package/src/runtime/channel-guardian-service.ts +15 -3
  230. package/src/runtime/channel-retry-sweep.ts +7 -2
  231. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  232. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  233. package/src/runtime/guardian-action-message-composer.ts +245 -0
  234. package/src/runtime/guardian-outbound-actions.ts +26 -6
  235. package/src/runtime/guardian-verification-templates.ts +15 -9
  236. package/src/runtime/http-errors.ts +93 -0
  237. package/src/runtime/http-server.ts +133 -44
  238. package/src/runtime/http-types.ts +53 -0
  239. package/src/runtime/ingress-service.ts +237 -0
  240. package/src/runtime/middleware/error-handler.ts +4 -3
  241. package/src/runtime/middleware/rate-limiter.ts +160 -0
  242. package/src/runtime/middleware/request-logger.ts +71 -0
  243. package/src/runtime/middleware/twilio-validation.ts +7 -6
  244. package/src/runtime/pending-interactions.ts +12 -0
  245. package/src/runtime/routes/access-request-decision.ts +215 -0
  246. package/src/runtime/routes/app-routes.ts +25 -18
  247. package/src/runtime/routes/approval-routes.ts +18 -47
  248. package/src/runtime/routes/attachment-routes.ts +15 -41
  249. package/src/runtime/routes/call-routes.ts +20 -20
  250. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  251. package/src/runtime/routes/contact-routes.ts +4 -9
  252. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  253. package/src/runtime/routes/conversation-routes.ts +26 -57
  254. package/src/runtime/routes/debug-routes.ts +71 -0
  255. package/src/runtime/routes/events-routes.ts +3 -2
  256. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  257. package/src/runtime/routes/identity-routes.ts +14 -10
  258. package/src/runtime/routes/inbound-conversation.ts +3 -2
  259. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  260. package/src/runtime/routes/ingress-routes.ts +174 -0
  261. package/src/runtime/routes/integration-routes.ts +78 -16
  262. package/src/runtime/routes/pairing-routes.ts +11 -10
  263. package/src/runtime/routes/secret-routes.ts +10 -18
  264. package/src/runtime/verification-rate-limiter.ts +83 -0
  265. package/src/schedule/schedule-store.ts +13 -1
  266. package/src/schedule/scheduler.ts +1 -1
  267. package/src/security/secret-ingress.ts +5 -2
  268. package/src/security/secret-scanner.ts +72 -6
  269. package/src/subagent/manager.ts +6 -4
  270. package/src/swarm/plan-validator.ts +4 -1
  271. package/src/tasks/task-runner.ts +3 -1
  272. package/src/tools/browser/api-map.ts +9 -6
  273. package/src/tools/calls/call-start.ts +20 -0
  274. package/src/tools/executor.ts +50 -568
  275. package/src/tools/permission-checker.ts +272 -0
  276. package/src/tools/registry.ts +14 -6
  277. package/src/tools/reminder/reminder-store.ts +7 -7
  278. package/src/tools/reminder/reminder.ts +6 -3
  279. package/src/tools/secret-detection-handler.ts +301 -0
  280. package/src/tools/subagent/message.ts +1 -1
  281. package/src/tools/system/voice-config.ts +62 -0
  282. package/src/tools/tasks/index.ts +3 -3
  283. package/src/tools/tasks/work-item-list.ts +3 -3
  284. package/src/tools/tasks/work-item-update.ts +4 -5
  285. package/src/tools/tool-approval-handler.ts +192 -0
  286. package/src/tools/tool-manifest.ts +2 -0
  287. package/src/watcher/watcher-store.ts +9 -9
  288. package/src/work-items/work-item-runner.ts +9 -6
  289. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  290. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -0,0 +1,569 @@
1
+ /**
2
+ * Guardian approval request tracking.
3
+ *
4
+ * Approval requests track per-run guardian approval decisions — whether
5
+ * a guardian has approved or denied a tool invocation on behalf of a
6
+ * channel requester.
7
+ */
8
+
9
+ import { and, count, desc, eq, gt, lte } from 'drizzle-orm';
10
+ import { v4 as uuid } from 'uuid';
11
+
12
+ import { getDb } from './db.js';
13
+ import { channelGuardianApprovalRequests } from './schema.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export type ApprovalRequestStatus = 'pending' | 'approved' | 'denied' | 'expired' | 'cancelled';
20
+
21
+ export interface GuardianApprovalRequest {
22
+ id: string;
23
+ runId: string;
24
+ requestId: string | null;
25
+ conversationId: string;
26
+ assistantId: string;
27
+ channel: string;
28
+ requesterExternalUserId: string;
29
+ requesterChatId: string;
30
+ guardianExternalUserId: string;
31
+ guardianChatId: string;
32
+ toolName: string;
33
+ riskLevel: string | null;
34
+ reason: string | null;
35
+ status: ApprovalRequestStatus;
36
+ decidedByExternalUserId: string | null;
37
+ expiresAt: number;
38
+ createdAt: number;
39
+ updatedAt: number;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function rowToApprovalRequest(row: typeof channelGuardianApprovalRequests.$inferSelect): GuardianApprovalRequest {
47
+ return {
48
+ id: row.id,
49
+ runId: row.runId,
50
+ requestId: row.requestId ?? null,
51
+ conversationId: row.conversationId,
52
+ assistantId: row.assistantId,
53
+ channel: row.channel,
54
+ requesterExternalUserId: row.requesterExternalUserId,
55
+ requesterChatId: row.requesterChatId,
56
+ guardianExternalUserId: row.guardianExternalUserId,
57
+ guardianChatId: row.guardianChatId,
58
+ toolName: row.toolName,
59
+ riskLevel: row.riskLevel,
60
+ reason: row.reason,
61
+ status: row.status as ApprovalRequestStatus,
62
+ decidedByExternalUserId: row.decidedByExternalUserId,
63
+ expiresAt: row.expiresAt,
64
+ createdAt: row.createdAt,
65
+ updatedAt: row.updatedAt,
66
+ };
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Operations
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export function createApprovalRequest(params: {
74
+ runId: string;
75
+ requestId?: string;
76
+ conversationId: string;
77
+ assistantId?: string;
78
+ channel: string;
79
+ requesterExternalUserId: string;
80
+ requesterChatId: string;
81
+ guardianExternalUserId: string;
82
+ guardianChatId: string;
83
+ toolName: string;
84
+ riskLevel?: string;
85
+ reason?: string;
86
+ expiresAt: number;
87
+ }): GuardianApprovalRequest {
88
+ const db = getDb();
89
+ const now = Date.now();
90
+ const id = uuid();
91
+
92
+ const row = {
93
+ id,
94
+ runId: params.runId,
95
+ requestId: params.requestId ?? null,
96
+ conversationId: params.conversationId,
97
+ assistantId: params.assistantId ?? 'self',
98
+ channel: params.channel,
99
+ requesterExternalUserId: params.requesterExternalUserId,
100
+ requesterChatId: params.requesterChatId,
101
+ guardianExternalUserId: params.guardianExternalUserId,
102
+ guardianChatId: params.guardianChatId,
103
+ toolName: params.toolName,
104
+ riskLevel: params.riskLevel ?? null,
105
+ reason: params.reason ?? null,
106
+ status: 'pending' as const,
107
+ decidedByExternalUserId: null,
108
+ expiresAt: params.expiresAt,
109
+ createdAt: now,
110
+ updatedAt: now,
111
+ };
112
+
113
+ db.insert(channelGuardianApprovalRequests).values(row).run();
114
+
115
+ return rowToApprovalRequest(row);
116
+ }
117
+
118
+ export function getPendingApprovalForRun(runId: string): GuardianApprovalRequest | null {
119
+ const db = getDb();
120
+ const now = Date.now();
121
+
122
+ const row = db
123
+ .select()
124
+ .from(channelGuardianApprovalRequests)
125
+ .where(
126
+ and(
127
+ eq(channelGuardianApprovalRequests.runId, runId),
128
+ eq(channelGuardianApprovalRequests.status, 'pending'),
129
+ gt(channelGuardianApprovalRequests.expiresAt, now),
130
+ ),
131
+ )
132
+ .get();
133
+
134
+ return row ? rowToApprovalRequest(row) : null;
135
+ }
136
+
137
+ export function getPendingApprovalForRequest(requestId: string): GuardianApprovalRequest | null {
138
+ const db = getDb();
139
+ const now = Date.now();
140
+
141
+ const row = db
142
+ .select()
143
+ .from(channelGuardianApprovalRequests)
144
+ .where(
145
+ and(
146
+ eq(channelGuardianApprovalRequests.requestId, requestId),
147
+ eq(channelGuardianApprovalRequests.status, 'pending'),
148
+ gt(channelGuardianApprovalRequests.expiresAt, now),
149
+ ),
150
+ )
151
+ .get();
152
+
153
+ return row ? rowToApprovalRequest(row) : null;
154
+ }
155
+
156
+ /**
157
+ * Find a pending (status = 'pending') guardian approval request for a run
158
+ * regardless of whether it has expired. Used by the non-guardian gate to
159
+ * detect expired-but-unresolved approvals that should still block the
160
+ * requester from self-approving.
161
+ */
162
+ export function getUnresolvedApprovalForRun(runId: string): GuardianApprovalRequest | null {
163
+ const db = getDb();
164
+
165
+ const row = db
166
+ .select()
167
+ .from(channelGuardianApprovalRequests)
168
+ .where(
169
+ and(
170
+ eq(channelGuardianApprovalRequests.runId, runId),
171
+ eq(channelGuardianApprovalRequests.status, 'pending'),
172
+ ),
173
+ )
174
+ .get();
175
+
176
+ return row ? rowToApprovalRequest(row) : null;
177
+ }
178
+
179
+ export function getUnresolvedApprovalForRequest(requestId: string): GuardianApprovalRequest | null {
180
+ const db = getDb();
181
+
182
+ const row = db
183
+ .select()
184
+ .from(channelGuardianApprovalRequests)
185
+ .where(
186
+ and(
187
+ eq(channelGuardianApprovalRequests.requestId, requestId),
188
+ eq(channelGuardianApprovalRequests.status, 'pending'),
189
+ ),
190
+ )
191
+ .get();
192
+
193
+ return row ? rowToApprovalRequest(row) : null;
194
+ }
195
+
196
+ /**
197
+ * Find a pending guardian approval request by the guardian's chat ID.
198
+ * Used when the guardian sends a decision from their chat.
199
+ *
200
+ * When `assistantId` is provided, the lookup is scoped to that assistant,
201
+ * preventing cross-assistant approval consumption in shared guardian chats.
202
+ */
203
+ export function getPendingApprovalByGuardianChat(
204
+ channel: string,
205
+ guardianChatId: string,
206
+ assistantId?: string,
207
+ ): GuardianApprovalRequest | null {
208
+ const db = getDb();
209
+ const now = Date.now();
210
+
211
+ const conditions = [
212
+ eq(channelGuardianApprovalRequests.channel, channel),
213
+ eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
214
+ eq(channelGuardianApprovalRequests.status, 'pending'),
215
+ gt(channelGuardianApprovalRequests.expiresAt, now),
216
+ ];
217
+ if (assistantId) {
218
+ conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
219
+ }
220
+
221
+ const row = db
222
+ .select()
223
+ .from(channelGuardianApprovalRequests)
224
+ .where(and(...conditions))
225
+ .orderBy(desc(channelGuardianApprovalRequests.createdAt))
226
+ .get();
227
+
228
+ return row ? rowToApprovalRequest(row) : null;
229
+ }
230
+
231
+ /**
232
+ * Find a pending guardian approval request scoped to a specific run,
233
+ * guardian chat, and channel. Used when a callback button provides a run ID,
234
+ * so the decision is applied to exactly the right approval even when
235
+ * multiple approvals target the same guardian chat.
236
+ *
237
+ * When `assistantId` is provided, the lookup is further scoped to that
238
+ * assistant to prevent cross-assistant approval consumption.
239
+ */
240
+ export function getPendingApprovalByRunAndGuardianChat(
241
+ runId: string,
242
+ channel: string,
243
+ guardianChatId: string,
244
+ assistantId?: string,
245
+ ): GuardianApprovalRequest | null {
246
+ const db = getDb();
247
+ const now = Date.now();
248
+
249
+ const conditions = [
250
+ eq(channelGuardianApprovalRequests.runId, runId),
251
+ eq(channelGuardianApprovalRequests.channel, channel),
252
+ eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
253
+ eq(channelGuardianApprovalRequests.status, 'pending'),
254
+ gt(channelGuardianApprovalRequests.expiresAt, now),
255
+ ];
256
+ if (assistantId) {
257
+ conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
258
+ }
259
+
260
+ const row = db
261
+ .select()
262
+ .from(channelGuardianApprovalRequests)
263
+ .where(and(...conditions))
264
+ .get();
265
+
266
+ return row ? rowToApprovalRequest(row) : null;
267
+ }
268
+
269
+ /**
270
+ * Find a pending guardian approval request scoped to a specific requestId,
271
+ * guardian chat, and channel. Used when a callback button provides a requestId,
272
+ * so the decision is applied to exactly the right approval even when
273
+ * multiple approvals target the same guardian chat.
274
+ */
275
+ export function getPendingApprovalByRequestAndGuardianChat(
276
+ requestId: string,
277
+ channel: string,
278
+ guardianChatId: string,
279
+ assistantId?: string,
280
+ ): GuardianApprovalRequest | null {
281
+ const db = getDb();
282
+ const now = Date.now();
283
+
284
+ const conditions = [
285
+ eq(channelGuardianApprovalRequests.requestId, requestId),
286
+ eq(channelGuardianApprovalRequests.channel, channel),
287
+ eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
288
+ eq(channelGuardianApprovalRequests.status, 'pending'),
289
+ gt(channelGuardianApprovalRequests.expiresAt, now),
290
+ ];
291
+ if (assistantId) {
292
+ conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
293
+ }
294
+
295
+ const row = db
296
+ .select()
297
+ .from(channelGuardianApprovalRequests)
298
+ .where(and(...conditions))
299
+ .get();
300
+
301
+ return row ? rowToApprovalRequest(row) : null;
302
+ }
303
+
304
+ /**
305
+ * Return all pending (non-expired) guardian approval requests for a given
306
+ * guardian chat and channel. Used to detect ambiguity when a guardian sends
307
+ * a plain-text decision while multiple approvals are pending.
308
+ *
309
+ * When `assistantId` is provided, the results are scoped to that assistant
310
+ * to prevent cross-assistant approval consumption.
311
+ */
312
+ export function getAllPendingApprovalsByGuardianChat(
313
+ channel: string,
314
+ guardianChatId: string,
315
+ assistantId?: string,
316
+ ): GuardianApprovalRequest[] {
317
+ const db = getDb();
318
+ const now = Date.now();
319
+
320
+ const conditions = [
321
+ eq(channelGuardianApprovalRequests.channel, channel),
322
+ eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
323
+ eq(channelGuardianApprovalRequests.status, 'pending'),
324
+ gt(channelGuardianApprovalRequests.expiresAt, now),
325
+ ];
326
+ if (assistantId) {
327
+ conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
328
+ }
329
+
330
+ const rows = db
331
+ .select()
332
+ .from(channelGuardianApprovalRequests)
333
+ .where(and(...conditions))
334
+ .orderBy(desc(channelGuardianApprovalRequests.createdAt))
335
+ .all();
336
+
337
+ return rows.map(rowToApprovalRequest);
338
+ }
339
+
340
+ /**
341
+ * Return all pending approval requests whose expiresAt has passed.
342
+ * Used by the proactive expiry sweep to auto-deny expired approvals
343
+ * without waiting for requester follow-up traffic.
344
+ */
345
+ export function getExpiredPendingApprovals(): GuardianApprovalRequest[] {
346
+ const db = getDb();
347
+ const now = Date.now();
348
+
349
+ const rows = db
350
+ .select()
351
+ .from(channelGuardianApprovalRequests)
352
+ .where(
353
+ and(
354
+ eq(channelGuardianApprovalRequests.status, 'pending'),
355
+ lte(channelGuardianApprovalRequests.expiresAt, now),
356
+ ),
357
+ )
358
+ .all();
359
+
360
+ return rows.map(rowToApprovalRequest);
361
+ }
362
+
363
+ export function updateApprovalDecision(
364
+ id: string,
365
+ decision: { status: ApprovalRequestStatus; decidedByExternalUserId?: string },
366
+ ): void {
367
+ const db = getDb();
368
+ const now = Date.now();
369
+
370
+ db.update(channelGuardianApprovalRequests)
371
+ .set({
372
+ status: decision.status,
373
+ decidedByExternalUserId: decision.decidedByExternalUserId ?? null,
374
+ updatedAt: now,
375
+ })
376
+ .where(eq(channelGuardianApprovalRequests.id, id))
377
+ .run();
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // Escalation Query Helpers
382
+ // ---------------------------------------------------------------------------
383
+
384
+ /**
385
+ * List approval requests filtered by assistant, and optionally by channel,
386
+ * conversation, and status. Returns a paginated list of escalations.
387
+ */
388
+ export function listPendingApprovalRequests(params: {
389
+ assistantId?: string;
390
+ channel?: string;
391
+ conversationId?: string;
392
+ status?: ApprovalRequestStatus;
393
+ limit?: number;
394
+ offset?: number;
395
+ }): GuardianApprovalRequest[] {
396
+ const db = getDb();
397
+
398
+ const conditions = [
399
+ eq(channelGuardianApprovalRequests.assistantId, params.assistantId ?? 'self'),
400
+ ];
401
+ if (params.channel) {
402
+ conditions.push(eq(channelGuardianApprovalRequests.channel, params.channel));
403
+ }
404
+ if (params.conversationId) {
405
+ conditions.push(eq(channelGuardianApprovalRequests.conversationId, params.conversationId));
406
+ }
407
+ conditions.push(
408
+ eq(channelGuardianApprovalRequests.status, params.status ?? 'pending'),
409
+ );
410
+
411
+ let query = db
412
+ .select()
413
+ .from(channelGuardianApprovalRequests)
414
+ .where(and(...conditions))
415
+ .orderBy(desc(channelGuardianApprovalRequests.createdAt));
416
+
417
+ if (params.limit !== undefined) {
418
+ query = query.limit(params.limit) as typeof query;
419
+ }
420
+ if (params.offset !== undefined) {
421
+ query = query.offset(params.offset) as typeof query;
422
+ }
423
+
424
+ return query.all().map(rowToApprovalRequest);
425
+ }
426
+
427
+ /**
428
+ * Fetch a single approval request by its primary key.
429
+ */
430
+ export function getApprovalRequestById(id: string): GuardianApprovalRequest | null {
431
+ const db = getDb();
432
+
433
+ const row = db
434
+ .select()
435
+ .from(channelGuardianApprovalRequests)
436
+ .where(eq(channelGuardianApprovalRequests.id, id))
437
+ .get();
438
+
439
+ return row ? rowToApprovalRequest(row) : null;
440
+ }
441
+
442
+ /**
443
+ * Fetch a single approval request by run ID (any status).
444
+ * Useful for checking whether a run has an associated approval request.
445
+ */
446
+ export function getApprovalRequestByRunId(runId: string): GuardianApprovalRequest | null {
447
+ const db = getDb();
448
+
449
+ const row = db
450
+ .select()
451
+ .from(channelGuardianApprovalRequests)
452
+ .where(eq(channelGuardianApprovalRequests.runId, runId))
453
+ .orderBy(desc(channelGuardianApprovalRequests.createdAt))
454
+ .get();
455
+
456
+ return row ? rowToApprovalRequest(row) : null;
457
+ }
458
+
459
+ /**
460
+ * Resolve a pending approval request with a decision.
461
+ *
462
+ * Idempotent: if the request is already resolved with the same decision,
463
+ * the existing record is returned unchanged. Returns null if the request
464
+ * does not exist or was resolved with a *different* decision.
465
+ */
466
+ export function resolveApprovalRequest(
467
+ id: string,
468
+ decision: 'approved' | 'denied',
469
+ decidedByExternalUserId?: string,
470
+ ): GuardianApprovalRequest | null {
471
+ const db = getDb();
472
+
473
+ const existing = db
474
+ .select()
475
+ .from(channelGuardianApprovalRequests)
476
+ .where(eq(channelGuardianApprovalRequests.id, id))
477
+ .get();
478
+
479
+ if (!existing) return null;
480
+
481
+ // Idempotent: already resolved with the same decision
482
+ if (existing.status === decision) {
483
+ return rowToApprovalRequest(existing);
484
+ }
485
+
486
+ // Only resolve if currently pending
487
+ if (existing.status !== 'pending') {
488
+ return null;
489
+ }
490
+
491
+ const now = Date.now();
492
+
493
+ db.update(channelGuardianApprovalRequests)
494
+ .set({
495
+ status: decision,
496
+ decidedByExternalUserId: decidedByExternalUserId ?? null,
497
+ updatedAt: now,
498
+ })
499
+ .where(eq(channelGuardianApprovalRequests.id, id))
500
+ .run();
501
+
502
+ return rowToApprovalRequest({
503
+ ...existing,
504
+ status: decision,
505
+ decidedByExternalUserId: decidedByExternalUserId ?? null,
506
+ updatedAt: now,
507
+ });
508
+ }
509
+
510
+ /**
511
+ * Count pending approval requests for a given conversation.
512
+ * Used by thread state projection to compute `pending_escalation_count`.
513
+ */
514
+ export function countPendingByConversation(
515
+ conversationId: string,
516
+ assistantId?: string,
517
+ ): number {
518
+ const db = getDb();
519
+
520
+ const conditions = [
521
+ eq(channelGuardianApprovalRequests.conversationId, conversationId),
522
+ eq(channelGuardianApprovalRequests.status, 'pending'),
523
+ ];
524
+ if (assistantId) {
525
+ conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
526
+ }
527
+
528
+ const result = db
529
+ .select({ count: count() })
530
+ .from(channelGuardianApprovalRequests)
531
+ .where(and(...conditions))
532
+ .get();
533
+
534
+ return result?.count ?? 0;
535
+ }
536
+
537
+ /**
538
+ * Check for an existing pending (non-expired) approval request for a specific
539
+ * requester on a channel. Used to deduplicate access requests — repeated
540
+ * messages from the same non-member should not create duplicate approval
541
+ * requests while one is already pending.
542
+ */
543
+ export function findPendingAccessRequestForRequester(
544
+ assistantId: string,
545
+ channel: string,
546
+ requesterExternalUserId: string,
547
+ toolName: string,
548
+ ): GuardianApprovalRequest | null {
549
+ const db = getDb();
550
+ const now = Date.now();
551
+
552
+ const row = db
553
+ .select()
554
+ .from(channelGuardianApprovalRequests)
555
+ .where(
556
+ and(
557
+ eq(channelGuardianApprovalRequests.assistantId, assistantId),
558
+ eq(channelGuardianApprovalRequests.channel, channel),
559
+ eq(channelGuardianApprovalRequests.requesterExternalUserId, requesterExternalUserId),
560
+ eq(channelGuardianApprovalRequests.toolName, toolName),
561
+ eq(channelGuardianApprovalRequests.status, 'pending'),
562
+ gt(channelGuardianApprovalRequests.expiresAt, now),
563
+ ),
564
+ )
565
+ .orderBy(desc(channelGuardianApprovalRequests.createdAt))
566
+ .get();
567
+
568
+ return row ? rowToApprovalRequest(row) : null;
569
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Guardian binding CRUD operations.
3
+ *
4
+ * A binding records which external user is the designated guardian
5
+ * for a given (assistantId, channel) pair.
6
+ */
7
+
8
+ import { and, eq } from 'drizzle-orm';
9
+ import { v4 as uuid } from 'uuid';
10
+
11
+ import { getDb } from './db.js';
12
+ import { channelGuardianBindings } from './schema.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export type BindingStatus = 'active' | 'revoked';
19
+
20
+ export interface GuardianBinding {
21
+ id: string;
22
+ assistantId: string;
23
+ channel: string;
24
+ guardianExternalUserId: string;
25
+ guardianDeliveryChatId: string;
26
+ status: BindingStatus;
27
+ verifiedAt: number;
28
+ verifiedVia: string;
29
+ metadataJson: string | null;
30
+ createdAt: number;
31
+ updatedAt: number;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function rowToBinding(row: typeof channelGuardianBindings.$inferSelect): GuardianBinding {
39
+ return {
40
+ id: row.id,
41
+ assistantId: row.assistantId,
42
+ channel: row.channel,
43
+ guardianExternalUserId: row.guardianExternalUserId,
44
+ guardianDeliveryChatId: row.guardianDeliveryChatId,
45
+ status: row.status as BindingStatus,
46
+ verifiedAt: row.verifiedAt,
47
+ verifiedVia: row.verifiedVia,
48
+ metadataJson: row.metadataJson,
49
+ createdAt: row.createdAt,
50
+ updatedAt: row.updatedAt,
51
+ };
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Operations
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export function createBinding(params: {
59
+ assistantId: string;
60
+ channel: string;
61
+ guardianExternalUserId: string;
62
+ guardianDeliveryChatId: string;
63
+ verifiedVia?: string;
64
+ metadataJson?: string | null;
65
+ }): GuardianBinding {
66
+ const db = getDb();
67
+ const now = Date.now();
68
+ const id = uuid();
69
+
70
+ const row = {
71
+ id,
72
+ assistantId: params.assistantId,
73
+ channel: params.channel,
74
+ guardianExternalUserId: params.guardianExternalUserId,
75
+ guardianDeliveryChatId: params.guardianDeliveryChatId,
76
+ status: 'active' as const,
77
+ verifiedAt: now,
78
+ verifiedVia: params.verifiedVia ?? 'challenge',
79
+ metadataJson: params.metadataJson ?? null,
80
+ createdAt: now,
81
+ updatedAt: now,
82
+ };
83
+
84
+ db.insert(channelGuardianBindings).values(row).run();
85
+
86
+ return rowToBinding(row);
87
+ }
88
+
89
+ export function getActiveBinding(assistantId: string, channel: string): GuardianBinding | null {
90
+ const db = getDb();
91
+ const row = db
92
+ .select()
93
+ .from(channelGuardianBindings)
94
+ .where(
95
+ and(
96
+ eq(channelGuardianBindings.assistantId, assistantId),
97
+ eq(channelGuardianBindings.channel, channel),
98
+ eq(channelGuardianBindings.status, 'active'),
99
+ ),
100
+ )
101
+ .get();
102
+
103
+ return row ? rowToBinding(row) : null;
104
+ }
105
+
106
+ export function revokeBinding(assistantId: string, channel: string): boolean {
107
+ const db = getDb();
108
+ const now = Date.now();
109
+
110
+ const existing = db
111
+ .select({ id: channelGuardianBindings.id })
112
+ .from(channelGuardianBindings)
113
+ .where(
114
+ and(
115
+ eq(channelGuardianBindings.assistantId, assistantId),
116
+ eq(channelGuardianBindings.channel, channel),
117
+ eq(channelGuardianBindings.status, 'active'),
118
+ ),
119
+ )
120
+ .get();
121
+
122
+ if (!existing) return false;
123
+
124
+ db.update(channelGuardianBindings)
125
+ .set({ status: 'revoked', updatedAt: now })
126
+ .where(eq(channelGuardianBindings.id, existing.id))
127
+ .run();
128
+
129
+ return true;
130
+ }