@vellumai/assistant 0.3.14 → 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 (295) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +2 -2
  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-control-plane-policy.test.ts +1 -3
  40. package/src/__tests__/guardian-outbound-http.test.ts +202 -10
  41. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  42. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  43. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  44. package/src/__tests__/hooks-runner.test.ts +13 -4
  45. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  46. package/src/__tests__/intent-routing.test.ts +14 -0
  47. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  48. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  49. package/src/__tests__/memory-regressions.test.ts +16 -12
  50. package/src/__tests__/non-member-access-request.test.ts +282 -0
  51. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  52. package/src/__tests__/notification-routing-intent.test.ts +11 -2
  53. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  54. package/src/__tests__/recording-intent-fallback.test.ts +0 -1
  55. package/src/__tests__/recording-intent-handler.test.ts +6 -3
  56. package/src/__tests__/recording-intent.test.ts +3 -2
  57. package/src/__tests__/recording-state-machine.test.ts +337 -26
  58. package/src/__tests__/registry.test.ts +17 -8
  59. package/src/__tests__/relay-server.test.ts +105 -0
  60. package/src/__tests__/reminder.test.ts +13 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  63. package/src/__tests__/server-history-render.test.ts +8 -8
  64. package/src/__tests__/session-agent-loop.test.ts +1 -0
  65. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  66. package/src/__tests__/session-skill-tools.test.ts +1 -0
  67. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  68. package/src/__tests__/slack-channel-config.test.ts +230 -0
  69. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  70. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  71. package/src/__tests__/system-prompt.test.ts +43 -0
  72. package/src/__tests__/task-management-tools.test.ts +3 -3
  73. package/src/__tests__/task-tools.test.ts +3 -3
  74. package/src/__tests__/trust-store.test.ts +17 -1
  75. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  76. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  77. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  78. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  79. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  80. package/src/__tests__/update-bulletin.test.ts +260 -0
  81. package/src/__tests__/update-template-contract.test.ts +29 -0
  82. package/src/agent/loop.ts +2 -2
  83. package/src/amazon/client.ts +2 -3
  84. package/src/calls/call-controller.ts +115 -34
  85. package/src/calls/call-conversation-messages.ts +2 -2
  86. package/src/calls/call-domain.ts +10 -3
  87. package/src/calls/call-pointer-messages.ts +17 -5
  88. package/src/calls/guardian-action-sweep.ts +77 -36
  89. package/src/calls/relay-server.ts +51 -12
  90. package/src/calls/twilio-routes.ts +3 -1
  91. package/src/calls/types.ts +1 -1
  92. package/src/calls/voice-session-bridge.ts +4 -4
  93. package/src/cli/core-commands.ts +3 -3
  94. package/src/cli/map.ts +8 -5
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  96. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  97. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  98. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  99. package/src/config/computer-use-prompt.ts +1 -0
  100. package/src/config/core-schema.ts +16 -0
  101. package/src/config/env-registry.ts +1 -0
  102. package/src/config/env.ts +16 -1
  103. package/src/config/memory-schema.ts +5 -0
  104. package/src/config/schema.ts +4 -0
  105. package/src/config/system-prompt.ts +69 -2
  106. package/src/config/templates/BOOTSTRAP.md +1 -1
  107. package/src/config/templates/IDENTITY.md +8 -4
  108. package/src/config/templates/SOUL.md +14 -0
  109. package/src/config/templates/UPDATES.md +16 -0
  110. package/src/config/templates/USER.md +5 -1
  111. package/src/config/types.ts +1 -0
  112. package/src/config/update-bulletin-format.ts +52 -0
  113. package/src/config/update-bulletin-state.ts +49 -0
  114. package/src/config/update-bulletin.ts +82 -0
  115. package/src/config/vellum-skills/catalog.json +6 -0
  116. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  117. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  119. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  120. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  121. package/src/context/window-manager.ts +43 -3
  122. package/src/daemon/config-watcher.ts +1 -0
  123. package/src/daemon/connection-policy.ts +21 -1
  124. package/src/daemon/daemon-control.ts +164 -7
  125. package/src/daemon/date-context.ts +174 -1
  126. package/src/daemon/guardian-action-generators.ts +175 -0
  127. package/src/daemon/guardian-verification-intent.ts +120 -0
  128. package/src/daemon/handlers/apps.ts +1 -3
  129. package/src/daemon/handlers/config-channels.ts +8 -8
  130. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  131. package/src/daemon/handlers/config-inbox.ts +55 -159
  132. package/src/daemon/handlers/config-ingress.ts +1 -1
  133. package/src/daemon/handlers/config-integrations.ts +1 -1
  134. package/src/daemon/handlers/config-platform.ts +1 -1
  135. package/src/daemon/handlers/config-scheduling.ts +2 -2
  136. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  137. package/src/daemon/handlers/config-telegram.ts +1 -1
  138. package/src/daemon/handlers/config-twilio.ts +1 -1
  139. package/src/daemon/handlers/config-voice.ts +100 -0
  140. package/src/daemon/handlers/config.ts +3 -0
  141. package/src/daemon/handlers/index.ts +1 -1
  142. package/src/daemon/handlers/misc.ts +84 -6
  143. package/src/daemon/handlers/navigate-settings.ts +27 -0
  144. package/src/daemon/handlers/recording.ts +270 -144
  145. package/src/daemon/handlers/sessions.ts +107 -24
  146. package/src/daemon/handlers/subagents.ts +3 -3
  147. package/src/daemon/handlers/work-items.ts +10 -7
  148. package/src/daemon/ipc-contract/integrations.ts +9 -1
  149. package/src/daemon/ipc-contract/messages.ts +4 -0
  150. package/src/daemon/ipc-contract/sessions.ts +1 -1
  151. package/src/daemon/ipc-contract/settings.ts +26 -0
  152. package/src/daemon/ipc-contract/shared.ts +2 -0
  153. package/src/daemon/ipc-contract/work-items.ts +1 -7
  154. package/src/daemon/ipc-contract-inventory.json +5 -1
  155. package/src/daemon/ipc-contract.ts +5 -1
  156. package/src/daemon/lifecycle.ts +306 -266
  157. package/src/daemon/recording-executor.ts +1 -1
  158. package/src/daemon/recording-intent.ts +0 -41
  159. package/src/daemon/response-tier.ts +2 -2
  160. package/src/daemon/server.ts +6 -6
  161. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  162. package/src/daemon/session-agent-loop.ts +15 -8
  163. package/src/daemon/session-history.ts +3 -2
  164. package/src/daemon/session-media-retry.ts +3 -0
  165. package/src/daemon/session-messaging.ts +38 -4
  166. package/src/daemon/session-notifiers.ts +2 -2
  167. package/src/daemon/session-process.ts +256 -23
  168. package/src/daemon/session-queue-manager.ts +2 -0
  169. package/src/daemon/session-runtime-assembly.ts +39 -0
  170. package/src/daemon/session-skill-tools.ts +13 -4
  171. package/src/daemon/session-tool-setup.ts +6 -7
  172. package/src/daemon/session.ts +19 -8
  173. package/src/daemon/tls-certs.ts +55 -13
  174. package/src/daemon/tool-side-effects.ts +13 -5
  175. package/src/gallery/default-gallery.ts +32 -9
  176. package/src/influencer/client.ts +2 -1
  177. package/src/memory/channel-delivery-store.ts +37 -567
  178. package/src/memory/channel-guardian-store.ts +66 -1317
  179. package/src/memory/conflict-store.ts +4 -4
  180. package/src/memory/conversation-attention-store.ts +4 -7
  181. package/src/memory/conversation-crud.ts +668 -0
  182. package/src/memory/conversation-queries.ts +361 -0
  183. package/src/memory/conversation-store.ts +45 -983
  184. package/src/memory/db-connection.ts +3 -0
  185. package/src/memory/db-init.ts +25 -0
  186. package/src/memory/delivery-channels.ts +175 -0
  187. package/src/memory/delivery-crud.ts +211 -0
  188. package/src/memory/delivery-status.ts +199 -0
  189. package/src/memory/embedding-backend.ts +70 -4
  190. package/src/memory/embedding-local.ts +12 -2
  191. package/src/memory/entity-extractor.ts +3 -8
  192. package/src/memory/fts-reconciler.ts +121 -0
  193. package/src/memory/guardian-action-store.ts +366 -3
  194. package/src/memory/guardian-approvals.ts +569 -0
  195. package/src/memory/guardian-bindings.ts +130 -0
  196. package/src/memory/guardian-rate-limits.ts +196 -0
  197. package/src/memory/guardian-verification.ts +520 -0
  198. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  199. package/src/memory/job-utils.ts +8 -5
  200. package/src/memory/jobs-store.ts +66 -6
  201. package/src/memory/jobs-worker.ts +23 -1
  202. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  203. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  204. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  205. package/src/memory/migrations/100-core-tables.ts +1 -1
  206. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  207. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  208. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  209. package/src/memory/migrations/113-late-migrations.ts +1 -1
  210. package/src/memory/migrations/116-messages-fts.ts +13 -0
  211. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  212. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  213. package/src/memory/migrations/index.ts +8 -3
  214. package/src/memory/migrations/validate-migration-state.ts +114 -15
  215. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  216. package/src/memory/retriever.ts +46 -13
  217. package/src/memory/schema-migration.ts +3 -0
  218. package/src/memory/schema.ts +25 -7
  219. package/src/memory/search/semantic.ts +8 -90
  220. package/src/notifications/README.md +1 -1
  221. package/src/notifications/broadcaster.ts +20 -2
  222. package/src/notifications/conversation-pairing.ts +3 -3
  223. package/src/notifications/decision-engine.ts +173 -8
  224. package/src/notifications/deliveries-store.ts +27 -8
  225. package/src/notifications/preferences-store.ts +7 -7
  226. package/src/notifications/thread-candidates.ts +234 -0
  227. package/src/notifications/types.ts +18 -0
  228. package/src/permissions/defaults.ts +11 -1
  229. package/src/permissions/prompter.ts +17 -0
  230. package/src/permissions/trust-store.ts +2 -0
  231. package/src/providers/failover.ts +19 -0
  232. package/src/providers/registry.ts +46 -1
  233. package/src/runtime/approval-message-composer.ts +1 -1
  234. package/src/runtime/channel-guardian-service.ts +15 -3
  235. package/src/runtime/channel-retry-sweep.ts +7 -2
  236. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  237. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  238. package/src/runtime/guardian-action-message-composer.ts +245 -0
  239. package/src/runtime/guardian-outbound-actions.ts +35 -15
  240. package/src/runtime/guardian-verification-templates.ts +15 -9
  241. package/src/runtime/http-errors.ts +93 -0
  242. package/src/runtime/http-server.ts +140 -51
  243. package/src/runtime/http-types.ts +53 -0
  244. package/src/runtime/ingress-service.ts +237 -0
  245. package/src/runtime/middleware/error-handler.ts +4 -3
  246. package/src/runtime/middleware/rate-limiter.ts +160 -0
  247. package/src/runtime/middleware/request-logger.ts +71 -0
  248. package/src/runtime/middleware/twilio-validation.ts +7 -6
  249. package/src/runtime/pending-interactions.ts +12 -0
  250. package/src/runtime/routes/access-request-decision.ts +215 -0
  251. package/src/runtime/routes/app-routes.ts +25 -18
  252. package/src/runtime/routes/approval-routes.ts +18 -47
  253. package/src/runtime/routes/attachment-routes.ts +15 -41
  254. package/src/runtime/routes/call-routes.ts +20 -20
  255. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  256. package/src/runtime/routes/contact-routes.ts +4 -9
  257. package/src/runtime/routes/conversation-attention-routes.ts +5 -4
  258. package/src/runtime/routes/conversation-routes.ts +26 -57
  259. package/src/runtime/routes/debug-routes.ts +71 -0
  260. package/src/runtime/routes/events-routes.ts +3 -2
  261. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  262. package/src/runtime/routes/identity-routes.ts +14 -10
  263. package/src/runtime/routes/inbound-conversation.ts +3 -2
  264. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  265. package/src/runtime/routes/ingress-routes.ts +174 -0
  266. package/src/runtime/routes/integration-routes.ts +82 -20
  267. package/src/runtime/routes/pairing-routes.ts +11 -10
  268. package/src/runtime/routes/secret-routes.ts +10 -18
  269. package/src/runtime/verification-rate-limiter.ts +83 -0
  270. package/src/schedule/schedule-store.ts +13 -1
  271. package/src/schedule/scheduler.ts +2 -2
  272. package/src/security/secret-ingress.ts +5 -2
  273. package/src/security/secret-scanner.ts +72 -6
  274. package/src/subagent/manager.ts +6 -4
  275. package/src/swarm/plan-validator.ts +4 -1
  276. package/src/tasks/task-runner.ts +3 -1
  277. package/src/tools/browser/api-map.ts +9 -6
  278. package/src/tools/calls/call-start.ts +20 -0
  279. package/src/tools/executor.ts +50 -568
  280. package/src/tools/permission-checker.ts +272 -0
  281. package/src/tools/registry.ts +14 -6
  282. package/src/tools/reminder/reminder-store.ts +7 -7
  283. package/src/tools/reminder/reminder.ts +6 -3
  284. package/src/tools/secret-detection-handler.ts +301 -0
  285. package/src/tools/subagent/message.ts +1 -1
  286. package/src/tools/system/voice-config.ts +62 -0
  287. package/src/tools/tasks/index.ts +3 -3
  288. package/src/tools/tasks/work-item-list.ts +3 -3
  289. package/src/tools/tasks/work-item-update.ts +4 -5
  290. package/src/tools/tool-approval-handler.ts +192 -0
  291. package/src/tools/tool-manifest.ts +2 -0
  292. package/src/watcher/watcher-store.ts +9 -9
  293. package/src/work-items/work-item-runner.ts +9 -6
  294. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  295. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Shared business logic for ingress member and invite management.
