@vellumai/vellum-gateway 0.7.0 → 0.7.2

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 (162) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +67 -25
  3. package/Dockerfile +2 -0
  4. package/README.md +50 -13
  5. package/bun.lock +16 -2
  6. package/knip.json +3 -1
  7. package/package.json +3 -1
  8. package/src/__tests__/auto-approve-thresholds.test.ts +49 -22
  9. package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
  10. package/src/__tests__/config-file-watcher.test.ts +181 -0
  11. package/src/__tests__/config.test.ts +0 -1
  12. package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
  13. package/src/__tests__/credential-watcher-managed-bootstrap.test.ts +10 -2
  14. package/src/__tests__/credential-watcher.test.ts +30 -2
  15. package/src/__tests__/db-connection-isolation.test.ts +157 -0
  16. package/src/__tests__/fake-assistant-ipc.ts +39 -0
  17. package/src/__tests__/feature-flags-route.test.ts +8 -8
  18. package/src/__tests__/guardian-init-lockfile.test.ts +30 -4
  19. package/src/__tests__/ipc-feature-flag-routes.test.ts +1 -1
  20. package/src/__tests__/live-voice-websocket.test.ts +0 -1
  21. package/src/__tests__/load-guards.test.ts +0 -1
  22. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
  23. package/src/__tests__/oauth-callback.test.ts +0 -1
  24. package/src/__tests__/pair-origin-allowlist.test.ts +155 -0
  25. package/src/__tests__/rate-limit-loopback.test.ts +1 -1
  26. package/src/__tests__/remote-feature-flag-sync.test.ts +47 -7
  27. package/src/__tests__/resolve-assistant.test.ts +0 -1
  28. package/src/__tests__/route-schema-guard.test.ts +42 -6
  29. package/src/__tests__/runtime-client.test.ts +0 -1
  30. package/src/__tests__/runtime-health-proxy.test.ts +0 -1
  31. package/src/__tests__/runtime-proxy-auth.test.ts +0 -1
  32. package/src/__tests__/runtime-proxy.test.ts +0 -1
  33. package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
  34. package/src/__tests__/slack-display-name.test.ts +66 -1
  35. package/src/__tests__/slack-normalize.test.ts +158 -4
  36. package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
  37. package/src/__tests__/slack-socket-mode-catchup.test.ts +857 -0
  38. package/src/__tests__/slack-socket-mode-scopes.test.ts +52 -0
  39. package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +654 -0
  40. package/src/__tests__/stt-stream-websocket.test.ts +0 -1
  41. package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
  42. package/src/__tests__/telegram-send-attachments.test.ts +0 -1
  43. package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
  44. package/src/__tests__/text-verification-helpers.test.ts +136 -0
  45. package/src/__tests__/twilio-media-websocket.test.ts +0 -1
  46. package/src/__tests__/twilio-relay-websocket.test.ts +0 -1
  47. package/src/__tests__/twilio-webhooks.test.ts +220 -3
  48. package/src/__tests__/upstream-transport.test.ts +0 -36
  49. package/src/__tests__/whatsapp-download.test.ts +0 -1
  50. package/src/__tests__/whatsapp-webhook.test.ts +0 -1
  51. package/src/auth/guardian-refresh.ts +4 -18
  52. package/src/auth/ipc-route-policy.ts +217 -0
  53. package/src/backup/backup-key.ts +138 -0
  54. package/src/backup/backup-routes.ts +159 -0
  55. package/src/backup/backup-worker.ts +374 -0
  56. package/src/backup/list-snapshots.ts +97 -0
  57. package/src/backup/local-writer.ts +87 -0
  58. package/src/backup/offsite-writer.ts +182 -0
  59. package/src/backup/paths.ts +123 -0
  60. package/src/backup/stream-crypt.ts +258 -0
  61. package/src/chrome-extension-origins.ts +28 -0
  62. package/src/cli/enable-proxy.ts +0 -1
  63. package/src/config-file-cache.ts +3 -19
  64. package/src/config-file-utils.ts +124 -0
  65. package/src/config-file-watcher.ts +57 -25
  66. package/src/config.ts +4 -7
  67. package/src/db/connection.ts +65 -3
  68. package/src/db/contact-store.ts +30 -1
  69. package/src/db/data-migrations/index.ts +2 -0
  70. package/src/db/data-migrations/m0003-recover-backup-key.ts +71 -0
  71. package/src/db/schema.ts +92 -0
  72. package/src/db/slack-store.ts +144 -11
  73. package/src/feature-flag-registry.json +40 -152
  74. package/src/handlers/handle-inbound.ts +123 -0
  75. package/src/http/middleware/auth.ts +44 -1
  76. package/src/http/middleware/cors.ts +84 -0
  77. package/src/http/middleware/rate-limit.ts +6 -8
  78. package/src/http/routes/auto-approve-thresholds.ts +17 -1
  79. package/src/http/routes/brain-graph-proxy.ts +1 -1
  80. package/src/http/routes/channel-readiness-proxy.ts +2 -2
  81. package/src/http/routes/channel-verification-session-proxy.ts +19 -37
  82. package/src/http/routes/contact-prompt.ts +149 -0
  83. package/src/http/routes/contacts-control-plane-proxy.ts +2 -2
  84. package/src/http/routes/email-webhook.test.ts +0 -1
  85. package/src/http/routes/ipc-runtime-proxy.test.ts +197 -1
  86. package/src/http/routes/ipc-runtime-proxy.ts +95 -0
  87. package/src/http/routes/log-export.test.ts +0 -1
  88. package/src/http/routes/log-tail.test.ts +336 -0
  89. package/src/http/routes/log-tail.ts +87 -0
  90. package/src/http/routes/migration-proxy.ts +1 -2
  91. package/src/http/routes/oauth-apps-proxy.ts +2 -2
  92. package/src/http/routes/oauth-providers-proxy.ts +2 -2
  93. package/src/http/routes/pair.ts +322 -0
  94. package/src/http/routes/privacy-config.ts +65 -79
  95. package/src/http/routes/runtime-health-proxy.ts +2 -2
  96. package/src/http/routes/runtime-proxy.ts +3 -1
  97. package/src/http/routes/slack-control-plane-proxy.ts +3 -20
  98. package/src/http/routes/stt-stream-websocket.ts +2 -3
  99. package/src/http/routes/telegram-control-plane-proxy.ts +2 -2
  100. package/src/http/routes/telegram-webhook.test.ts +0 -1
  101. package/src/http/routes/telegram-webhook.ts +6 -0
  102. package/src/http/routes/trust-rules.suggest.test.ts +25 -0
  103. package/src/http/routes/trust-rules.ts +7 -0
  104. package/src/http/routes/twilio-control-plane-proxy.ts +2 -2
  105. package/src/http/routes/twilio-media-websocket.ts +5 -5
  106. package/src/http/routes/twilio-voice-verify-callback.ts +310 -0
  107. package/src/http/routes/twilio-voice-webhook.test.ts +65 -1
  108. package/src/http/routes/twilio-voice-webhook.ts +45 -1
  109. package/src/http/routes/whatsapp-webhook.test.ts +0 -1
  110. package/src/index.ts +357 -278
  111. package/src/ipc/assistant-client.ts +8 -4
  112. package/src/ipc/contact-handlers.ts +88 -3
  113. package/src/ipc/threshold-handlers.ts +2 -0
  114. package/src/post-assistant-ready.ts +5 -3
  115. package/src/risk/bash-risk-classifier.test.ts +35 -27
  116. package/src/risk/bash-risk-classifier.ts +44 -14
  117. package/src/risk/command-registry/commands/assistant.ts +8 -19
  118. package/src/risk/command-registry.test.ts +0 -15
  119. package/src/risk/risk-classifier-parity.test.ts +1 -3
  120. package/src/runtime/client.ts +58 -3
  121. package/src/schema.ts +277 -104
  122. package/src/slack/normalize.test.ts +98 -0
  123. package/src/slack/normalize.ts +107 -32
  124. package/src/slack/slack-web.ts +213 -0
  125. package/src/slack/socket-mode.ts +701 -39
  126. package/src/telegram/send.test.ts +0 -1
  127. package/src/twilio/validate-webhook.ts +53 -14
  128. package/src/twilio/webhook-sync-trigger.ts +58 -0
  129. package/src/twilio/webhook-sync.test.ts +286 -0
  130. package/src/twilio/webhook-sync.ts +84 -0
  131. package/src/util/is-loopback-address.ts +27 -0
  132. package/src/velay/bridge-utils.ts +228 -0
  133. package/src/velay/client.test.ts +939 -0
  134. package/src/velay/client.ts +555 -0
  135. package/src/velay/http-bridge.test.ts +217 -0
  136. package/src/velay/http-bridge.ts +83 -0
  137. package/src/velay/protocol.ts +178 -0
  138. package/src/velay/test-fake-websocket.ts +69 -0
  139. package/src/velay/websocket-bridge.test.ts +367 -0
  140. package/src/velay/websocket-bridge.ts +324 -0
  141. package/src/verification/binding-helpers.ts +107 -0
  142. package/src/verification/code-parsing.ts +44 -0
  143. package/src/verification/contact-helpers.ts +342 -0
  144. package/src/verification/identity-match.ts +68 -0
  145. package/src/verification/identity.ts +61 -0
  146. package/src/verification/rate-limit-helpers.ts +205 -0
  147. package/src/verification/reply-delivery.ts +109 -0
  148. package/src/verification/session-helpers.ts +164 -0
  149. package/src/verification/text-verification.ts +372 -0
  150. package/src/version.ts +35 -0
  151. package/src/voice/verification.ts +456 -0
  152. package/src/webhook-pipeline.ts +4 -0
  153. package/src/__tests__/browser-relay-websocket.test.ts +0 -698
  154. package/src/__tests__/telegram-only-default.test.ts +0 -133
  155. package/src/auth/capability-tokens.ts +0 -248
  156. package/src/http/routes/browser-extension-pair.ts +0 -455
  157. package/src/http/routes/browser-relay-websocket.ts +0 -381
  158. package/src/http/routes/config-file-utils.ts +0 -73
  159. package/src/ipc/capability-token-handlers.ts +0 -30
  160. package/src/pairing/approved-devices-store.ts +0 -110
  161. package/src/pairing/pairing-routes.ts +0 -379
  162. package/src/pairing/pairing-store.ts +0 -218
