@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.
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +67 -25
- package/Dockerfile +2 -0
- package/README.md +50 -13
- package/bun.lock +16 -2
- package/knip.json +3 -1
- package/package.json +3 -1
- package/src/__tests__/auto-approve-thresholds.test.ts +49 -22
- package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
- package/src/__tests__/config-file-watcher.test.ts +181 -0
- package/src/__tests__/config.test.ts +0 -1
- package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/credential-watcher-managed-bootstrap.test.ts +10 -2
- package/src/__tests__/credential-watcher.test.ts +30 -2
- package/src/__tests__/db-connection-isolation.test.ts +157 -0
- package/src/__tests__/fake-assistant-ipc.ts +39 -0
- package/src/__tests__/feature-flags-route.test.ts +8 -8
- package/src/__tests__/guardian-init-lockfile.test.ts +30 -4
- package/src/__tests__/ipc-feature-flag-routes.test.ts +1 -1
- package/src/__tests__/live-voice-websocket.test.ts +0 -1
- package/src/__tests__/load-guards.test.ts +0 -1
- package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
- package/src/__tests__/oauth-callback.test.ts +0 -1
- package/src/__tests__/pair-origin-allowlist.test.ts +155 -0
- package/src/__tests__/rate-limit-loopback.test.ts +1 -1
- package/src/__tests__/remote-feature-flag-sync.test.ts +47 -7
- package/src/__tests__/resolve-assistant.test.ts +0 -1
- package/src/__tests__/route-schema-guard.test.ts +42 -6
- package/src/__tests__/runtime-client.test.ts +0 -1
- package/src/__tests__/runtime-health-proxy.test.ts +0 -1
- package/src/__tests__/runtime-proxy-auth.test.ts +0 -1
- package/src/__tests__/runtime-proxy.test.ts +0 -1
- package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/slack-display-name.test.ts +66 -1
- package/src/__tests__/slack-normalize.test.ts +158 -4
- package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-catchup.test.ts +857 -0
- package/src/__tests__/slack-socket-mode-scopes.test.ts +52 -0
- package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +654 -0
- package/src/__tests__/stt-stream-websocket.test.ts +0 -1
- package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/telegram-send-attachments.test.ts +0 -1
- package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
- package/src/__tests__/text-verification-helpers.test.ts +136 -0
- package/src/__tests__/twilio-media-websocket.test.ts +0 -1
- package/src/__tests__/twilio-relay-websocket.test.ts +0 -1
- package/src/__tests__/twilio-webhooks.test.ts +220 -3
- package/src/__tests__/upstream-transport.test.ts +0 -36
- package/src/__tests__/whatsapp-download.test.ts +0 -1
- package/src/__tests__/whatsapp-webhook.test.ts +0 -1
- package/src/auth/guardian-refresh.ts +4 -18
- package/src/auth/ipc-route-policy.ts +217 -0
- package/src/backup/backup-key.ts +138 -0
- package/src/backup/backup-routes.ts +159 -0
- package/src/backup/backup-worker.ts +374 -0
- package/src/backup/list-snapshots.ts +97 -0
- package/src/backup/local-writer.ts +87 -0
- package/src/backup/offsite-writer.ts +182 -0
- package/src/backup/paths.ts +123 -0
- package/src/backup/stream-crypt.ts +258 -0
- package/src/chrome-extension-origins.ts +28 -0
- package/src/cli/enable-proxy.ts +0 -1
- package/src/config-file-cache.ts +3 -19
- package/src/config-file-utils.ts +124 -0
- package/src/config-file-watcher.ts +57 -25
- package/src/config.ts +4 -7
- package/src/db/connection.ts +65 -3
- package/src/db/contact-store.ts +30 -1
- package/src/db/data-migrations/index.ts +2 -0
- package/src/db/data-migrations/m0003-recover-backup-key.ts +71 -0
- package/src/db/schema.ts +92 -0
- package/src/db/slack-store.ts +144 -11
- package/src/feature-flag-registry.json +40 -152
- package/src/handlers/handle-inbound.ts +123 -0
- package/src/http/middleware/auth.ts +44 -1
- package/src/http/middleware/cors.ts +84 -0
- package/src/http/middleware/rate-limit.ts +6 -8
- package/src/http/routes/auto-approve-thresholds.ts +17 -1
- package/src/http/routes/brain-graph-proxy.ts +1 -1
- package/src/http/routes/channel-readiness-proxy.ts +2 -2
- package/src/http/routes/channel-verification-session-proxy.ts +19 -37
- package/src/http/routes/contact-prompt.ts +149 -0
- package/src/http/routes/contacts-control-plane-proxy.ts +2 -2
- package/src/http/routes/email-webhook.test.ts +0 -1
- package/src/http/routes/ipc-runtime-proxy.test.ts +197 -1
- package/src/http/routes/ipc-runtime-proxy.ts +95 -0
- package/src/http/routes/log-export.test.ts +0 -1
- package/src/http/routes/log-tail.test.ts +336 -0
- package/src/http/routes/log-tail.ts +87 -0
- package/src/http/routes/migration-proxy.ts +1 -2
- package/src/http/routes/oauth-apps-proxy.ts +2 -2
- package/src/http/routes/oauth-providers-proxy.ts +2 -2
- package/src/http/routes/pair.ts +322 -0
- package/src/http/routes/privacy-config.ts +65 -79
- package/src/http/routes/runtime-health-proxy.ts +2 -2
- package/src/http/routes/runtime-proxy.ts +3 -1
- package/src/http/routes/slack-control-plane-proxy.ts +3 -20
- package/src/http/routes/stt-stream-websocket.ts +2 -3
- package/src/http/routes/telegram-control-plane-proxy.ts +2 -2
- package/src/http/routes/telegram-webhook.test.ts +0 -1
- package/src/http/routes/telegram-webhook.ts +6 -0
- package/src/http/routes/trust-rules.suggest.test.ts +25 -0
- package/src/http/routes/trust-rules.ts +7 -0
- package/src/http/routes/twilio-control-plane-proxy.ts +2 -2
- package/src/http/routes/twilio-media-websocket.ts +5 -5
- package/src/http/routes/twilio-voice-verify-callback.ts +310 -0
- package/src/http/routes/twilio-voice-webhook.test.ts +65 -1
- package/src/http/routes/twilio-voice-webhook.ts +45 -1
- package/src/http/routes/whatsapp-webhook.test.ts +0 -1
- package/src/index.ts +357 -278
- package/src/ipc/assistant-client.ts +8 -4
- package/src/ipc/contact-handlers.ts +88 -3
- package/src/ipc/threshold-handlers.ts +2 -0
- package/src/post-assistant-ready.ts +5 -3
- package/src/risk/bash-risk-classifier.test.ts +35 -27
- package/src/risk/bash-risk-classifier.ts +44 -14
- package/src/risk/command-registry/commands/assistant.ts +8 -19
- package/src/risk/command-registry.test.ts +0 -15
- package/src/risk/risk-classifier-parity.test.ts +1 -3
- package/src/runtime/client.ts +58 -3
- package/src/schema.ts +277 -104
- package/src/slack/normalize.test.ts +98 -0
- package/src/slack/normalize.ts +107 -32
- package/src/slack/slack-web.ts +213 -0
- package/src/slack/socket-mode.ts +701 -39
- package/src/telegram/send.test.ts +0 -1
- package/src/twilio/validate-webhook.ts +53 -14
- package/src/twilio/webhook-sync-trigger.ts +58 -0
- package/src/twilio/webhook-sync.test.ts +286 -0
- package/src/twilio/webhook-sync.ts +84 -0
- package/src/util/is-loopback-address.ts +27 -0
- package/src/velay/bridge-utils.ts +228 -0
- package/src/velay/client.test.ts +939 -0
- package/src/velay/client.ts +555 -0
- package/src/velay/http-bridge.test.ts +217 -0
- package/src/velay/http-bridge.ts +83 -0
- package/src/velay/protocol.ts +178 -0
- package/src/velay/test-fake-websocket.ts +69 -0
- package/src/velay/websocket-bridge.test.ts +367 -0
- package/src/velay/websocket-bridge.ts +324 -0
- package/src/verification/binding-helpers.ts +107 -0
- package/src/verification/code-parsing.ts +44 -0
- package/src/verification/contact-helpers.ts +342 -0
- package/src/verification/identity-match.ts +68 -0
- package/src/verification/identity.ts +61 -0
- package/src/verification/rate-limit-helpers.ts +205 -0
- package/src/verification/reply-delivery.ts +109 -0
- package/src/verification/session-helpers.ts +164 -0
- package/src/verification/text-verification.ts +372 -0
- package/src/version.ts +35 -0
- package/src/voice/verification.ts +456 -0
- package/src/webhook-pipeline.ts +4 -0
- package/src/__tests__/browser-relay-websocket.test.ts +0 -698
- package/src/__tests__/telegram-only-default.test.ts +0 -133
- package/src/auth/capability-tokens.ts +0 -248
- package/src/http/routes/browser-extension-pair.ts +0 -455
- package/src/http/routes/browser-relay-websocket.ts +0 -381
- package/src/http/routes/config-file-utils.ts +0 -73
- package/src/ipc/capability-token-handlers.ts +0 -30
- package/src/pairing/approved-devices-store.ts +0 -110
- package/src/pairing/pairing-routes.ts +0 -379
- package/src/pairing/pairing-store.ts +0 -218
package/AGENTS.md
CHANGED
|
@@ -37,6 +37,10 @@ In Docker mode, the gateway is the sole owner of trust rule storage. Trust files
|
|
|
37
37
|
|
|
38
38
|
The assistant reads and writes trust rules via the gateway's HTTP trust API instead of accessing the filesystem directly. This ensures the security boundary is enforced at the container level — even if the assistant container is compromised, it cannot tamper with trust rules without going through the gateway's API.
|
|
39
39
|
|
|
40
|
+
### Backup Encryption Key
|
|
41
|
+
|
|
42
|
+
The backup encryption key (`backup.key`) lives in `GATEWAY_SECURITY_DIR` and is never exposed to the assistant daemon or workspace. The gateway owns all backup encryption/decryption — the assistant produces plaintext vbundles via `/v1/migrations/export`, and the gateway encrypts them for offsite storage. The assistant cannot read the key via `file_read` or any other tool. This is a security boundary: even if the assistant is prompt-injected, backup encryption remains intact.
|
|
43
|
+
|
|
40
44
|
### Credential Access in Docker Mode
|
|
41
45
|
|
|
42
46
|
In Docker mode, the gateway accesses stored credentials via the CES HTTP API (`CES_CREDENTIAL_URL`), authenticated with `CES_SERVICE_TOKEN`. The gateway does not have direct filesystem access to credential encryption keys (`keys.enc`, `store.key`), which reside on the CES security volume.
|
package/ARCHITECTURE.md
CHANGED
|
@@ -128,7 +128,7 @@ The assistant daemon does not read or distribute a feature-flag token. All featu
|
|
|
128
128
|
|
|
129
129
|
### Channel Verification Session Control-Plane Proxy
|
|
130
130
|
|
|
131
|
-
Channel verification session endpoints are exposed directly by the gateway and forwarded to runtime integration handlers
|
|
131
|
+
Channel verification session endpoints are exposed directly by the gateway and forwarded to runtime integration handlers for dedicated auth handling. This keeps assistant skills and user-facing tooling on gateway URLs only.
|
|
132
132
|
|
|
133
133
|
**Forwarded endpoints:**
|
|
134
134
|
|
|
@@ -158,7 +158,7 @@ The `/v1/guardian/refresh` endpoint is the only public ingress for rotating JWT
|
|
|
158
158
|
|
|
159
159
|
### Runtime Health Proxy
|
|
160
160
|
|
|
161
|
-
Runtime health is exposed directly by the gateway at `GET /v1/health` and forwarded to the runtime's `GET /v1/health` endpoint
|
|
161
|
+
Runtime health is exposed directly by the gateway at `GET /v1/health` and forwarded to the runtime's `GET /v1/health` endpoint for dedicated auth handling.
|
|
162
162
|
|
|
163
163
|
**Authentication boundary:**
|
|
164
164
|
|
|
@@ -175,7 +175,7 @@ Runtime health is exposed directly by the gateway at `GET /v1/health` and forwar
|
|
|
175
175
|
|
|
176
176
|
### Telegram + Contacts Control-Plane Proxies
|
|
177
177
|
|
|
178
|
-
Telegram integration setup/config endpoints and contacts/invites endpoints are also exposed directly by the gateway and forwarded to runtime handlers
|
|
178
|
+
Telegram integration setup/config endpoints and contacts/invites endpoints are also exposed directly by the gateway and forwarded to runtime handlers for dedicated auth handling.
|
|
179
179
|
|
|
180
180
|
**Forwarded Telegram endpoints:**
|
|
181
181
|
|
|
@@ -213,7 +213,7 @@ Telegram integration setup/config endpoints and contacts/invites endpoints are a
|
|
|
213
213
|
|
|
214
214
|
### Twilio Control-Plane Proxy
|
|
215
215
|
|
|
216
|
-
Twilio integration setup/config endpoints are exposed directly by the gateway and forwarded to runtime handlers
|
|
216
|
+
Twilio integration setup/config endpoints are exposed directly by the gateway and forwarded to runtime handlers for dedicated auth handling. This keeps skills and clients on gateway URLs exclusively.
|
|
217
217
|
|
|
218
218
|
**Forwarded endpoints:**
|
|
219
219
|
|
|
@@ -242,7 +242,7 @@ Twilio integration setup/config endpoints are exposed directly by the gateway an
|
|
|
242
242
|
|
|
243
243
|
### Channel Readiness Proxy
|
|
244
244
|
|
|
245
|
-
Channel readiness endpoints are exposed directly by the gateway and forwarded to runtime handlers
|
|
245
|
+
Channel readiness endpoints are exposed directly by the gateway and forwarded to runtime handlers for dedicated auth handling.
|
|
246
246
|
|
|
247
247
|
**Forwarded endpoints:**
|
|
248
248
|
|
|
@@ -280,9 +280,10 @@ Channel bindings follow a three-phase lifecycle:
|
|
|
280
280
|
|
|
281
281
|
The public URL where the gateway is reachable is configured via:
|
|
282
282
|
|
|
283
|
-
| Source
|
|
284
|
-
|
|
|
285
|
-
| `ingress.publicBaseUrl` (workspace config)
|
|
283
|
+
| Source | Description |
|
|
284
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
285
|
+
| `ingress.publicBaseUrl` (workspace config) | Canonical public ingress URL for Telegram webhooks, OAuth callbacks, email callbacks, generic JSON webhooks, and custom/ngrok tunnel based Twilio fallback |
|
|
286
|
+
| `ingress.twilioPublicBaseUrl` (workspace config) | Twilio-specific public ingress URL written by Velay registration; used only by Twilio URL builders when present |
|
|
286
287
|
|
|
287
288
|
### Tunnel-Agnostic Setup
|
|
288
289
|
|
|
@@ -291,7 +292,34 @@ To expose the gateway for external callbacks during local development:
|
|
|
291
292
|
1. **Start your tunnel** service (ngrok, Cloudflare Tunnel, or any similar tool), pointing it at the local gateway: `http://127.0.0.1:7830`
|
|
292
293
|
2. **Set the public URL** provided by the tunnel as `ingress.publicBaseUrl` in the Settings UI (Public Ingress section)
|
|
293
294
|
|
|
294
|
-
The assistant runtime reads this URL via the centralized `public-ingress-urls.ts` module and uses it to construct
|
|
295
|
+
The assistant runtime reads this URL via the centralized `public-ingress-urls.ts` module and uses it to construct webhook and callback URLs automatically. Ngrok and custom tunnels remain supported for every ingress surface, including Twilio fallback.
|
|
296
|
+
|
|
297
|
+
### Velay Twilio Ingress
|
|
298
|
+
|
|
299
|
+
Velay is a platform-managed tunnel for assistant-hosted HTTP and WebSocket traffic. It is intentionally additive to the tunnel-agnostic setup above: Velay registration does not overwrite `ingress.publicBaseUrl`, and operators can continue using ngrok or custom tunnels for non-Twilio webhooks.
|
|
300
|
+
|
|
301
|
+
When `VELAY_BASE_URL` is present in the gateway environment, the gateway starts `VelayTunnelClient`. The client registers with Velay over `GET /v1/register` using the assistant API key, then receives a `registered` frame containing a public assistant URL such as `https://velay.vellum.ai/<assistant-id>`. The gateway writes that URL to `ingress.twilioPublicBaseUrl` only. When the tunnel disconnects, it clears that same value if it still matches the URL the tunnel published, leaving `ingress.publicBaseUrl` intact.
|
|
302
|
+
|
|
303
|
+
Velay forwards both HTTP request frames and WebSocket frames into the local gateway loopback listener:
|
|
304
|
+
|
|
305
|
+
```text
|
|
306
|
+
Public Velay HTTPS/WSS URL
|
|
307
|
+
→ Velay tunnel session
|
|
308
|
+
→ Gateway Velay bridge
|
|
309
|
+
→ Gateway loopback listener (http://127.0.0.1:<GATEWAY_PORT>)
|
|
310
|
+
→ Existing gateway route handlers
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The HTTP bridge can carry normal JSON requests and health checks, so it is useful for local bridge smoke tests. The advertised URL split is Twilio-scoped in this phase, though: only Twilio URL generation prefers `ingress.twilioPublicBaseUrl`; Telegram webhook registration, OAuth redirects, email callbacks, and generic public URL builders continue to use `ingress.publicBaseUrl`.
|
|
314
|
+
|
|
315
|
+
Local platform smoke-test flow:
|
|
316
|
+
|
|
317
|
+
1. In `vellum-assistant-platform`, run `vel up velay`.
|
|
318
|
+
2. Ensure vembda passes `VELAY_BASE_URL=http://host.docker.internal:8501` into assistant gateway containers.
|
|
319
|
+
3. Re-hatch or restart the assistant so the gateway receives the new environment.
|
|
320
|
+
4. Confirm gateway logs show `Velay tunnel connected` and `Velay tunnel registered`.
|
|
321
|
+
5. Verify HTTP forwarding by requesting `${VELAY_PUBLIC_BASE_URL}/<assistant-id>/healthz` and `${VELAY_PUBLIC_BASE_URL}/<assistant-id>/schema`. When validating a JSON webhook route under active development, POST a small JSON body through the same Velay public URL and confirm it reaches the loopback gateway.
|
|
322
|
+
6. Verify Twilio WebSocket forwarding with a synthetic local WebSocket client against `${VELAY_PUBLIC_BASE_URL}/<assistant-id>/webhooks/twilio/relay?callSessionId=...&token=...`, then with a real Twilio call after `VELAY_PUBLIC_BASE_URL` is backed by a public HTTPS/WSS tunnel.
|
|
295
323
|
|
|
296
324
|
### URL Builders
|
|
297
325
|
|
|
@@ -300,10 +328,10 @@ All public-facing URLs are constructed by `assistant/src/inbound/public-ingress-
|
|
|
300
328
|
| Function | URL Pattern |
|
|
301
329
|
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
302
330
|
| `getPublicBaseUrl()` | Resolves the canonical base URL from `ingress.publicBaseUrl` in workspace config or module-level state (assistant-side; the gateway reads via `ConfigFileCache`) |
|
|
303
|
-
| `getTwilioVoiceWebhookUrl()` | `${
|
|
304
|
-
| `getTwilioStatusCallbackUrl()` | `${
|
|
305
|
-
| `getTwilioConnectActionUrl()` | `${
|
|
306
|
-
| `getTwilioRelayUrl()` | `ws(s)://.../webhooks/twilio/relay
|
|
331
|
+
| `getTwilioVoiceWebhookUrl()` | `${twilioBase}/webhooks/twilio/voice?callSessionId=...`, where `twilioBase` is `ingress.twilioPublicBaseUrl` when present, otherwise `ingress.publicBaseUrl` |
|
|
332
|
+
| `getTwilioStatusCallbackUrl()` | `${twilioBase}/webhooks/twilio/status`, with the same Twilio-specific base resolution |
|
|
333
|
+
| `getTwilioConnectActionUrl()` | `${twilioBase}/webhooks/twilio/connect-action`, with the same Twilio-specific base resolution |
|
|
334
|
+
| `getTwilioRelayUrl()` | `ws(s)://.../webhooks/twilio/relay`, with the same Twilio-specific base resolution |
|
|
307
335
|
| `getOAuthCallbackUrl()` | `${base}/webhooks/oauth/callback` |
|
|
308
336
|
| `getTelegramWebhookUrl()` | `${base}/webhooks/telegram` |
|
|
309
337
|
|
|
@@ -702,7 +730,7 @@ The Slack channel enables inbound and outbound messaging via Slack's Socket Mode
|
|
|
702
730
|
|
|
703
731
|
1. Every Socket Mode envelope is ACKed immediately by echoing `{ envelope_id }` back on the WebSocket — this is required by Slack regardless of whether the event is processed.
|
|
704
732
|
2. Only `events_api` envelopes with `app_mention` events are processed in MVP. Other envelope types (slash commands, interactive payloads) are ACKed but ignored.
|
|
705
|
-
3. Events are deduplicated by `
|
|
733
|
+
3. Events are deduplicated by a compound key in the SQLite-backed `slack_seen_events` table: every event records its Slack `event_id`, and message-shaped events additionally record `msg:${channel}:${ts}` so the live and reconnect-replay paths dedup symmetrically. Entries TTL out after 24h; a periodic cleanup sweep evicts expired rows.
|
|
706
734
|
4. The `normalizeSlackAppMention()` function strips leading bot-mention tokens (`<@U...>`) from the message text and produces a `GatewayInboundEvent` with `sourceChannel: "slack"`, using the Slack channel ID as `conversationExternalId` and the sender's user ID as `actorExternalId`.
|
|
707
735
|
5. Routing uses the standard `resolveAssistant()` chain (conversation_id -> actor_id -> default/reject). Events that cannot be routed are dropped.
|
|
708
736
|
6. The normalized event is forwarded to the runtime via `POST /v1/channels/inbound` with a `replyCallbackUrl` pointing to `/deliver/slack`.
|
|
@@ -729,13 +757,24 @@ Both tokens are stored in secure storage (`credential/slack_channel/app_token`,
|
|
|
729
757
|
|
|
730
758
|
The Socket Mode client auto-reconnects on any WebSocket close or error. The backoff schedule is: `min(1000 * 2^attempt, 30000)` + random jitter. After a successful connection, the attempt counter resets. Slack-initiated disconnects (envelope `type: "disconnect"`) trigger an immediate reconnect with no backoff.
|
|
731
759
|
|
|
760
|
+
**Reconnect catch-up:**
|
|
761
|
+
|
|
762
|
+
Slack [does not buffer Socket Mode events](https://api.slack.com/apis/socket-mode#events) for disconnected clients, so any @mention or DM that arrives during a reconnect window is lost on the wire. The client recovers these by maintaining a persistent high-watermark (`slack_last_seen_ts`) of the latest accepted event timestamp and, on every WebSocket `open`, fetching a bounded slice of [`conversations.history`](https://api.slack.com/methods/conversations.history) and [`conversations.replies`](https://api.slack.com/methods/conversations.replies) since that watermark. Recovered messages are wrapped in synthetic Socket Mode envelopes and dispatched through the same `processEventPayload` path as live events, so filter, dedup, normalize, and routing logic stay in one place.
|
|
763
|
+
|
|
764
|
+
Catch-up is scoped to channels the gateway can reasonably reach: routing entries with a Slack-shaped conversation ID (`C…` / `D…` / `G…`), tracked active threads (`slack_active_threads`), and previously-seen DM channels (`contact_channels` rows of type `slack`). It is bounded by max-lookback (1h), per-channel limit (50), and concurrency (4); on a 429 the cycle aborts immediately and resumes on the next reconnect. Brand-new mentions in unrouted, never-engaged channels remain unrecoverable here — the daemon's existing inbound-triggered Slack backfill (`triggerSlackThreadBackfillIfNeeded` and `tryBackfillSlackDmIfCold`) hydrates context once the next live event arrives.
|
|
765
|
+
|
|
766
|
+
**General principle — stateful-stream transports require catch-up-on-reconnect:**
|
|
767
|
+
|
|
768
|
+
Any persistent-stream transport that does not buffer events for disconnected clients (Slack Socket Mode, similar WebSocket gateways) must combine the WebSocket with a HTTP catch-up path on reconnect. A persisted high-watermark + bounded replay through the shared event pipeline is the standard pattern; without it, every transient disconnect silently drops events.
|
|
769
|
+
|
|
732
770
|
**Key modules:**
|
|
733
771
|
|
|
734
|
-
| Module | Purpose
|
|
735
|
-
| ------------------------------------------ |
|
|
736
|
-
| `gateway/src/slack/socket-mode.ts` | `SlackSocketModeClient` — WebSocket lifecycle, ACK, dedup, auto-reconnect
|
|
737
|
-
| `gateway/src/slack/
|
|
738
|
-
| `gateway/src/
|
|
772
|
+
| Module | Purpose |
|
|
773
|
+
| ------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
|
774
|
+
| `gateway/src/slack/socket-mode.ts` | `SlackSocketModeClient` — WebSocket lifecycle, ACK, dedup, auto-reconnect, reconnect catch-up |
|
|
775
|
+
| `gateway/src/slack/slack-web.ts` | `conversations.history` / `conversations.replies` helpers for reconnect catch-up |
|
|
776
|
+
| `gateway/src/slack/normalize.ts` | `normalizeSlackAppMention()` — event normalization and bot-mention stripping |
|
|
777
|
+
| `gateway/src/http/routes/slack-deliver.ts` | `/deliver/slack` — outbound message delivery via `chat.postMessage` |
|
|
739
778
|
|
|
740
779
|
**Limitations (MVP):** Text-only — attachments are rejected. Only `app_mention` events are processed (direct messages to the bot are not handled). Rich approval UI (inline buttons) is not supported.
|
|
741
780
|
|
|
@@ -1038,18 +1077,21 @@ In gateway-fronted deployments, the TwiML WebSocket URL (returned by the voice w
|
|
|
1038
1077
|
|
|
1039
1078
|
Signature validation is **fail-closed**: if the Twilio auth token is not configured, all webhook requests are rejected with `403`. Missing or invalid `X-Twilio-Signature` headers are also rejected with `403`. Payload size is capped by `maxWebhookPayloadBytes` (checked via both `Content-Length` header and actual body size).
|
|
1040
1079
|
|
|
1041
|
-
**Webhook base URL resolution:**
|
|
1080
|
+
**Webhook base URL resolution:** Public ingress URL construction is centralized in `public-ingress-urls.ts`, with a Twilio-specific override for Velay:
|
|
1042
1081
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1082
|
+
- Twilio voice/status/connect-action/relay/media-stream URLs use `ingress.twilioPublicBaseUrl` when present. This is the value published by Velay registration.
|
|
1083
|
+
- If `ingress.twilioPublicBaseUrl` is absent, Twilio falls back to `ingress.publicBaseUrl`.
|
|
1084
|
+
- Telegram webhooks, OAuth callbacks, email callbacks, and normal JSON webhook URLs use `ingress.publicBaseUrl`; Velay does not replace those advertised URLs in this phase.
|
|
1085
|
+
- Module-level assistant state remains a fallback for legacy tunnel start/stop flows.
|
|
1045
1086
|
|
|
1046
1087
|
All webhook paths (`/webhooks/twilio/voice`, `/webhooks/twilio/status`, `/webhooks/telegram`, `/webhooks/oauth/callback`, etc.) are appended automatically.
|
|
1047
1088
|
|
|
1048
1089
|
For **inbound Twilio signature validation** at the gateway, URL reconstruction now supports multiple candidates in order:
|
|
1049
1090
|
|
|
1050
|
-
1. `ConfigFileCache.getString("ingress", "
|
|
1051
|
-
2.
|
|
1052
|
-
3.
|
|
1091
|
+
1. `ConfigFileCache.getString("ingress", "twilioPublicBaseUrl")` (if configured)
|
|
1092
|
+
2. `ConfigFileCache.getString("ingress", "publicBaseUrl")` (if configured)
|
|
1093
|
+
3. Forwarded public URL headers from the tunnel/proxy (`X-Forwarded-Proto` + `X-Forwarded-Host`/`X-Original-Host` fallbacks)
|
|
1094
|
+
4. Raw request URL (always included as the final fallback)
|
|
1053
1095
|
|
|
1054
1096
|
This makes ingress URL updates smoother in local tunnel workflows because Twilio webhooks can continue validating immediately. For Telegram, the config file watcher detects ingress URL changes and triggers webhook reconciliation directly, so neither channel requires a gateway restart.
|
|
1055
1097
|
|
package/Dockerfile
CHANGED
|
@@ -12,6 +12,8 @@ COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun
|
|
|
12
12
|
COPY packages/assistant-client ./packages/assistant-client
|
|
13
13
|
COPY packages/ces-client ./packages/ces-client
|
|
14
14
|
COPY packages/service-contracts ./packages/service-contracts
|
|
15
|
+
COPY packages/slack-text ./packages/slack-text
|
|
16
|
+
COPY packages/twilio-client ./packages/twilio-client
|
|
15
17
|
|
|
16
18
|
# Install deps for shared packages that have their own file: dependencies.
|
|
17
19
|
RUN cd /app/packages/ces-client && bun install --frozen-lockfile
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vellum Gateway
|
|
2
2
|
|
|
3
|
-
Standalone service that serves as the public ingress boundary for all external webhooks and callbacks. It owns Telegram integration end-to-end, routes Twilio voice webhooks, handles OAuth callbacks, and
|
|
3
|
+
Standalone service that serves as the public ingress boundary for all external webhooks and callbacks. It owns Telegram integration end-to-end, routes Twilio voice webhooks, handles OAuth callbacks, and acts as an authenticated reverse proxy for the assistant runtime.
|
|
4
4
|
|
|
5
5
|
## Architecture
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Telegram → gateway/ → Assistant Runtime (/v1/assistants/:id/channels/inbound
|
|
|
10
10
|
Client → gateway/ (Bearer auth) → Assistant Runtime (any path)
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The web app is **not** in the Telegram request path.
|
|
13
|
+
The web app is **not** in the Telegram request path. All non-Telegram requests that don't match a dedicated gateway route are forwarded to the assistant runtime with bearer token authentication.
|
|
14
14
|
|
|
15
15
|
For ingress and channel architecture details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
|
16
16
|
|
|
@@ -26,11 +26,11 @@ bun run dev
|
|
|
26
26
|
|
|
27
27
|
## Configuration
|
|
28
28
|
|
|
29
|
-
| Variable | Required | Default | Description
|
|
30
|
-
| ------------------------- | -------- | ------- |
|
|
29
|
+
| Variable | Required | Default | Description |
|
|
30
|
+
| ------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
31
31
|
| `TELEGRAM_BOT_TOKEN` | No | — | Bot token from @BotFather (Telegram disabled when unset). When not set as an env var, the gateway reads from the assistant's secure credential store: CES HTTP API first (when `CES_CREDENTIAL_URL` is configured), then the encrypted file store (`~/.vellum/protected/keys.enc`). |
|
|
32
|
-
| `TELEGRAM_WEBHOOK_SECRET` | No | — | Secret for verifying webhook requests (Telegram disabled when unset). Same credential reader fallback behavior as `TELEGRAM_BOT_TOKEN`.
|
|
33
|
-
| `GATEWAY_PORT` | No | `7830` | Port for the gateway HTTP server
|
|
32
|
+
| `TELEGRAM_WEBHOOK_SECRET` | No | — | Secret for verifying webhook requests (Telegram disabled when unset). Same credential reader fallback behavior as `TELEGRAM_BOT_TOKEN`. |
|
|
33
|
+
| `GATEWAY_PORT` | No | `7830` | Port for the gateway HTTP server |
|
|
34
34
|
|
|
35
35
|
Most gateway behavior is now configured via hardcoded defaults or workspace config (`~/.vellum/workspace/config.json`) rather than environment variables. Channel operational settings (Telegram API base URL, timeouts, deliver auth bypass flags, runtime base URL, routing, proxy settings, attachment limits, shutdown drain) are managed via `workspace/config.json` through `ConfigFileCache`. See the channel-specific sections in `ARCHITECTURE.md` for details.
|
|
36
36
|
|
|
@@ -176,7 +176,7 @@ The gateway serves as the single public ingress point for all external callbacks
|
|
|
176
176
|
|
|
177
177
|
### Tunnel Setup
|
|
178
178
|
|
|
179
|
-
To receive external callbacks during local development, point a tunnel service at the local gateway (default `http://127.0.0.1:7830`) and configure the resulting public URL
|
|
179
|
+
To receive external callbacks during local development, point a tunnel service at the local gateway (default `http://127.0.0.1:7830`) and configure the resulting public URL. Ngrok, Cloudflare Tunnel, and other custom HTTPS/WSS tunnels remain supported.
|
|
180
180
|
|
|
181
181
|
#### Test Gateway Source Changes Locally (No Release Needed)
|
|
182
182
|
|
|
@@ -210,6 +210,47 @@ In local tunnel setups, updating `ingress.publicBaseUrl` in Settings is typicall
|
|
|
210
210
|
|
|
211
211
|
The assistant runtime uses this URL to construct all webhook and OAuth callback URLs automatically.
|
|
212
212
|
|
|
213
|
+
### Velay for Twilio Local Testing
|
|
214
|
+
|
|
215
|
+
Velay is an additional managed ingress transport for Twilio calls. It does not replace ngrok or `ingress.publicBaseUrl`. In this phase, Velay registration writes only `ingress.twilioPublicBaseUrl`; Twilio URL builders prefer that value when it is present, while Telegram webhooks, OAuth callbacks, email callbacks, and normal JSON webhook URLs continue to use `ingress.publicBaseUrl`.
|
|
216
|
+
|
|
217
|
+
Use Velay when testing Twilio voice webhooks or Twilio WebSocket upgrades through the platform-managed tunnel:
|
|
218
|
+
|
|
219
|
+
1. In `vellum-assistant-platform`, start the local Velay service:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
vel up velay
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
2. Ensure vembda injects the Velay endpoint into assistant gateway containers:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
VELAY_BASE_URL=http://host.docker.internal:8501
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The `host.docker.internal` host is important for Docker-hosted assistants because the gateway container must dial the Velay service running on the host.
|
|
232
|
+
|
|
233
|
+
3. Re-hatch or restart the assistant so the gateway process receives `VELAY_BASE_URL`.
|
|
234
|
+
4. Confirm the gateway logs include `Velay tunnel connected` followed by `Velay tunnel registered`. Registration publishes the returned Velay URL to `ingress.twilioPublicBaseUrl` without changing `ingress.publicBaseUrl`.
|
|
235
|
+
|
|
236
|
+
For an HTTP bridge smoke test, send a request to the registered Velay public URL and confirm it reaches the loopback gateway, for example:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
curl -i "$VELAY_PUBLIC_BASE_URL/<assistant-id>/healthz"
|
|
240
|
+
curl -i "$VELAY_PUBLIC_BASE_URL/<assistant-id>/schema"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When testing a JSON webhook route under active development, POST a small JSON body through the same Velay public URL and confirm the gateway logs or handler response show the request reached the loopback listener.
|
|
244
|
+
|
|
245
|
+
For a synthetic Twilio WebSocket smoke test, connect a local WebSocket client to the Velay public URL using one of the gateway Twilio WebSocket paths, such as:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
bun -e 'const ws = new WebSocket(process.argv[1]); ws.onopen = () => { console.log("open"); ws.close(); }; ws.onerror = (event) => console.error(event);' \
|
|
249
|
+
"wss://<velay-host>/<assistant-id>/webhooks/twilio/relay?callSessionId=session-123&token=<edge-token>"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
For a real Twilio call, expose local Velay with a public HTTPS/WSS tunnel and configure the platform Velay service with that origin as `VELAY_PUBLIC_BASE_URL`. After the assistant re-registers, Twilio should fetch `/webhooks/twilio/voice` and open `/webhooks/twilio/relay` or `/webhooks/twilio/media-stream/...` through the Velay URL. Keep using ngrok or another custom tunnel in `ingress.publicBaseUrl` when you need Telegram, OAuth, email, or non-Twilio webhook ingress.
|
|
253
|
+
|
|
213
254
|
## Ingress Boundary Guarantees
|
|
214
255
|
|
|
215
256
|
The gateway is the **sole public ingress point** for all external webhooks. The assistant runtime never directly accepts public webhook traffic — all Twilio and Telegram webhook routes on the runtime return `410 GATEWAY_ONLY` when accessed directly.
|
|
@@ -218,13 +259,9 @@ The gateway is the **sole public ingress point** for all external webhooks. The
|
|
|
218
259
|
|
|
219
260
|
When the ingress public base URL is configured (via `ingress.publicBaseUrl` in workspace config, read through `ConfigFileCache`), the gateway prioritizes it as the canonical URL for Twilio signature validation. If the signature only validates against the raw local request URL (fallback), a warning is logged indicating potential drift between the configured ingress URL and the actual webhook registration. The raw URL fallback is preserved for local-dev operability.
|
|
220
261
|
|
|
221
|
-
##
|
|
222
|
-
|
|
223
|
-
By default, the broad runtime proxy is disabled. Dedicated gateway-managed routes (webhooks, delivery endpoints, explicit control-plane proxies such as `/v1/channel-verification-sessions/*`, `/v1/integrations/telegram/*`, `/v1/integrations/slack/*`, and `/v1/contacts/invites/*`, plus the authenticated runtime health route `/v1/health`) remain available, but arbitrary runtime passthrough routes return `404` unless the runtime proxy is enabled via workspace config.
|
|
224
|
-
|
|
225
|
-
## Runtime Proxy Mode
|
|
262
|
+
## Runtime Proxy
|
|
226
263
|
|
|
227
|
-
|
|
264
|
+
The gateway acts as the single ingress point for all traffic. Dedicated gateway routes (webhooks, control-plane proxies, health checks) are matched first; any request that doesn't match a specific route is forwarded to the assistant runtime via a catch-all proxy.
|
|
228
265
|
|
|
229
266
|
### Auth behavior
|
|
230
267
|
|
package/bun.lock
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"@vellumai/assistant-client": "file:../packages/assistant-client",
|
|
9
9
|
"@vellumai/ces-client": "file:../packages/ces-client",
|
|
10
10
|
"@vellumai/service-contracts": "file:../packages/service-contracts",
|
|
11
|
+
"@vellumai/slack-text": "file:../packages/slack-text",
|
|
12
|
+
"@vellumai/twilio-client": "file:../packages/twilio-client",
|
|
11
13
|
"drizzle-kit": "0.30.6",
|
|
12
14
|
"drizzle-orm": "0.45.2",
|
|
13
15
|
"file-type": "21.3.0",
|
|
@@ -209,6 +211,10 @@
|
|
|
209
211
|
|
|
210
212
|
"@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", { "dependencies": { "zod": "4.3.6" }, "devDependencies": { "@types/bun": "1.2.4", "typescript": "5.7.3" } }],
|
|
211
213
|
|
|
214
|
+
"@vellumai/slack-text": ["@vellumai/slack-text@file:../packages/slack-text", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
|
|
215
|
+
|
|
216
|
+
"@vellumai/twilio-client": ["@vellumai/twilio-client@file:../packages/twilio-client", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
|
|
217
|
+
|
|
212
218
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
|
213
219
|
|
|
214
220
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
|
@@ -349,7 +355,7 @@
|
|
|
349
355
|
|
|
350
356
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
|
351
357
|
|
|
352
|
-
"minimatch": ["minimatch@10.2.
|
|
358
|
+
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
|
353
359
|
|
|
354
360
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
|
355
361
|
|
|
@@ -491,7 +497,7 @@
|
|
|
491
497
|
|
|
492
498
|
"@vellumai/ces-client/@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
|
|
493
499
|
|
|
494
|
-
"@vellumai/ces-client/@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", {
|
|
500
|
+
"@vellumai/ces-client/@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", {}],
|
|
495
501
|
|
|
496
502
|
"@vellumai/ces-client/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
|
497
503
|
|
|
@@ -499,6 +505,10 @@
|
|
|
499
505
|
|
|
500
506
|
"@vellumai/service-contracts/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
|
501
507
|
|
|
508
|
+
"@vellumai/slack-text/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
509
|
+
|
|
510
|
+
"@vellumai/twilio-client/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
511
|
+
|
|
502
512
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
503
513
|
|
|
504
514
|
"gel/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
|
@@ -559,6 +569,10 @@
|
|
|
559
569
|
|
|
560
570
|
"@vellumai/service-contracts/@types/bun/bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
|
|
561
571
|
|
|
572
|
+
"@vellumai/slack-text/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
573
|
+
|
|
574
|
+
"@vellumai/twilio-client/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
575
|
+
|
|
562
576
|
"gel/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
|
563
577
|
|
|
564
578
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
package/knip.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/vellum-gateway",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"@vellumai/assistant-client": "file:../packages/assistant-client",
|
|
28
28
|
"@vellumai/ces-client": "file:../packages/ces-client",
|
|
29
29
|
"@vellumai/service-contracts": "file:../packages/service-contracts",
|
|
30
|
+
"@vellumai/slack-text": "file:../packages/slack-text",
|
|
31
|
+
"@vellumai/twilio-client": "file:../packages/twilio-client",
|
|
30
32
|
"drizzle-kit": "0.30.6",
|
|
31
33
|
"drizzle-orm": "0.45.2",
|
|
32
34
|
"file-type": "21.3.0",
|
|
@@ -52,6 +52,7 @@ describe("auto-approve thresholds", () => {
|
|
|
52
52
|
expect(data).toEqual({
|
|
53
53
|
interactive: "medium",
|
|
54
54
|
autonomous: "low",
|
|
55
|
+
headless: "none",
|
|
55
56
|
});
|
|
56
57
|
});
|
|
57
58
|
|
|
@@ -59,16 +60,15 @@ describe("auto-approve thresholds", () => {
|
|
|
59
60
|
const putHandler = createGlobalThresholdPutHandler();
|
|
60
61
|
const getHandler = createGlobalThresholdGetHandler();
|
|
61
62
|
|
|
62
|
-
// First PUT to set values
|
|
63
63
|
await putHandler(makeRequest({ interactive: "none" }));
|
|
64
64
|
|
|
65
|
-
// GET should reflect the update
|
|
66
65
|
const res = await getHandler(makeRequest(undefined, "GET"));
|
|
67
66
|
expect(res.status).toBe(200);
|
|
68
67
|
const data = await res.json();
|
|
69
68
|
expect(data).toEqual({
|
|
70
69
|
interactive: "none",
|
|
71
70
|
autonomous: "low",
|
|
71
|
+
headless: "none",
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
74
|
});
|
|
@@ -83,6 +83,7 @@ describe("auto-approve thresholds", () => {
|
|
|
83
83
|
expect(data).toEqual({
|
|
84
84
|
interactive: "none",
|
|
85
85
|
autonomous: "low",
|
|
86
|
+
headless: "none",
|
|
86
87
|
});
|
|
87
88
|
});
|
|
88
89
|
|
|
@@ -105,6 +106,7 @@ describe("auto-approve thresholds", () => {
|
|
|
105
106
|
expect(data).toEqual({
|
|
106
107
|
interactive: "high",
|
|
107
108
|
autonomous: "low",
|
|
109
|
+
headless: "none",
|
|
108
110
|
});
|
|
109
111
|
});
|
|
110
112
|
|
|
@@ -160,7 +162,6 @@ describe("auto-approve thresholds", () => {
|
|
|
160
162
|
test("upserts correctly — first write creates, second write updates", async () => {
|
|
161
163
|
const handler = createGlobalThresholdPutHandler();
|
|
162
164
|
|
|
163
|
-
// First write — creates the row
|
|
164
165
|
const res1 = await handler(
|
|
165
166
|
makeRequest({ interactive: "none", autonomous: "low" }),
|
|
166
167
|
);
|
|
@@ -169,17 +170,16 @@ describe("auto-approve thresholds", () => {
|
|
|
169
170
|
expect(data1).toEqual({
|
|
170
171
|
interactive: "none",
|
|
171
172
|
autonomous: "low",
|
|
173
|
+
headless: "none",
|
|
172
174
|
});
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
const res2 = await handler(
|
|
176
|
-
makeRequest({ autonomous: "medium" }),
|
|
177
|
-
);
|
|
176
|
+
const res2 = await handler(makeRequest({ autonomous: "medium" }));
|
|
178
177
|
expect(res2.status).toBe(200);
|
|
179
178
|
const data2 = await res2.json();
|
|
180
179
|
expect(data2).toEqual({
|
|
181
180
|
interactive: "none",
|
|
182
181
|
autonomous: "medium",
|
|
182
|
+
headless: "none",
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
185
|
|
|
@@ -187,42 +187,69 @@ describe("auto-approve thresholds", () => {
|
|
|
187
187
|
const handler = createGlobalThresholdPutHandler();
|
|
188
188
|
|
|
189
189
|
const res = await handler(
|
|
190
|
-
makeRequest({
|
|
191
|
-
interactive: "medium",
|
|
192
|
-
autonomous: "low",
|
|
193
|
-
}),
|
|
190
|
+
makeRequest({ interactive: "medium", autonomous: "low" }),
|
|
194
191
|
);
|
|
195
192
|
expect(res.status).toBe(200);
|
|
196
193
|
const data = await res.json();
|
|
197
194
|
expect(data).toEqual({
|
|
198
195
|
interactive: "medium",
|
|
199
196
|
autonomous: "low",
|
|
197
|
+
headless: "none",
|
|
200
198
|
});
|
|
201
199
|
});
|
|
202
200
|
|
|
203
201
|
test("empty object preserves existing values when row exists", async () => {
|
|
204
202
|
const putHandler = createGlobalThresholdPutHandler();
|
|
205
203
|
|
|
206
|
-
|
|
207
|
-
await putHandler(
|
|
208
|
-
makeRequest({
|
|
209
|
-
interactive: "medium",
|
|
210
|
-
autonomous: "low",
|
|
211
|
-
}),
|
|
212
|
-
);
|
|
204
|
+
await putHandler(makeRequest({ interactive: "medium", autonomous: "low" }));
|
|
213
205
|
|
|
214
|
-
// Then PUT empty object — existing values should be preserved
|
|
215
206
|
const res = await putHandler(makeRequest({}));
|
|
216
207
|
expect(res.status).toBe(200);
|
|
217
208
|
const data = await res.json();
|
|
218
209
|
expect(data).toEqual({
|
|
219
210
|
interactive: "medium",
|
|
220
211
|
autonomous: "low",
|
|
212
|
+
headless: "none",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── headless field ────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
test("can set headless threshold", async () => {
|
|
219
|
+
const handler = createGlobalThresholdPutHandler();
|
|
220
|
+
|
|
221
|
+
const res = await handler(makeRequest({ headless: "low" }));
|
|
222
|
+
expect(res.status).toBe(200);
|
|
223
|
+
const data = await res.json();
|
|
224
|
+
expect(data).toEqual({
|
|
225
|
+
interactive: "medium",
|
|
226
|
+
autonomous: "low",
|
|
227
|
+
headless: "low",
|
|
221
228
|
});
|
|
222
229
|
});
|
|
223
230
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
test("returns 400 for invalid headless value", async () => {
|
|
232
|
+
const handler = createGlobalThresholdPutHandler();
|
|
233
|
+
|
|
234
|
+
const res = await handler(makeRequest({ headless: "extreme" }));
|
|
235
|
+
expect(res.status).toBe(400);
|
|
236
|
+
const data = await res.json();
|
|
237
|
+
expect(data.error).toContain("headless");
|
|
238
|
+
expect(data.error).toContain("none, low, medium, high");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("headless is preserved when other fields are updated", async () => {
|
|
242
|
+
const handler = createGlobalThresholdPutHandler();
|
|
243
|
+
|
|
244
|
+
// Explicitly set headless to a non-default value
|
|
245
|
+
await handler(makeRequest({ headless: "low" }));
|
|
246
|
+
|
|
247
|
+
// Update only interactive — headless should remain "low"
|
|
248
|
+
const res = await handler(makeRequest({ interactive: "high" }));
|
|
249
|
+
expect(res.status).toBe(200);
|
|
250
|
+
const data = await res.json();
|
|
251
|
+
expect(data.headless).toBe("low");
|
|
252
|
+
expect(data.interactive).toBe("high");
|
|
253
|
+
});
|
|
227
254
|
});
|
|
228
255
|
});
|
|
@@ -27,7 +27,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
27
27
|
defaultAssistantId: undefined,
|
|
28
28
|
unmappedPolicy: "reject",
|
|
29
29
|
port: 7830,
|
|
30
|
-
runtimeProxyEnabled: false,
|
|
31
30
|
runtimeProxyRequireAuth: true,
|
|
32
31
|
shutdownDrainMs: 5000,
|
|
33
32
|
runtimeTimeoutMs: 30000,
|