3
+ *
4
+ * Extracted from the IPC handlers in daemon/handlers/config-inbox.ts so that
5
+ * both the HTTP routes and the IPC handlers call the same logic.
6
+ */
7
+
8
+ import {
9
+ createInvite,
10
+ type IngressInvite,
11
+ type InviteStatus,
12
+ listInvites,
13
+ redeemInvite,
14
+ revokeInvite,
15
+ } from '../memory/ingress-invite-store.js';
16
+ import {
17
+ blockMember,
18
+ type IngressMember,
19
+ listMembers,
20
+ type MemberPolicy,
21
+ type MemberStatus,
22
+ revokeMember,
23
+ upsertMember,
24
+ } from '../memory/ingress-member-store.js';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Response shapes — used by both HTTP routes and IPC handlers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface InviteResponseData {
31
+ id: string;
32
+ sourceChannel: string;
33
+ token?: string;
34
+ tokenHash: string;
35
+ maxUses: number;
36
+ useCount: number;
37
+ expiresAt: number | null;
38
+ status: string;
39
+ note?: string;
40
+ createdAt: number;
41
+ }
42
+
43
+ export interface MemberResponseData {
44
+ id: string;
45
+ sourceChannel: string;
46
+ externalUserId?: string;
47
+ externalChatId?: string;
48
+ displayName?: string;
49
+ username?: string;
50
+ status: string;
51
+ policy: string;
52
+ lastSeenAt?: number;
53
+ createdAt: number;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Mappers
58
+ // ---------------------------------------------------------------------------
59
+
60
+ function inviteToResponse(inv: IngressInvite, rawToken?: string): InviteResponseData {
61
+ return {
62
+ id: inv.id,
63
+ sourceChannel: inv.sourceChannel,
64
+ ...(rawToken ? { token: rawToken } : {}),
65
+ tokenHash: inv.tokenHash,
66
+ maxUses: inv.maxUses,
67
+ useCount: inv.useCount,
68
+ expiresAt: inv.expiresAt,
69
+ status: inv.status,
70
+ note: inv.note ?? undefined,
71
+ createdAt: inv.createdAt,
72
+ };
73
+ }
74
+
75
+ export function memberToResponse(m: IngressMember): MemberResponseData {
76
+ return {
77
+ id: m.id,
78
+ sourceChannel: m.sourceChannel,
79
+ externalUserId: m.externalUserId ?? undefined,
80
+ externalChatId: m.externalChatId ?? undefined,
81
+ displayName: m.displayName ?? undefined,
82
+ username: m.username ?? undefined,
83
+ status: m.status,
84
+ policy: m.policy,
85
+ lastSeenAt: m.lastSeenAt ?? undefined,
86
+ createdAt: m.createdAt,
87
+ };
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Result types
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export type IngressResult<T> =
95
+ | { ok: true; data: T }
96
+ | { ok: false; error: string };
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Invite operations
100
+ // ---------------------------------------------------------------------------
101
+
102
+ export function createIngressInvite(params: {
103
+ sourceChannel?: string;
104
+ note?: string;
105
+ maxUses?: number;
106
+ expiresInMs?: number;
107
+ }): IngressResult<InviteResponseData> {
108
+ if (!params.sourceChannel) {
109
+ return { ok: false, error: 'sourceChannel is required for create' };
110
+ }
111
+ const { invite, rawToken } = createInvite({
112
+ sourceChannel: params.sourceChannel,
113
+ note: params.note,
114
+ maxUses: params.maxUses,
115
+ expiresInMs: params.expiresInMs,
116
+ });
117
+ return { ok: true, data: inviteToResponse(invite, rawToken) };
118
+ }
119
+
120
+ export function listIngressInvites(params: {
121
+ sourceChannel?: string;
122
+ status?: string;
123
+ }): IngressResult<InviteResponseData[]> {
124
+ const invites = listInvites({
125
+ sourceChannel: params.sourceChannel,
126
+ status: params.status as InviteStatus | undefined,
127
+ });
128
+ return {
129
+ ok: true,
130
+ data: invites.map((inv) => inviteToResponse(inv)),
131
+ };
132
+ }
133
+
134
+ export function revokeIngressInvite(inviteId?: string): IngressResult<InviteResponseData> {
135
+ if (!inviteId) {
136
+ return { ok: false, error: 'inviteId is required for revoke' };
137
+ }
138
+ const revoked = revokeInvite(inviteId);
139
+ if (!revoked) {
140
+ return { ok: false, error: 'Invite not found or already revoked' };
141
+ }
142
+ return { ok: true, data: inviteToResponse(revoked) };
143
+ }
144
+
145
+ export function redeemIngressInvite(params: {
146
+ token?: string;
147
+ externalUserId?: string;
148
+ externalChatId?: string;
149
+ sourceChannel?: string;
150
+ }): IngressResult<InviteResponseData> {
151
+ if (!params.token) {
152
+ return { ok: false, error: 'token is required for redeem' };
153
+ }
154
+ const result = redeemInvite({
155
+ rawToken: params.token,
156
+ externalUserId: params.externalUserId,
157
+ externalChatId: params.externalChatId,
158
+ sourceChannel: params.sourceChannel,
159
+ });
160
+ if ('error' in result) {
161
+ return { ok: false, error: result.error };
162
+ }
163
+ return { ok: true, data: inviteToResponse(result.invite) };
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Member operations
168
+ // ---------------------------------------------------------------------------
169
+
170
+ export function listIngressMembers(params: {
171
+ assistantId?: string;
172
+ sourceChannel?: string;
173
+ status?: string;
174
+ policy?: string;
175
+ }): IngressResult<MemberResponseData[]> {
176
+ const members = listMembers({
177
+ assistantId: params.assistantId,
178
+ sourceChannel: params.sourceChannel,
179
+ status: params.status as MemberStatus | undefined,
180
+ policy: params.policy as MemberPolicy | undefined,
181
+ });
182
+ return {
183
+ ok: true,
184
+ data: members.map(memberToResponse),
185
+ };
186
+ }
187
+
188
+ export function upsertIngressMember(params: {
189
+ sourceChannel?: string;
190
+ externalUserId?: string;
191
+ externalChatId?: string;
192
+ displayName?: string;
193
+ username?: string;
194
+ policy?: string;
195
+ status?: string;
196
+ assistantId?: string;
197
+ }): IngressResult<MemberResponseData> {
198
+ if (!params.sourceChannel) {
199
+ return { ok: false, error: 'sourceChannel is required for upsert' };
200
+ }
201
+ if (!params.externalUserId && !params.externalChatId) {
202
+ return { ok: false, error: 'At least one of externalUserId or externalChatId is required for upsert' };
203
+ }
204
+ const member = upsertMember({
205
+ assistantId: params.assistantId,
206
+ sourceChannel: params.sourceChannel,
207
+ externalUserId: params.externalUserId,
208
+ externalChatId: params.externalChatId,
209
+ displayName: params.displayName,
210
+ username: params.username,
211
+ policy: params.policy as MemberPolicy | undefined,
212
+ status: params.status as MemberStatus | undefined,
213
+ });
214
+ return { ok: true, data: memberToResponse(member) };
215
+ }
216
+
217
+ export function revokeIngressMember(memberId?: string, reason?: string): IngressResult<MemberResponseData> {
218
+ if (!memberId) {
219
+ return { ok: false, error: 'memberId is required for revoke' };
220
+ }
221
+ const revoked = revokeMember(memberId, reason);
222
+ if (!revoked) {
223
+ return { ok: false, error: 'Member not found or cannot be revoked' };
224
+ }
225
+ return { ok: true, data: memberToResponse(revoked) };
226
+ }
227
+
228
+ export function blockIngressMember(memberId?: string, reason?: string): IngressResult<MemberResponseData> {
229
+ if (!memberId) {
230
+ return { ok: false, error: 'memberId is required for block' };
231
+ }
232
+ const blocked = blockMember(memberId, reason);
233
+ if (!blocked) {
234
+ return { ok: false, error: 'Member not found or already blocked' };
235
+ }
236
+ return { ok: true, data: memberToResponse(blocked) };
237
+ }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { ConfigError, IngressBlockedError } from '../../util/errors.js';
6
6
  import { getLogger } from '../../util/logger.js';
7
+ import { httpError } from '../http-errors.js';
7
8
 
8
9
  const log = getLogger('runtime-http');
9
10
 
@@ -20,14 +21,14 @@ export async function withErrorHandling(
20
21
  } catch (err) {
21
22
  if (err instanceof IngressBlockedError) {
22
23
  log.warn({ endpoint, detectedTypes: err.detectedTypes }, 'Blocked HTTP request containing secrets');
23
- return Response.json({ error: err.message, code: err.code }, { status: 422 });
24
+ return httpError('UNPROCESSABLE_ENTITY', err.message, 422);
24
25
  }
25
26
  if (err instanceof ConfigError) {
26
27
  log.warn({ err, endpoint }, 'Runtime HTTP config error');
27
- return Response.json({ error: err.message, code: err.code }, { status: 422 });
28
+ return httpError('UNPROCESSABLE_ENTITY', err.message, 422);
28
29
  }
29
30
  log.error({ err, endpoint }, 'Runtime HTTP handler error');
30
31
  const message = err instanceof Error ? err.message : 'Internal server error';
31
- return Response.json({ error: message }, { status: 500 });
32
+ return httpError('INTERNAL_ERROR', message, 500);
32
33
  }
33
34
  }
@@ -0,0 +1,160 @@
1
+ // Per-client-IP sliding-window rate limiter for /v1/* API endpoints.
2
+ // Tracks request counts per key and returns 429 when the limit is exceeded.
3
+ // Follows the same sliding-window pattern as gateway/src/auth-rate-limiter.ts.
4
+
5
+ import type { HttpErrorResponse } from '../http-errors.js';
6
+ import { isPrivateAddress } from './auth.js';
7
+
8
+ const DEFAULT_MAX_REQUESTS = 60;
9
+ const DEFAULT_WINDOW_MS = 60_000; // 60 seconds
10
+ const MAX_TRACKED_TOKENS = 10_000;
11
+
12
+ // Lower limit for unauthenticated (IP-based) requests to reduce abuse surface.
13
+ const DEFAULT_IP_MAX_REQUESTS = 20;
14
+ const DEFAULT_IP_WINDOW_MS = 60_000;
15
+ const MAX_TRACKED_IPS = 50_000;
16
+
17
+ export class TokenRateLimiter {
18
+ private requests = new Map<string, number[]>();
19
+ private readonly maxRequests: number;
20
+ private readonly windowMs: number;
21
+ private readonly maxTrackedKeys: number;
22
+
23
+ constructor(
24
+ maxRequests = DEFAULT_MAX_REQUESTS,
25
+ windowMs = DEFAULT_WINDOW_MS,
26
+ maxTrackedKeys = MAX_TRACKED_TOKENS,
27
+ ) {
28
+ this.maxRequests = maxRequests;
29
+ this.windowMs = windowMs;
30
+ this.maxTrackedKeys = maxTrackedKeys;
31
+ }
32
+
33
+ /**
34
+ * Check whether the request should be allowed and record it.
35
+ * Returns rate limit metadata for response headers.
36
+ */
37
+ check(key: string): RateLimitResult {
38
+ const now = Date.now();
39
+ let timestamps = this.requests.get(key);
40
+
41
+ if (!timestamps) {
42
+ if (this.requests.size >= this.maxTrackedKeys) {
43
+ this.evictStale(now);
44
+ if (this.requests.size >= this.maxTrackedKeys) {
45
+ const oldest = this.requests.keys().next().value;
46
+ if (oldest !== undefined) this.requests.delete(oldest);
47
+ }
48
+ }
49
+ timestamps = [];
50
+ this.requests.set(key, timestamps);
51
+ }
52
+
53
+ const cutoff = now - this.windowMs;
54
+
55
+ // Remove expired timestamps from the front
56
+ while (timestamps.length > 0 && timestamps[0] <= cutoff) {
57
+ timestamps.shift();
58
+ }
59
+
60
+ const remaining = Math.max(0, this.maxRequests - timestamps.length);
61
+ const resetAt = timestamps.length > 0
62
+ ? Math.ceil((timestamps[0] + this.windowMs) / 1000)
63
+ : Math.ceil((now + this.windowMs) / 1000);
64
+
65
+ if (timestamps.length >= this.maxRequests) {
66
+ return {
67
+ allowed: false,
68
+ limit: this.maxRequests,
69
+ remaining: 0,
70
+ resetAt,
71
+ };
72
+ }
73
+
74
+ timestamps.push(now);
75
+
76
+ return {
77
+ allowed: true,
78
+ limit: this.maxRequests,
79
+ remaining: remaining - 1,
80
+ resetAt,
81
+ };
82
+ }
83
+
84
+ private evictStale(now: number): void {
85
+ const cutoff = now - this.windowMs;
86
+ for (const [key, timestamps] of this.requests) {
87
+ while (timestamps.length > 0 && timestamps[0] <= cutoff) {
88
+ timestamps.shift();
89
+ }
90
+ if (timestamps.length === 0) {
91
+ this.requests.delete(key);
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ export interface RateLimitResult {
98
+ allowed: boolean;
99
+ limit: number;
100
+ remaining: number;
101
+ /** Unix timestamp (seconds) when the window resets. */
102
+ resetAt: number;
103
+ }
104
+
105
+ /** Build standard rate limit headers from a check result. */
106
+ export function rateLimitHeaders(result: RateLimitResult): Record<string, string> {
107
+ return {
108
+ 'X-RateLimit-Limit': String(result.limit),
109
+ 'X-RateLimit-Remaining': String(result.remaining),
110
+ 'X-RateLimit-Reset': String(result.resetAt),
111
+ };
112
+ }
113
+
114
+ /** Return a 429 response with rate limit headers and a Retry-After hint. */
115
+ export function rateLimitResponse(result: RateLimitResult): Response {
116
+ const retryAfter = Math.max(1, result.resetAt - Math.ceil(Date.now() / 1000));
117
+ const body: HttpErrorResponse = {
118
+ error: { code: 'RATE_LIMITED', message: 'Too Many Requests' },
119
+ };
120
+ return Response.json(body, {
121
+ status: 429,
122
+ headers: {
123
+ ...rateLimitHeaders(result),
124
+ 'Retry-After': String(retryAfter),
125
+ },
126
+ });
127
+ }
128
+
129
+ /** Singleton rate limiter for authenticated /v1/* requests (per-client-IP). */
130
+ export const apiRateLimiter = new TokenRateLimiter();
131
+
132
+ /** Singleton rate limiter for unauthenticated requests (per-IP, lower limits). */
133
+ export const ipRateLimiter = new TokenRateLimiter(DEFAULT_IP_MAX_REQUESTS, DEFAULT_IP_WINDOW_MS, MAX_TRACKED_IPS);
134
+
135
+ /**
136
+ * Extract the client IP from a request. Only trusts proxy headers
137
+ * (X-Forwarded-For, X-Real-IP) when the peer IP is loopback or private,
138
+ * meaning the request arrived via the gateway. Direct connections from
139
+ * external clients use the peer IP, preventing header spoofing.
140
+ */
141
+ export function extractClientIp(
142
+ req: Request,
143
+ server: { requestIP(req: Request): { address: string } | null },
144
+ ): string {
145
+ const peerIp = server.requestIP(req)?.address ?? '0.0.0.0';
146
+
147
+ if (isPrivateAddress(peerIp)) {
148
+ const forwarded = req.headers.get('x-forwarded-for');
149
+ if (forwarded) {
150
+ const first = forwarded.split(',')[0].trim();
151
+ if (first) return first;
152
+ }
153
+
154
+ const realIp = req.headers.get('x-real-ip');
155
+ if (realIp) return realIp.trim();
156
+ }
157
+
158
+ return peerIp;
159
+ }
160
+
@@ -0,0 +1,71 @@
1
+ /**
2
+ * HTTP request/response logging middleware.
3
+ *
4
+ * Logs method, path, status, and latency for every request to aid
5
+ * debugging client issues. Uses structured Pino logging.
6
+ */
7
+
8
+ import { getLogger } from '../../util/logger.js';
9
+
10
+ const log = getLogger('http-request');
11
+
12
+ /**
13
+ * Wrap a request handler to log request metadata and response timing.
14
+ *
15
+ * The handler may return `undefined` for WebSocket upgrades (Bun consumes
16
+ * the request and there is no HTTP response to send).
17
+ */
18
+ export async function withRequestLogging(
19
+ req: Request,
20
+ handler: () => Promise<Response>,
21
+ ): Promise<Response> {
22
+ const start = performance.now();
23
+ const url = new URL(req.url);
24
+ const method = req.method;
25
+ const path = url.pathname;
26
+
27
+ let response: Response;
28
+ try {
29
+ response = await handler();
30
+ } catch (err) {
31
+ const latencyMs = Math.round(performance.now() - start);
32
+ log.error(
33
+ { method, path, latencyMs, err },
34
+ `${method} ${path} -> error (${latencyMs}ms)`,
35
+ );
36
+ throw err;
37
+ }
38
+
39
+ const latencyMs = Math.round(performance.now() - start);
40
+
41
+ // WebSocket upgrades return undefined — log and pass through without
42
+ // dereferencing response properties.
43
+ if (!response) {
44
+ log.info(
45
+ { method, path, latencyMs },
46
+ `${method} ${path} -> ws-upgrade (${latencyMs}ms)`,
47
+ );
48
+ return response;
49
+ }
50
+
51
+ const status = response.status;
52
+
53
+ const logData = {
54
+ method,
55
+ path,
56
+ status,
57
+ latencyMs,
58
+ contentType: req.headers.get('content-type') ?? undefined,
59
+ userAgent: req.headers.get('user-agent') ?? undefined,
60
+ };
61
+
62
+ if (status >= 500) {
63
+ log.error(logData, `${method} ${path} -> ${status} (${latencyMs}ms)`);
64
+ } else if (status >= 400) {
65
+ log.warn(logData, `${method} ${path} -> ${status} (${latencyMs}ms)`);
66
+ } else {
67
+ log.info(logData, `${method} ${path} -> ${status} (${latencyMs}ms)`);
68
+ }
69
+
70
+ return response;
71
+ }
@@ -7,6 +7,7 @@ import { isTwilioWebhookValidationDisabled } from '../../config/env.js';
7
7
  import { loadConfig } from '../../config/loader.js';
8
8
  import { getPublicBaseUrl } from '../../inbound/public-ingress-urls.js';
9
9
  import { getLogger } from '../../util/logger.js';
10
+ import { httpError } from '../http-errors.js';
10
11
 
11
12
  const log = getLogger('runtime-http');
12
13
 
@@ -68,13 +69,13 @@ export async function validateTwilioWebhook(
68
69
  // Fail-closed: reject if no auth token is configured
69
70
  if (!authToken) {
70
71
  log.error('Twilio auth token not configured — rejecting webhook request (fail-closed)');
71
- return Response.json({ error: 'Forbidden' }, { status: 403 });
72
+ return httpError('FORBIDDEN', 'Forbidden', 403);
72
73
  }
73
74
 
74
75
  const signature = req.headers.get('x-twilio-signature');
75
76
  if (!signature) {
76
77
  log.warn('Twilio webhook request missing X-Twilio-Signature header');
77
- return Response.json({ error: 'Forbidden' }, { status: 403 });
78
+ return httpError('FORBIDDEN', 'Forbidden', 403);
78
79
  }
79
80
 
80
81
  // Parse form-urlencoded body into key-value params for signature computation
@@ -85,9 +86,9 @@ export async function validateTwilioWebhook(
85
86
  }
86
87
 
87
88
  // Reconstruct the public-facing URL that Twilio signed against.
88
- // Behind proxies/gateways, req.url is the local server URL (e.g.
89
- // http://127.0.0.1:7821/...) which differs from the public URL Twilio
90
- // used to compute the HMAC-SHA1 signature.
89
+ // Behind proxies/gateways, req.url is the local runtime URL which
90
+ // differs from the public URL Twilio used to compute the HMAC-SHA1
91
+ // signature.
91
92
  let publicBaseUrl: string | undefined;
92
93
  try {
93
94
  publicBaseUrl = getPublicBaseUrl(loadConfig());
@@ -108,7 +109,7 @@ export async function validateTwilioWebhook(
108
109
 
109
110
  if (!isValid) {
110
111
  log.warn('Twilio webhook signature validation failed');
111
- return Response.json({ error: 'Forbidden' }, { status: 403 });
112
+ return httpError('FORBIDDEN', 'Forbidden', 403);
112
113
  }
113
114
 
114
115
  return { body: rawBody };
@@ -67,6 +67,18 @@ export function getByConversation(conversationId: string): Array<{ requestId: st
67
67
  return results;
68
68
  }
69
69
 
70
+ /**
71
+ * Remove all pending interactions for a given session.
72
+ * Used when auto-denying all pending confirmations (e.g. new user message).
73
+ */
74
+ export function removeBySession(session: Session): void {
75
+ for (const [requestId, interaction] of pending) {
76
+ if (interaction.session === session) {
77
+ pending.delete(requestId);
78
+ }
79
+ }
80
+ }
81
+
70
82
  /** Clear all pending interactions. Useful for testing. */
71
83
  export function clear(): void {
72
84
  pending.clear();