package/src/schema.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import packageJson from "../package.json" with { type: "json" };
2
+ import {
3
+ TWILIO_CONNECT_ACTION_WEBHOOK_PATH,
4
+ TWILIO_MEDIA_STREAM_WEBHOOK_PATH,
5
+ TWILIO_RELAY_WEBHOOK_PATH,
6
+ TWILIO_STATUS_WEBHOOK_PATH,
7
+ TWILIO_VOICE_WEBHOOK_PATH,
8
+ } from "@vellumai/service-contracts/twilio-ingress";
2
9
 
3
10
  export function buildSchema(): Record<string, unknown> {
4
11
  return {
@@ -300,7 +307,7 @@ export function buildSchema(): Record<string, unknown> {
300
307
  },
301
308
  },
302
309
  },
303
- "/webhooks/twilio/voice": {
310
+ [TWILIO_VOICE_WEBHOOK_PATH]: {
304
311
  post: {
305
312
  summary: "Twilio voice webhook",
306
313
  description:
@@ -358,7 +365,7 @@ export function buildSchema(): Record<string, unknown> {
358
365
  },
359
366
  },
360
367
  },
361
- "/webhooks/twilio/status": {
368
+ [TWILIO_STATUS_WEBHOOK_PATH]: {
362
369
  post: {
363
370
  summary: "Twilio status webhook",
364
371
  description:
@@ -415,7 +422,7 @@ export function buildSchema(): Record<string, unknown> {
415
422
  },
416
423
  },
417
424
  },
418
- "/webhooks/twilio/connect-action": {
425
+ [TWILIO_CONNECT_ACTION_WEBHOOK_PATH]: {
419
426
  post: {
420
427
  summary: "Twilio connect-action webhook",
421
428
  description:
@@ -472,6 +479,54 @@ export function buildSchema(): Record<string, unknown> {
472
479
  },
473
480
  },
474
481
  },
482
+ "/webhooks/twilio/voice-verify": {
483
+ post: {
484
+ summary: "Twilio voice verification callback",
485
+ description:
486
+ "Receives DTMF digits from Twilio <Gather> during gateway-owned voice verification. Validates the verification code, creates the guardian binding on success, and returns TwiML to either re-prompt or forward to the assistant for ConversationRelay setup.",
487
+ operationId: "twilioVoiceVerifyCallback",
488
+ security: [{ TwilioSignature: [] }],
489
+ parameters: [
490
+ {
491
+ name: "attempt",
492
+ in: "query",
493
+ required: false,
494
+ schema: { type: "integer", default: 0 },
495
+ description: "Zero-based attempt counter for retry tracking.",
496
+ },
497
+ ],
498
+ requestBody: {
499
+ required: true,
500
+ content: {
501
+ "application/x-www-form-urlencoded": {
502
+ schema: {
503
+ type: "object",
504
+ additionalProperties: { type: "string" },
505
+ },
506
+ },
507
+ },
508
+ },
509
+ responses: {
510
+ "200": {
511
+ description:
512
+ "TwiML response — either a re-prompt <Gather>, a failure <Say>, or forwarded ConversationRelay setup.",
513
+ content: {
514
+ "text/xml": {
515
+ schema: { type: "string" },
516
+ },
517
+ },
518
+ },
519
+ "403": {
520
+ description: "Twilio signature validation failed",
521
+ content: {
522
+ "application/json": {
523
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
524
+ },
525
+ },
526
+ },
527
+ },
528
+ },
529
+ },
475
530
  "/webhooks/whatsapp": {
476
531
  get: {
477
532
  summary: "WhatsApp webhook verification",
@@ -914,7 +969,7 @@ export function buildSchema(): Record<string, unknown> {
914
969
  },
915
970
  },
916
971
  },
917
- "/webhooks/twilio/relay": {
972
+ [TWILIO_RELAY_WEBHOOK_PATH]: {
918
973
  get: {
919
974
  summary: "Twilio ConversationRelay WebSocket",
920
975
  description:
@@ -954,7 +1009,7 @@ export function buildSchema(): Record<string, unknown> {
954
1009
  },
955
1010
  },
956
1011
  },
957
- "/webhooks/twilio/media-stream": {
1012
+ [TWILIO_MEDIA_STREAM_WEBHOOK_PATH]: {
958
1013
  get: {
959
1014
  summary: "Twilio Media Stream WebSocket",
960
1015
  description:
@@ -1195,68 +1250,6 @@ export function buildSchema(): Record<string, unknown> {
1195
1250
  },
1196
1251
  },
1197
1252
  },
1198
- "/pairing/register": {
1199
- post: {
1200
- summary: "Register pairing code",
1201
- description:
1202
- "Authenticated gateway endpoint that registers a new pairing code for device linking via the assistant runtime.",
1203
- operationId: "pairingRegister",
1204
- security: [{ BearerAuth: [] }],
1205
- requestBody: {
1206
- required: true,
1207
- content: {
1208
- "application/json": {
1209
- schema: { type: "object", additionalProperties: true },
1210
- },
1211
- },
1212
- },
1213
- responses: {
1214
- "200": { description: "Pairing code registered" },
1215
- "400": { description: "Invalid request payload" },
1216
- "401": {
1217
- description: "Unauthorized — missing or invalid bearer token",
1218
- },
1219
- "502": { description: "Failed to reach assistant runtime" },
1220
- },
1221
- },
1222
- },
1223
- "/pairing/request": {
1224
- post: {
1225
- summary: "Request pairing",
1226
- description:
1227
- "Initiates a pairing request using a pairing code. Auth failures are tracked for rate limiting.",
1228
- operationId: "pairingRequest",
1229
- requestBody: {
1230
- required: true,
1231
- content: {
1232
- "application/json": {
1233
- schema: { type: "object", additionalProperties: true },
1234
- },
1235
- },
1236
- },
1237
- responses: {
1238
- "200": { description: "Pairing request accepted" },
1239
- "400": { description: "Invalid request payload" },
1240
- "401": { description: "Unauthorized" },
1241
- "403": { description: "Pairing code expired or invalid" },
1242
- "502": { description: "Failed to reach assistant runtime" },
1243
- },
1244
- },
1245
- },
1246
- "/pairing/status": {
1247
- get: {
1248
- summary: "Check pairing status",
1249
- description:
1250
- "Checks the current status of a pairing request. Auth failures are tracked for rate limiting.",
1251
- operationId: "pairingStatus",
1252
- responses: {
1253
- "200": { description: "Pairing status returned" },
1254
- "401": { description: "Unauthorized" },
1255
- "403": { description: "Pairing session not found" },
1256
- "502": { description: "Failed to reach assistant runtime" },
1257
- },
1258
- },
1259
- },
1260
1253
  "/v1/integrations/telegram/config": {
1261
1254
  get: {
1262
1255
  summary: "Get Telegram integration config",
@@ -1500,6 +1493,32 @@ export function buildSchema(): Record<string, unknown> {
1500
1493
  },
1501
1494
  },
1502
1495
  },
1496
+ "/v1/contacts/prompt/submit": {
1497
+ post: {
1498
+ summary: "Submit a contact address in response to a prompt",
1499
+ description:
1500
+ "Authenticated gateway endpoint that accepts a contact address submitted by the user in response to a contacts/prompt IPC request. Writes the contact, notifies the daemon to unblock the waiting CLI call.",
1501
+ operationId: "contactsPromptSubmitPost",
1502
+ security: [{ BearerAuth: [] }],
1503
+ requestBody: {
1504
+ required: true,
1505
+ content: {
1506
+ "application/json": {
1507
+ schema: { type: "object", additionalProperties: true },
1508
+ },
1509
+ },
1510
+ },
1511
+ responses: {
1512
+ "200": { description: "Contact created and prompt resolved" },
1513
+ "400": { description: "Invalid request payload" },
1514
+ "401": {
1515
+ description: "Unauthorized — missing or invalid bearer token",
1516
+ },
1517
+ "409": { description: "Channel already exists for this contact" },
1518
+ "503": { description: "Bearer token not configured" },
1519
+ },
1520
+ },
1521
+ },
1503
1522
  "/v1/contact-channels/{contactChannelId}": {
1504
1523
  patch: {
1505
1524
  summary: "Update a contact channel",
@@ -2153,33 +2172,6 @@ export function buildSchema(): Record<string, unknown> {
2153
2172
  },
2154
2173
  },
2155
2174
  },
2156
- "/v1/integrations/slack/channel/oauth-install": {
2157
- post: {
2158
- summary: "Slack OAuth install",
2159
- description:
2160
- "Scope-protected gateway endpoint that initiates the Slack OAuth loopback flow to capture bot and user tokens. This endpoint blocks while the user completes the Slack consent screen (up to 6 minutes). Requires a bearer token with `settings.write` scope.",
2161
- operationId: "slackChannelOAuthInstallPost",
2162
- security: [{ BearerAuth: [] }],
2163
- requestBody: {
2164
- required: false,
2165
- content: {
2166
- "application/json": {
2167
- schema: { type: "object", additionalProperties: true },
2168
- },
2169
- },
2170
- },
2171
- responses: {
2172
- "200": { description: "OAuth install completed successfully" },
2173
- "400": { description: "Invalid request payload" },
2174
- "401": {
2175
- description: "Unauthorized — missing or invalid bearer token",
2176
- },
2177
- "403": { description: "Insufficient scope" },
2178
- "502": { description: "Failed to reach assistant runtime" },
2179
- "504": { description: "Assistant runtime request timed out" },
2180
- },
2181
- },
2182
- },
2183
2175
  "/v1/oauth/providers": {
2184
2176
  get: {
2185
2177
  summary: "List OAuth providers",
@@ -2907,6 +2899,7 @@ export function buildSchema(): Record<string, unknown> {
2907
2899
  properties: {
2908
2900
  interactive: { type: "string" },
2909
2901
  autonomous: { type: "string" },
2902
+ headless: { type: "string" },
2910
2903
  },
2911
2904
  },
2912
2905
  },
@@ -2928,6 +2921,7 @@ export function buildSchema(): Record<string, unknown> {
2928
2921
  properties: {
2929
2922
  interactive: { type: "string" },
2930
2923
  autonomous: { type: "string" },
2924
+ headless: { type: "string" },
2931
2925
  },
2932
2926
  },
2933
2927
  },
@@ -2943,6 +2937,7 @@ export function buildSchema(): Record<string, unknown> {
2943
2937
  properties: {
2944
2938
  interactive: { type: "string" },
2945
2939
  autonomous: { type: "string" },
2940
+ headless: { type: "string" },
2946
2941
  },
2947
2942
  },
2948
2943
  },
@@ -2976,6 +2971,7 @@ export function buildSchema(): Record<string, unknown> {
2976
2971
  properties: {
2977
2972
  interactive: { type: "string" },
2978
2973
  autonomous: { type: "string" },
2974
+ headless: { type: "string" },
2979
2975
  },
2980
2976
  },
2981
2977
  },
@@ -3005,6 +3001,7 @@ export function buildSchema(): Record<string, unknown> {
3005
3001
  properties: {
3006
3002
  interactive: { type: "string" },
3007
3003
  autonomous: { type: "string" },
3004
+ headless: { type: "string" },
3008
3005
  },
3009
3006
  },
3010
3007
  },
@@ -3020,6 +3017,7 @@ export function buildSchema(): Record<string, unknown> {
3020
3017
  properties: {
3021
3018
  interactive: { type: "string" },
3022
3019
  autonomous: { type: "string" },
3020
+ headless: { type: "string" },
3023
3021
  },
3024
3022
  },
3025
3023
  },
@@ -3283,6 +3281,71 @@ export function buildSchema(): Record<string, unknown> {
3283
3281
  },
3284
3282
  },
3285
3283
  },
3284
+ "/v1/logs/tail": {
3285
+ get: {
3286
+ summary: "Tail gateway log entries",
3287
+ description:
3288
+ "Returns the last N structured log entries from the gateway's pino log files, " +
3289
+ "with optional filtering by minimum level and module name.",
3290
+ operationId: "gatewayLogsTail",
3291
+ security: [{ BearerAuth: [] }],
3292
+ parameters: [
3293
+ {
3294
+ name: "n",
3295
+ in: "query",
3296
+ required: false,
3297
+ schema: { type: "integer", minimum: 1, maximum: 1000, default: 10 },
3298
+ description: "Number of log entries to return (1–1000, default: 10)",
3299
+ },
3300
+ {
3301
+ name: "level",
3302
+ in: "query",
3303
+ required: false,
3304
+ schema: {
3305
+ type: "string",
3306
+ enum: ["trace", "debug", "info", "warn", "error", "fatal"],
3307
+ default: "info",
3308
+ },
3309
+ description: "Minimum pino level name (default: info)",
3310
+ },
3311
+ {
3312
+ name: "module",
3313
+ in: "query",
3314
+ required: false,
3315
+ schema: { type: "string" },
3316
+ description: "Filter to exact pino module name",
3317
+ },
3318
+ ],
3319
+ responses: {
3320
+ "200": {
3321
+ description: "Log entries and truncation flag",
3322
+ content: {
3323
+ "application/json": {
3324
+ schema: {
3325
+ type: "object",
3326
+ required: ["lines", "truncated"],
3327
+ properties: {
3328
+ lines: {
3329
+ type: "array",
3330
+ items: { type: "object" },
3331
+ description: "Matching log entries in chronological order",
3332
+ },
3333
+ truncated: {
3334
+ type: "boolean",
3335
+ description: "True if earlier matching entries exist beyond n",
3336
+ },
3337
+ },
3338
+ },
3339
+ },
3340
+ },
3341
+ },
3342
+ "400": { description: "Invalid level parameter" },
3343
+ "401": {
3344
+ description: "Unauthorized — missing or invalid edge JWT",
3345
+ },
3346
+ },
3347
+ },
3348
+ },
3286
3349
  "/v1/trust-rules": {
3287
3350
  get: {
3288
3351
  summary: "List trust rules",
@@ -3716,7 +3779,7 @@ export function buildSchema(): Record<string, unknown> {
3716
3779
  post: {
3717
3780
  summary: "Kick off an async export-to-GCS job",
3718
3781
  description:
3719
- "Transparent proxy to the assistant daemon's `POST /v1/migrations/export-to-gcs` endpoint. The daemon schedules a background export job that streams the workspace backup to the supplied signed GCS upload URL and returns `202 Accepted` with a `job_id`. Callers poll `GET /v1/migrations/jobs/{jobId}` for progress. Registered explicitly (not via the runtime-proxy catch-all) so local/docker teleport works regardless of `runtimeProxyEnabled`.",
3782
+ "Transparent proxy to the assistant daemon's `POST /v1/migrations/export-to-gcs` endpoint. The daemon schedules a background export job that streams the workspace backup to the supplied signed GCS upload URL and returns `202 Accepted` with a `job_id`. Callers poll `GET /v1/migrations/jobs/{jobId}` for progress. Registered explicitly (not via the runtime-proxy catch-all) for dedicated auth and timeout handling.",
3720
3783
  operationId: "migrationExportToGcs",
3721
3784
  security: [{ BearerAuth: [] }],
3722
3785
  requestBody: {
@@ -3755,7 +3818,7 @@ export function buildSchema(): Record<string, unknown> {
3755
3818
  post: {
3756
3819
  summary: "Kick off an async import-from-GCS job",
3757
3820
  description:
3758
- "Transparent proxy to the assistant daemon's `POST /v1/migrations/import-from-gcs` endpoint. The daemon schedules a background import job that fetches a `.vbundle` archive at the supplied `bundle_url` and streams it through the importer, returning `202 Accepted` with a `job_id`. Callers poll `GET /v1/migrations/jobs/{jobId}` for progress. Registered explicitly (not via the runtime-proxy catch-all) so local/docker teleport works regardless of `runtimeProxyEnabled`.",
3821
+ "Transparent proxy to the assistant daemon's `POST /v1/migrations/import-from-gcs` endpoint. The daemon schedules a background import job that fetches a `.vbundle` archive at the supplied `bundle_url` and streams it through the importer, returning `202 Accepted` with a `job_id`. Callers poll `GET /v1/migrations/jobs/{jobId}` for progress. Registered explicitly (not via the runtime-proxy catch-all) for dedicated auth and timeout handling.",
3759
3822
  operationId: "migrationImportFromGcs",
3760
3823
  security: [{ BearerAuth: [] }],
3761
3824
  requestBody: {
@@ -3805,7 +3868,7 @@ export function buildSchema(): Record<string, unknown> {
3805
3868
  get: {
3806
3869
  summary: "Poll unified migration job status",
3807
3870
  description:
3808
- "Transparent proxy to the assistant daemon's `GET /v1/migrations/jobs/{job_id}` endpoint. Returns the current status of an async export-to-GCS or import-from-GCS job tracked by the daemon's migration job registry. Registered explicitly (not via the runtime-proxy catch-all) so local/docker teleport works regardless of `runtimeProxyEnabled`.",
3871
+ "Transparent proxy to the assistant daemon's `GET /v1/migrations/jobs/{job_id}` endpoint. Returns the current status of an async export-to-GCS or import-from-GCS job tracked by the daemon's migration job registry. Registered explicitly (not via the runtime-proxy catch-all) for dedicated auth and timeout handling.",
3809
3872
  operationId: "migrationJobStatus",
3810
3873
  security: [{ BearerAuth: [] }],
3811
3874
  parameters: [
@@ -3923,8 +3986,7 @@ export function buildSchema(): Record<string, unknown> {
3923
3986
  properties: {
3924
3987
  type: {
3925
3988
  type: "string",
3926
- description:
3927
- "Email provider type (e.g. resend, mailgun)",
3989
+ description: "Email provider type (e.g. resend, mailgun)",
3928
3990
  },
3929
3991
  guardian_email: {
3930
3992
  type: "string",
@@ -3938,7 +4000,8 @@ export function buildSchema(): Record<string, unknown> {
3938
4000
  },
3939
4001
  responses: {
3940
4002
  "200": {
3941
- description: "Guardian email channel verified and binding created",
4003
+ description:
4004
+ "Guardian email channel verified and binding created",
3942
4005
  content: {
3943
4006
  "application/json": {
3944
4007
  schema: {
@@ -3994,11 +4057,111 @@ export function buildSchema(): Record<string, unknown> {
3994
4057
  },
3995
4058
  },
3996
4059
  },
4060
+ "/v1/backups": {
4061
+ get: {
4062
+ summary: "List backup snapshots",
4063
+ description:
4064
+ "Lists local and offsite backup snapshots. The gateway owns the backup encryption key and performs all encrypt/decrypt operations.",
4065
+ operationId: "backupsList",
4066
+ security: [{ BearerAuth: [] }],
4067
+ responses: {
4068
+ "200": {
4069
+ description: "Backup snapshots listed",
4070
+ content: {
4071
+ "application/json": {
4072
+ schema: {
4073
+ type: "object",
4074
+ required: ["local", "offsite"],
4075
+ properties: {
4076
+ local: {
4077
+ type: "object",
4078
+ required: ["directory", "snapshots"],
4079
+ properties: {
4080
+ directory: { type: "string" },
4081
+ snapshots: {
4082
+ type: "array",
4083
+ items: {
4084
+ $ref: "#/components/schemas/BackupSnapshot",
4085
+ },
4086
+ },
4087
+ },
4088
+ },
4089
+ offsite: {
4090
+ type: "array",
4091
+ items: {
4092
+ type: "object",
4093
+ required: ["directory", "encrypted", "snapshots"],
4094
+ properties: {
4095
+ directory: { type: "string" },
4096
+ encrypted: { type: "boolean" },
4097
+ snapshots: {
4098
+ type: "array",
4099
+ items: {
4100
+ $ref: "#/components/schemas/BackupSnapshot",
4101
+ },
4102
+ },
4103
+ },
4104
+ },
4105
+ },
4106
+ },
4107
+ },
4108
+ },
4109
+ },
4110
+ },
4111
+ "401": {
4112
+ description: "Unauthorized — missing or invalid bearer token",
4113
+ },
4114
+ "403": { description: "Insufficient scope" },
4115
+ "500": { description: "Internal server error" },
4116
+ },
4117
+ },
4118
+ },
4119
+ "/v1/backups/create": {
4120
+ post: {
4121
+ summary: "Create backup snapshot",
4122
+ description:
4123
+ "Triggers a manual backup snapshot. The gateway exports a plaintext vbundle from the daemon, writes it locally, and encrypts + mirrors to offsite destinations.",
4124
+ operationId: "backupsCreate",
4125
+ security: [{ BearerAuth: [] }],
4126
+ responses: {
4127
+ "200": {
4128
+ description: "Backup snapshot created",
4129
+ content: {
4130
+ "application/json": {
4131
+ schema: {
4132
+ type: "object",
4133
+ required: ["success", "local", "offsite", "duration_ms"],
4134
+ properties: {
4135
+ success: { type: "boolean" },
4136
+ local: {
4137
+ $ref: "#/components/schemas/BackupSnapshot",
4138
+ },
4139
+ offsite: {
4140
+ type: "array",
4141
+ items: { type: "object", additionalProperties: true },
4142
+ },
4143
+ duration_ms: { type: "number" },
4144
+ },
4145
+ },
4146
+ },
4147
+ },
4148
+ },
4149
+ "401": {
4150
+ description: "Unauthorized — missing or invalid bearer token",
4151
+ },
4152
+ "403": { description: "Insufficient scope" },
4153
+ "409": {
4154
+ description: "A backup snapshot is already in progress",
4155
+ },
4156
+ "500": { description: "Internal server error" },
4157
+ },
4158
+ },
4159
+ },
3997
4160
  "/{path}": {
3998
4161
  get: {
3999
4162
  summary: "Runtime proxy",
4000
4163
  description:
4001
- "Reverse-proxies requests to the assistant runtime when gateway.runtimeProxyEnabled is true in workspace config. Supports all HTTP methods. Returns 404 when the proxy is disabled.",
4164
+ "Reverse-proxies requests to the assistant runtime. Supports all HTTP methods.",
4002
4165
  operationId: "runtimeProxyGet",
4003
4166
  security: [{ BearerAuth: [] }],
4004
4167
  parameters: [
@@ -4023,7 +4186,7 @@ export function buildSchema(): Record<string, unknown> {
4023
4186
  },
4024
4187
  },
4025
4188
  "404": {
4026
- description: "Runtime proxy not enabled",
4189
+ description: "Route not found on upstream runtime",
4027
4190
  content: {
4028
4191
  "application/json": {
4029
4192
  schema: { $ref: "#/components/schemas/ErrorResponse" },
@@ -4059,8 +4222,7 @@ export function buildSchema(): Record<string, unknown> {
4059
4222
  },
4060
4223
  post: {
4061
4224
  summary: "Runtime proxy",
4062
- description:
4063
- "Reverse-proxies requests to the assistant runtime when gateway.runtimeProxyEnabled is true in workspace config.",
4225
+ description: "Reverse-proxies requests to the assistant runtime.",
4064
4226
  operationId: "runtimeProxyPost",
4065
4227
  security: [{ BearerAuth: [] }],
4066
4228
  parameters: [
@@ -4114,6 +4276,17 @@ export function buildSchema(): Record<string, unknown> {
4114
4276
  },
4115
4277
  components: {
4116
4278
  schemas: {
4279
+ BackupSnapshot: {
4280
+ type: "object",
4281
+ required: ["path", "filename", "created_at", "size_bytes", "encrypted"],
4282
+ properties: {
4283
+ path: { type: "string" },
4284
+ filename: { type: "string" },
4285
+ created_at: { type: "string", format: "date-time" },
4286
+ size_bytes: { type: "integer" },
4287
+ encrypted: { type: "boolean" },
4288
+ },
4289
+ },
4117
4290
  HealthResponse: {
4118
4291
  type: "object",
4119
4292
  required: ["status"],
@@ -492,7 +492,9 @@ describe("DM threading", () => {
492
492
  const result = normalizeSlackDirectMessage(event, "evt-dm-1", config);
493
493
 
494
494
  expect(result).not.toBeNull();
495
+ expect(result!.event.source.chatType).toBe("im");
495
496
  expect(result!.threadTs).toBeUndefined();
497
+ expect(result!.event.source.threadId).toBeUndefined();
496
498
  });
497
499
 
498
500
  it("threaded DM preserves threadTs", () => {
@@ -501,7 +503,9 @@ describe("DM threading", () => {
501
503
  const result = normalizeSlackDirectMessage(event, "evt-dm-2", config);
502
504
 
503
505
  expect(result).not.toBeNull();
506
+ expect(result!.event.source.chatType).toBe("im");
504
507
  expect(result!.threadTs).toBe("1700000000.000050");
508
+ expect(result!.event.source.threadId).toBe("1700000000.000050");
505
509
  });
506
510
  });
507
511
 
@@ -724,6 +728,39 @@ describe("attachment extraction in normalize functions", () => {
724
728
  expect(result!.slackFiles!.get("F030")!.id).toBe("F030");
725
729
  });
726
730
 
731
+ it("normalizes DM file_share mentions without stripping the bot mention", () => {
732
+ const config = makeConfig();
733
+ const event = makeDmEvent({
734
+ subtype: "file_share",
735
+ text: "<@UBOT> <@ULEO> shared this",
736
+ files: [
737
+ makeSlackFile({
738
+ id: "F032",
739
+ mimetype: "image/png",
740
+ name: "mention.png",
741
+ }),
742
+ ],
743
+ });
744
+ const result = normalizeSlackDirectMessage(
745
+ event,
746
+ "evt-fs-mentions-dm",
747
+ config,
748
+ "UBOT",
749
+ undefined,
750
+ { userLabels: { UBOT: "assistant", ULEO: "leo" } },
751
+ );
752
+
753
+ expect(result).not.toBeNull();
754
+ expect(result!.event.message.content).toBe(
755
+ "@assistant @leo shared this",
756
+ );
757
+ expect(result!.event.message.content).not.toContain("ULEO");
758
+ expect(result!.event.message.attachments).toHaveLength(1);
759
+ expect(result!.event.message.attachments![0].fileId).toBe("F032");
760
+ expect(result!.slackFiles).toBeDefined();
761
+ expect(result!.slackFiles!.get("F032")!.id).toBe("F032");
762
+ });
763
+
727
764
  it("normalizes DM with file_share subtype without files", () => {
728
765
  const config = makeConfig();
729
766
  const event = makeDmEvent({ subtype: "file_share" });
@@ -765,6 +802,43 @@ describe("attachment extraction in normalize functions", () => {
765
802
  expect(result!.slackFiles).toBeDefined();
766
803
  });
767
804
 
805
+ it("normalizes channel file_share mentions after stripping only the bot mention", () => {
806
+ const config = makeConfig();
807
+ const event = makeChannelEvent({
808
+ subtype: "file_share",
809
+ text: "<@UBOT> <@ULEO> shared this",
810
+ files: [
811
+ makeSlackFile({
812
+ id: "F033",
813
+ mimetype: "application/pdf",
814
+ name: "mention.pdf",
815
+ }),
816
+ ],
817
+ });
818
+ const result = normalizeSlackChannelMessage(
819
+ event,
820
+ "evt-fs-mentions-channel",
821
+ config,
822
+ "UBOT",
823
+ undefined,
824
+ { userLabels: { ULEO: "leo" } },
825
+ );
826
+
827
+ expect(result).not.toBeNull();
828
+ expect(result!.event.message.content).toBe("@leo shared this");
829
+ expect(result!.event.message.content).not.toContain("ULEO");
830
+ expect(result!.event.message.attachments).toHaveLength(1);
831
+ expect(result!.event.message.attachments![0]).toEqual({
832
+ type: "document",
833
+ fileId: "F033",
834
+ fileName: "mention.pdf",
835
+ mimeType: "application/pdf",
836
+ fileSize: 12345,
837
+ });
838
+ expect(result!.slackFiles).toBeDefined();
839
+ expect(result!.slackFiles!.get("F033")!.id).toBe("F033");
840
+ });
841
+
768
842
  it("normalizes channel message with file_share subtype without files", () => {
769
843
  const config = makeConfig();
770
844
  const event = makeChannelEvent({ subtype: "file_share" });
@@ -1157,6 +1231,30 @@ describe("normalizeSlackMessageDelete", () => {
1157
1231
  // DMs should not be tagged as channel chat even when inferred.
1158
1232
  expect(result!.event.source.chatType).toBeUndefined();
1159
1233
  });
1234
+
1235
+ it("returns null when previous_message author is the bot itself", () => {
1236
+ // Slack echoes self-deletes back via Socket Mode. Without this filter,
1237
+ // the bot's user ID flows through as the actor and triggers a spurious
1238
+ // ingress access-request notification to the guardian.
1239
+ const config = makeConfig();
1240
+ const event = makeMessageDeletedEvent({
1241
+ channel: "D789",
1242
+ channel_type: "im",
1243
+ previous_message: {
1244
+ user: "UBOT",
1245
+ text: "deleted bot message",
1246
+ ts: "1700000000.000100",
1247
+ },
1248
+ });
1249
+ const result = normalizeSlackMessageDelete(
1250
+ event,
1251
+ "evt-del-self",
1252
+ config,
1253
+ "UBOT",
1254
+ );
1255
+
1256
+ expect(result).toBeNull();
1257
+ });
1160
1258
  });
1161
1259
 
1162
1260
  // --- source.threadId propagation ---