@vellumai/vellum-gateway 0.12.3-staging.1 → 0.12.3
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 +1 -1
- package/ARCHITECTURE.md +34 -38
- package/README.md +29 -31
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +38 -7
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +5 -6
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +4 -8
- package/package.json +1 -1
- package/src/__tests__/config.test.ts +0 -1
- package/src/__tests__/handle-inbound-invite-intercept.test.ts +36 -41
- package/src/__tests__/pair-origin-allowlist.test.ts +20 -1
- package/src/__tests__/reply-path.test.ts +36 -0
- package/src/__tests__/runtime-client.test.ts +0 -84
- package/src/__tests__/text-verification-reply.test.ts +131 -0
- package/src/__tests__/verification-bootstrap-purpose.test.ts +126 -0
- package/src/channels/transport-hints.ts +9 -5
- package/src/config.ts +1 -2
- package/src/db/__tests__/session-store.test.ts +18 -4
- package/src/db/connection.ts +10 -0
- package/src/db/contact-store.ts +1 -3
- package/src/db/session-store.ts +21 -15
- package/src/feature-flag-registry.json +16 -0
- package/src/http/browser-auth-cookies.ts +1 -5
- package/src/http/middleware/cors.ts +4 -2
- package/src/http/routes/log-export.ts +1 -1
- package/src/http/routes/telegram-webhook.test.ts +0 -1
- package/src/http/routes/telegram-webhook.ts +0 -10
- package/src/http/routes/twilio-media-websocket.ts +4 -19
- package/src/http/routes/whatsapp-webhook.test.ts +0 -3
- package/src/http/routes/whatsapp-webhook.ts +1 -4
- package/src/index.ts +3 -3
- package/src/ipc/debug-export-handlers.test.ts +103 -0
- package/src/ipc/debug-export-handlers.ts +81 -0
- package/src/risk/bash-risk-classifier.test.ts +2 -1
- package/src/risk/bash-risk-classifier.ts +1 -3
- package/src/risk/command-registry/commands/assistant.ts +1 -0
- package/src/runtime/client.ts +0 -122
- package/src/telegram/api.ts +0 -33
- package/src/telegram/send.test.ts +16 -117
- package/src/telegram/send.ts +3 -157
- package/src/velay/binary-websocket.test.ts +53 -0
- package/src/velay/binary-websocket.ts +46 -0
- package/src/velay/client.test.ts +73 -1
- package/src/velay/client.ts +11 -1
- package/src/velay/protocol.ts +18 -0
- package/src/velay/websocket-bridge.test.ts +181 -56
- package/src/velay/websocket-bridge.ts +21 -3
- package/src/verification/invite-redemption.ts +11 -17
- package/src/verification/reply-delivery.ts +49 -64
- package/src/verification/session-service.ts +15 -5
- package/src/verification/text-verification.ts +2 -2
- package/src/whatsapp/api.ts +0 -145
- package/src/whatsapp/send.ts +1 -198
- package/src/__tests__/telegram-send-attachments.test.ts +0 -502
package/AGENTS.md
CHANGED
|
@@ -221,7 +221,7 @@ Two orthogonal axes, do not conflate them:
|
|
|
221
221
|
- **Admission** (above) — _who gets in the door_. `TRUST_CLASS_RANK` vs `ADMISSION_FLOOR`, enforced across gateway + runtime.
|
|
222
222
|
- **Capabilities** — _what an actor may do once admitted_. Resolved in the runtime, never on the gateway.
|
|
223
223
|
|
|
224
|
-
**Trust classes** (`TrustClass` in `
|
|
224
|
+
**Trust classes** (`TrustClass` in `packages/gateway-client/src/trust-verdict-contract.ts`; the daemon imports it and never redeclares it) are the _role_, ranked by `TRUST_CLASS_RANK`:
|
|
225
225
|
|
|
226
226
|
| Class | Rank | Meaning |
|
|
227
227
|
| -------------------- | ---- | ------------------------------------------------------------------------ |
|
package/ARCHITECTURE.md
CHANGED
|
@@ -282,7 +282,7 @@ Channel bindings follow a three-phase lifecycle:
|
|
|
282
282
|
|
|
283
283
|
1. **Bind** — An inbound message from an external channel (e.g., Telegram chat) arrives at the gateway, which normalizes it and forwards it to the runtime's `/v1/channels/inbound` endpoint. The runtime creates or reuses a conversation, establishing the channel binding (`sourceChannel` metadata on the conversation).
|
|
284
284
|
|
|
285
|
-
2. **Route**: Subsequent messages on the same external chat are routed to the same conversation via the channel binding. Slack and Telegram are thread-scoped: a message that arrives in a Slack thread (including every message in a Slack agent DM, which Slack always delivers in a thread) or a Telegram topic resolves to that thread's own conversation, keyed on the chat plus the thread id (`assistant/src/persistence/delivery-crud.ts`, `buildScopedConversationKey`); a thread-less message resolves to the chat's base conversation. Replies from the assistant
|
|
285
|
+
2. **Route**: Subsequent messages on the same external chat are routed to the same conversation via the channel binding. Slack and Telegram are thread-scoped: a message that arrives in a Slack thread (including every message in a Slack agent DM, which Slack always delivers in a thread) or a Telegram topic resolves to that thread's own conversation, keyed on the chat plus the thread id (`assistant/src/persistence/delivery-crud.ts`, `buildScopedConversationKey`); a thread-less message resolves to the chat's base conversation. Replies from the assistant go out through the daemon's channel transport for that channel (`assistant/src/messaging/providers`), which calls the provider's API directly; they never pass back through the gateway. The desktop client filters out channel-bound conversations during conversation restoration (`ConversationRestorer`) so they never appear in the desktop conversation list.
|
|
286
286
|
|
|
287
287
|
3. **Rebind** — If a message arrives on an external chat whose conversation was previously deleted, the channel inbound handler treats it as a new conversation and establishes a fresh binding. The external chat ID is reused, but the conversation is new.
|
|
288
288
|
|
|
@@ -371,18 +371,18 @@ Telegram messages follow three paths through the system:
|
|
|
371
371
|
Inbound (user → assistant):
|
|
372
372
|
Telegram → Gateway POST /webhooks/telegram → verify secret → normalize → route
|
|
373
373
|
→ Runtime POST /v1/assistants/:id/channels/inbound
|
|
374
|
-
(replyCallbackUrl = ${gatewayInternalBaseUrl}/deliver/telegram)
|
|
374
|
+
(replyCallbackUrl = ${gatewayInternalBaseUrl}/deliver/telegram[?threadId=<topic>])
|
|
375
375
|
|
|
376
376
|
Outbound reply (assistant → user, triggered by inbound):
|
|
377
|
-
|
|
377
|
+
Daemon Telegram transport → Telegram Bot API sendMessage/sendPhoto/sendDocument/sendChatAction
|
|
378
378
|
|
|
379
|
-
Outbound proactive (assistant → user,
|
|
380
|
-
|
|
379
|
+
Outbound proactive (assistant → user, messaging tool or POST /v1/channels/send):
|
|
380
|
+
Daemon Telegram transport → Telegram Bot API sendMessage
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
The `replyCallbackUrl`
|
|
383
|
+
The gateway does not serve `/deliver/telegram`. The `replyCallbackUrl` it attaches to the inbound forward is an addressing token: the daemon resolves its path to the channel's transport (`channelForCallback` in `assistant/src/messaging/providers/callback-routing.ts`) and reads per-channel parameters from its query (the Telegram transport reads `threadId` to reply into the same topic). The daemon never dials the URL's host and port, which come from `gatewayInternalBaseUrl` (`http://127.0.0.1:${GATEWAY_PORT}`, `gateway/src/config.ts`). The gateway's own replies to invite and verification codes it intercepts at ingress go out the same way: `deliverVerificationReply` (`gateway/src/verification/reply-delivery.ts`) hands the reply and the inbound message's callback URL to the daemon's IPC-only `deliver_gateway_reply` method, which resolves the transport from that URL as it does for any reply. The gateway never sends those replies to a provider itself. The daemon's Telegram transport (`assistant/src/messaging/providers/telegram-bot/`) reads the bot token from credential storage and calls the Bot API itself.
|
|
384
384
|
|
|
385
|
-
The
|
|
385
|
+
The gateway sends to Telegram on its own only for notices it composes while handling the webhook, before or instead of a runtime turn: the `/start` acknowledgement, the "not fully set up" routing-rejection notice, a setup-hiccup notice when the forward fails, and the denial text the runtime returns when ingress ACL rejects the sender. These go through `sendTelegramReply` in `gateway/src/telegram/send.ts`.
|
|
386
386
|
|
|
387
387
|
**Bot-account limitations:** The Telegram Bot API only supports sending messages to chats that have previously interacted with the bot. Bots cannot enumerate chats, read message history, or search messages. A future MTProto user-account session track may lift some of these restrictions.
|
|
388
388
|
|
|
@@ -403,8 +403,7 @@ The run transitions to `NeedsConfirmation` when the agent loop emits a `confirma
|
|
|
403
403
|
```
|
|
404
404
|
Runtime detects needs_confirmation
|
|
405
405
|
→ runtime builds approval prompt + UI metadata
|
|
406
|
-
→
|
|
407
|
-
→ gateway renders inline keyboard (buttons: Approve once, Approve always, Reject)
|
|
406
|
+
→ daemon Telegram transport sends the prompt with an inline keyboard (buttons: Approve once, Approve always, Reject)
|
|
408
407
|
→ user clicks button → Telegram callback_query
|
|
409
408
|
→ gateway normalizes callback_query into inbound event (callbackData field)
|
|
410
409
|
→ runtime parses callback data (format: apr:<requestId>:<action>)
|
|
@@ -431,19 +430,19 @@ Runtime detects needs_confirmation
|
|
|
431
430
|
|
|
432
431
|
**Key modules:**
|
|
433
432
|
|
|
434
|
-
| Module
|
|
435
|
-
|
|
|
436
|
-
| `assistant/src/runtime/approval-conversation-turn.ts`
|
|
437
|
-
| `assistant/src/runtime/approval-message-composer.ts`
|
|
438
|
-
| `assistant/src/runtime/channel-approvals.ts`
|
|
439
|
-
| `assistant/src/runtime/channel-approval-types.ts`
|
|
440
|
-
| `assistant/src/runtime/routes/channel-routes.ts`
|
|
441
|
-
| `assistant/src/runtime/channel-verification-service.ts`
|
|
442
|
-
| `assistant/src/memory/delivery-channels.ts`
|
|
443
|
-
| `assistant/src/channels/gateway-guardian-requests.ts`
|
|
444
|
-
| `assistant/src/runtime/gateway-client.ts`
|
|
445
|
-
| `
|
|
446
|
-
| `gateway/src/telegram/normalize.ts`
|
|
433
|
+
| Module | Purpose |
|
|
434
|
+
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
435
|
+
| `assistant/src/runtime/approval-conversation-turn.ts` | Conversational approval turn engine: LLM-based intent classification (structured output) for pending approval follow-ups, with fail-closed safety |
|
|
436
|
+
| `assistant/src/runtime/approval-message-composer.ts` | Centralized approval message composition: layered source selection (assistant preface → deterministic fallback) for all approval/guardian/verification user-facing copy |
|
|
437
|
+
| `assistant/src/runtime/channel-approvals.ts` | Orchestration: detect pending confirmations, build prompts (including guardian-aware prompts), apply decisions, plain-text fallback selection |
|
|
438
|
+
| `assistant/src/runtime/channel-approval-types.ts` | Shared types: actions, prompts, UI metadata, decisions |
|
|
439
|
+
| `assistant/src/runtime/routes/channel-routes.ts` | Integration point: approval interception, actor role resolution, guardian approval routing, deliver-once guard, fail-closed prompt delivery |
|
|
440
|
+
| `assistant/src/runtime/channel-verification-service.ts` | Guardian binding lookups: `isGuardian()`, `getGuardianBinding()` |
|
|
441
|
+
| `assistant/src/memory/delivery-channels.ts` | `claimRunDelivery()`: in-memory deliver-once guard for terminal reply idempotency |
|
|
442
|
+
| `assistant/src/channels/gateway-guardian-requests.ts` | Typed daemon client for the gateway-owned `guardian_requests` lifecycle (`guardian_requests_create` / `_decide` / `_list_expired_pending` / `_expire`) |
|
|
443
|
+
| `assistant/src/runtime/gateway-client.ts` | `deliverApprovalPrompt()`: hands the approval prompt to the channel transport the callback URL names |
|
|
444
|
+
| `assistant/src/messaging/providers/telegram-bot/send.ts` | `buildInlineKeyboard()`: renders approval actions as Telegram inline buttons |
|
|
445
|
+
| `gateway/src/telegram/normalize.ts` | `callback_query` normalization into `GatewayInboundEvent` (private chats, groups, and supergroups; drops callbacks without data) |
|
|
447
446
|
|
|
448
447
|
### Approval Message Composer
|
|
449
448
|
|
|
@@ -562,19 +561,19 @@ sequenceDiagram
|
|
|
562
561
|
GW->>Daemon: POST /v1/channels/inbound (JWT auth)
|
|
563
562
|
Daemon->>Daemon: Detect non-guardian, set forcePromptSideEffects
|
|
564
563
|
Daemon->>Daemon: Tool needs confirmation → create GuardianApprovalRequest
|
|
565
|
-
Daemon->>
|
|
566
|
-
|
|
567
|
-
Daemon->>
|
|
568
|
-
|
|
564
|
+
Daemon->>TG: sendMessage via Telegram transport (approval prompt + inline keyboard)
|
|
565
|
+
TG-->>Guardian: Approval prompt
|
|
566
|
+
Daemon->>TG: sendMessage via Telegram transport (requester notification)
|
|
567
|
+
TG-->>NG: "Waiting for guardian approval..."
|
|
569
568
|
Guardian->>TG: Approve / Deny (callback_query or text)
|
|
570
569
|
TG->>GW: POST /webhooks/telegram (callback_query)
|
|
571
570
|
GW->>Daemon: POST /v1/channels/inbound (JWT auth)
|
|
572
571
|
Daemon->>Daemon: Validate guardian identity, update approval decision
|
|
573
572
|
Daemon->>Daemon: Apply decision to pending run
|
|
574
|
-
Daemon->>
|
|
575
|
-
|
|
576
|
-
Daemon->>
|
|
577
|
-
|
|
573
|
+
Daemon->>TG: sendMessage via Telegram transport (outcome notification)
|
|
574
|
+
TG-->>NG: "Guardian approved/denied your request"
|
|
575
|
+
Daemon->>TG: sendMessage via Telegram transport (confirmation)
|
|
576
|
+
TG-->>Guardian: Confirmation of decision
|
|
578
577
|
```
|
|
579
578
|
|
|
580
579
|
Approval state lives in the gateway's `guardian_requests` table (kind `tool_approval`), with per-surface card deliveries in `guardian_request_deliveries`. Each request records the requester, guardian, tool name, risk level, and decision outcome; decisions commit atomically via the gateway's `guardian_requests_decide` IPC route.
|
|
@@ -732,14 +731,11 @@ The Slack channel enables inbound and outbound messaging via Slack's Socket Mode
|
|
|
732
731
|
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.
|
|
733
732
|
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`.
|
|
734
733
|
5. Routing uses the standard `resolveAssistant()` chain (conversation_id -> actor_id -> default/reject). Events that cannot be routed are dropped.
|
|
735
|
-
6. The normalized event is forwarded to the runtime via `POST /v1/channels/inbound` with a `replyCallbackUrl`
|
|
734
|
+
6. The normalized event is forwarded to the runtime via `POST /v1/channels/inbound` with a `replyCallbackUrl` of `/deliver/slack?channel=<id>`, plus `threadTs` for a threaded message or `messageTs` for a thread-less message.
|
|
736
735
|
|
|
737
|
-
**Egress
|
|
736
|
+
**Egress:**
|
|
738
737
|
|
|
739
|
-
|
|
740
|
-
2. The gateway authenticates the request via bearer token (same fail-closed model as other deliver endpoints).
|
|
741
|
-
3. The gateway posts the message via `POST https://slack.com/api/chat.postMessage` using the bot token.
|
|
742
|
-
4. Threading is supported via a `threadTs` query parameter on the deliver URL. When present, replies are posted as thread replies to the specified message timestamp.
|
|
738
|
+
The gateway does not serve `/deliver/slack`; for the daemon the callback URL only addresses the reply (the gateway's intercepted-code replies reach the same transport through the daemon, see Telegram Messaging Flow). The daemon's Slack transport (`assistant/src/messaging/providers/slack/`) calls the Slack Web API itself with the bot token: `chat.postMessage` and `chat.update` for whole messages, and `chat.startStream` / `chat.appendStream` / `chat.stopStream` for streamed replies. It reads `threadTs` from the callback URL to reply in the thread and `messageTs` to anchor the busy indicator on a thread-less message.
|
|
743
739
|
|
|
744
740
|
**Credential management:**
|
|
745
741
|
|
|
@@ -773,7 +769,7 @@ Any persistent-stream transport that does not buffer events for disconnected cli
|
|
|
773
769
|
| `gateway/src/slack/socket-mode.ts` | `SlackSocketModeClient`: WebSocket lifecycle, ACK, dedup, auto-reconnect, reconnect catch-up |
|
|
774
770
|
| `gateway/src/slack/slack-web.ts` | `conversations.history` / `conversations.replies` helpers for reconnect catch-up |
|
|
775
771
|
| `gateway/src/slack/message-normalizer.ts` | Normalizers per event family (`normalizeSlackAppMention()`, DM, group DM, channel message) with bot-mention stripping |
|
|
776
|
-
| `gateway/src/index.ts` | `/deliver/slack`
|
|
772
|
+
| `gateway/src/index.ts` | Builds the `/deliver/slack` callback URL (channel, thread ts, message ts) the daemon's Slack transport replies to |
|
|
777
773
|
|
|
778
774
|
**What the ingress does not do:** it never forwards the bot's own posts to the daemon (except a deletion of one, which the daemon records), and it never reads history on the daemon's behalf beyond the bounded reconnect catch-up above; the daemon's inbound-triggered backfill hydrates context.
|
|
779
775
|
|
|
@@ -846,7 +842,7 @@ sequenceDiagram
|
|
|
846
842
|
Ctrl->>CallStore: createPendingQuestion()
|
|
847
843
|
Ctrl->>GuardianDispatch: dispatchGuardianQuestion()
|
|
848
844
|
GuardianDispatch->>Mac: notification_conversation_created SSE
|
|
849
|
-
GuardianDispatch->>TG:
|
|
845
|
+
GuardianDispatch->>TG: sendMessage via notification pipeline
|
|
850
846
|
Note over Mac,TG: First channel to respond wins
|
|
851
847
|
Mac/TG->>Routes: guardian answer
|
|
852
848
|
Routes->>CallDomain: answerCall()
|
package/README.md
CHANGED
|
@@ -58,19 +58,6 @@ For manual setup (or reference), register the webhook with Telegram using the `s
|
|
|
58
58
|
|
|
59
59
|
See the [Telegram Bot API docs](https://core.telegram.org/bots/api#setwebhook) for the full API reference.
|
|
60
60
|
|
|
61
|
-
## Telegram Deliver Endpoint Security
|
|
62
|
-
|
|
63
|
-
The `/deliver/telegram` endpoint requires bearer auth by default (fail-closed). The security behavior is:
|
|
64
|
-
|
|
65
|
-
| Condition | Result |
|
|
66
|
-
| ----------------------------------------------------------------------------------------- | -------------------------- |
|
|
67
|
-
| Bearer token configured + valid `Authorization` header | Request allowed |
|
|
68
|
-
| Bearer token configured + missing/invalid `Authorization` header | 401 Unauthorized |
|
|
69
|
-
| No bearer token configured + `telegram.deliverAuthBypass=true` in `workspace/config.json` | Request allowed (dev-only) |
|
|
70
|
-
| No bearer token configured + bypass not set | 503 Service Not Configured |
|
|
71
|
-
|
|
72
|
-
This ensures that misconfiguration cannot expose an unauthenticated public message-send surface. In production, ensure JWT authentication is properly configured. The `telegram.deliverAuthBypass` config flag (in `workspace/config.json`) is intended for local development only and requires `APP_VERSION=0.0.0-dev`.
|
|
73
|
-
|
|
74
61
|
## Voice Ingress — Inbound Calls (Twilio)
|
|
75
62
|
|
|
76
63
|
The `/webhooks/twilio/voice` endpoint handles both outbound and inbound voice calls. For **outbound** calls (initiated by the assistant via `call_start`), the voice webhook URL includes a `callSessionId` query parameter that identifies the pre-created session. For **inbound** calls (someone dialing the assistant's Twilio phone number), no `callSessionId` is present — the gateway resolves the target assistant and the runtime creates a session on the fly.
|
|
@@ -112,9 +99,9 @@ These fields are forwarded to the runtime in the `/channels/inbound` payload alo
|
|
|
112
99
|
|
|
113
100
|
## Approval Buttons and Inline Keyboard
|
|
114
101
|
|
|
115
|
-
The
|
|
102
|
+
The gateway does not send approval prompts. The assistant's Telegram transport (`assistant/src/messaging/providers/telegram-bot/`) sends them to the Bot API directly, and when the reply payload carries an `approval` field it renders Telegram inline keyboard buttons below the message text. The gateway's part is the return trip: it normalizes the button press (`callback_query`) and forwards it as described above.
|
|
116
103
|
|
|
117
|
-
**Approval payload shape:**
|
|
104
|
+
**Approval reply payload shape:**
|
|
118
105
|
|
|
119
106
|
```json
|
|
120
107
|
{
|
|
@@ -134,7 +121,7 @@ The `/deliver/telegram` endpoint accepts an optional `approval` field in the req
|
|
|
134
121
|
|
|
135
122
|
**Inline keyboard format:** Each action is rendered as a single-button row. The callback data uses the compact format `apr:<requestId>:<action>` (e.g., `apr:request-uuid:approve_once`) so the runtime can parse it back when the button is clicked.
|
|
136
123
|
|
|
137
|
-
**Fallback behavior:** For non-rich channels that do not support inline keyboards, the runtime substitutes the `plainTextFallback` string for the structured `promptText` before
|
|
124
|
+
**Fallback behavior:** For non-rich channels that do not support inline keyboards, the runtime substitutes the `plainTextFallback` string for the structured `promptText` before handing the prompt to the channel transport. The fallback includes plain-text instructions so the user can respond via text. The `supportsInlineOptions` channel capability (`channelSupportsInlineOptions()`) in the runtime determines which format to use. Free-text responses are classified by the conversational approval engine.
|
|
138
125
|
|
|
139
126
|
## Public Ingress Routes
|
|
140
127
|
|
|
@@ -145,7 +132,6 @@ Control-plane routes are listed with their flat paths. Clients emit assistant-sc
|
|
|
145
132
|
| Route | Method | Description |
|
|
146
133
|
| ----------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
147
134
|
| `/webhooks/telegram` | POST | Telegram bot webhook (validated via `TELEGRAM_WEBHOOK_SECRET`) |
|
|
148
|
-
| `/deliver/telegram` | POST | Internal endpoint for the assistant runtime to deliver outbound messages/attachments to Telegram chats |
|
|
149
135
|
| `/webhooks/twilio/voice` | POST | Twilio voice webhook (validated via HMAC-SHA1 signature) |
|
|
150
136
|
| `/webhooks/twilio/status` | POST | Twilio status callback (validated via HMAC-SHA1 signature) |
|
|
151
137
|
| `/webhooks/twilio/media-stream/:callSessionId/:token` | WS | Twilio Media Streams WebSocket (bidirectional proxy to runtime; handshake metadata in URL path segments) |
|
|
@@ -304,15 +290,13 @@ curl -i -X POST http://localhost:7830/webhooks/telegram
|
|
|
304
290
|
|
|
305
291
|
## Outbound Attachments (Telegram)
|
|
306
292
|
|
|
307
|
-
|
|
293
|
+
The gateway does not send attachments. When a reply carries attachments, the assistant's Telegram transport (`assistant/src/messaging/providers/telegram-bot/`) reads each one from the assistant's attachment store and uploads it to the Bot API itself:
|
|
308
294
|
|
|
309
|
-
- **Images** (`image
|
|
310
|
-
- **
|
|
311
|
-
- **
|
|
312
|
-
- **Partial failures** are handled gracefully: each attachment is attempted independently. If any fail, a single summary notice is sent to the chat listing the undelivered filenames.
|
|
313
|
-
- **Concurrency** is controlled by a hardcoded max concurrency limit (default 3).
|
|
295
|
+
- **Images** (`image/jpeg`, `image/png`, `image/gif`, `image/webp`) within the Bot API's 10 MB photo upload limit are sent via `sendPhoto`; **larger images and other files** via `sendDocument`.
|
|
296
|
+
- **Oversized** attachments (over the 50 MB `sendDocument` limit) are skipped.
|
|
297
|
+
- **Partial failures**: each attachment is attempted on its own, and if any fail, one summary notice lists the undelivered filenames.
|
|
314
298
|
|
|
315
|
-
|
|
299
|
+
The text reply goes first via `sendMessage`, then each attachment follows in order.
|
|
316
300
|
|
|
317
301
|
## Health & Readiness Probes
|
|
318
302
|
|
|
@@ -385,10 +369,24 @@ See [`benchmarking/gateway/README.md`](../benchmarking/gateway/README.md) for lo
|
|
|
385
369
|
|
|
386
370
|
### Guardian-Specific Troubleshooting
|
|
387
371
|
|
|
388
|
-
| Symptom | Cause | Resolution
|
|
389
|
-
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
390
|
-
| Guardian verification code reply gets no response | The
|
|
391
|
-
| Non-guardian actions auto-denied with "no guardian configured" | No guardian binding exists for the channel. The runtime is fail-closed for unverified channels. | Set up a guardian by running the verification flow from the desktop UI.
|
|
392
|
-
| Approval prompt not delivered to guardian | The
|
|
393
|
-
| Guardian approval expired | The 30-minute TTL elapsed without a decision. A proactive sweep (every 60s) auto-denied the approval and notified both the requester and guardian. | The non-guardian user must re-trigger the action.
|
|
394
|
-
| "Only the verified guardian can approve or deny" | A non-guardian sender attempted to respond to a guardian approval prompt | Only the guardian whose `actorExternalId` matches the approval request can approve or deny.
|
|
372
|
+
| Symptom | Cause | Resolution |
|
|
373
|
+
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
374
|
+
| Guardian verification code reply gets no response | The code never reached the gateway, the challenge expired, or the assistant could not send the gateway's reply | Ensure the gateway is running, the bot token is valid, and the Telegram webhook is registered. Search the gateway log for `Verification reply`: a reply the assistant could not send does not undo the verification, so check the binding with the `channel-verification-sessions status` CLI command. Challenges expire after 10 minutes -- generate a new one via the desktop UI. |
|
|
375
|
+
| Non-guardian actions auto-denied with "no guardian configured" | No guardian binding exists for the channel. The runtime is fail-closed for unverified channels. | Set up a guardian by running the verification flow from the desktop UI. |
|
|
376
|
+
| Approval prompt not delivered to guardian | The assistant's channel transport could not send it (for example a missing or invalid bot token), or the guardian's chat ID is stale | Check the assistant log for the transport's delivery error and confirm the channel's bot token is configured. For a channel with a transport the assistant never dials the `replyCallbackUrl`, so gateway reachability is not the cause. Re-verify the guardian if the chat ID has changed. |
|
|
377
|
+
| Guardian approval expired | The 30-minute TTL elapsed without a decision. A proactive sweep (every 60s) auto-denied the approval and notified both the requester and guardian. | The non-guardian user must re-trigger the action. |
|
|
378
|
+
| "Only the verified guardian can approve or deny" | A non-guardian sender attempted to respond to a guardian approval prompt | Only the guardian whose `actorExternalId` matches the approval request can approve or deny. |
|
|
379
|
+
|
|
380
|
+
### Binary virtual desktop tunnel
|
|
381
|
+
|
|
382
|
+
The gateway advertises `X-Vellum-Velay-Binary-WebSocket: 1` when registering.
|
|
383
|
+
It uses binary tunnel messages only when Velay sends `binary_messages: true`
|
|
384
|
+
on a `/v1/desktop/stream` open frame. The envelope is byte `0x01`, 32 lowercase
|
|
385
|
+
ASCII hex connection-ID bytes, then the unchanged payload (including empty
|
|
386
|
+
payloads). Control and text messages remain JSON. Other routes and older relays
|
|
387
|
+
retain JSON/base64 framing. Deploy Velay support before updating gateways;
|
|
388
|
+
older gateways can continue using the upgraded relay.
|
|
389
|
+
|
|
390
|
+
Malformed tunnel messages retain the existing log-and-ignore behavior. Raw binary
|
|
391
|
+
frames addressed to streams without desktop negotiation are ignored, preserving
|
|
392
|
+
other active streams on the shared tunnel.
|
package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"./reactions": "./src/reactions.ts",
|
|
28
28
|
"./guardian-requests": "./src/guardian-requests.ts",
|
|
29
29
|
"./platform-credential": "./src/platform-credential.ts",
|
|
30
|
-
"./plan-credit": "./src/plan-credit.ts"
|
|
30
|
+
"./plan-credit": "./src/plan-credit.ts",
|
|
31
|
+
"./integration-categories": "./src/integration-categories.ts"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"typecheck": "bunx tsc --noEmit",
|
package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export * from "./trust-rules.js";
|
|
|
29
29
|
export * from "./ingress.js";
|
|
30
30
|
export * from "./no-response.js";
|
|
31
31
|
export * from "./plan-credit.js";
|
|
32
|
+
export * from "./integration-categories.js";
|
|
32
33
|
export * from "./platform-credential.js";
|
|
33
34
|
export * from "./remote-web-pairing.js";
|
|
34
35
|
export * from "./twilio-ingress.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category vocabulary for the integrations catalog, shared by the assistant
|
|
3
|
+
* daemon (which stamps it on OAuth providers and marketplace MCP entries) and
|
|
4
|
+
* the clients (which draw the category filter from it).
|
|
5
|
+
*
|
|
6
|
+
* The order here is the order clients show the categories in. The slug is the
|
|
7
|
+
* wire value; the user-facing label is the client's, so it can be translated.
|
|
8
|
+
*/
|
|
9
|
+
export const INTEGRATION_CATEGORIES = [
|
|
10
|
+
"productivity",
|
|
11
|
+
"communication",
|
|
12
|
+
"meetings",
|
|
13
|
+
"sales",
|
|
14
|
+
"marketing",
|
|
15
|
+
"finance",
|
|
16
|
+
"commerce",
|
|
17
|
+
"engineering",
|
|
18
|
+
"knowledge",
|
|
19
|
+
"recruiting",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type IntegrationCategory = (typeof INTEGRATION_CATEGORIES)[number];
|
|
23
|
+
|
|
24
|
+
const INTEGRATION_CATEGORY_SET: ReadonlySet<string> = new Set(
|
|
25
|
+
INTEGRATION_CATEGORIES,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether `value` names a category this build knows. A client reading a newer
|
|
30
|
+
* catalog treats an unknown slug as uncategorized rather than failing.
|
|
31
|
+
*/
|
|
32
|
+
export function isIntegrationCategory(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): value is IntegrationCategory {
|
|
35
|
+
return typeof value === "string" && INTEGRATION_CATEGORY_SET.has(value);
|
|
36
|
+
}
|
package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"./reactions": "./src/reactions.ts",
|
|
28
28
|
"./guardian-requests": "./src/guardian-requests.ts",
|
|
29
29
|
"./platform-credential": "./src/platform-credential.ts",
|
|
30
|
-
"./plan-credit": "./src/plan-credit.ts"
|
|
30
|
+
"./plan-credit": "./src/plan-credit.ts",
|
|
31
|
+
"./integration-categories": "./src/integration-categories.ts"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"typecheck": "bunx tsc --noEmit",
|
package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export * from "./trust-rules.js";
|
|
|
29
29
|
export * from "./ingress.js";
|
|
30
30
|
export * from "./no-response.js";
|
|
31
31
|
export * from "./plan-credit.js";
|
|
32
|
+
export * from "./integration-categories.js";
|
|
32
33
|
export * from "./platform-credential.js";
|
|
33
34
|
export * from "./remote-web-pairing.js";
|
|
34
35
|
export * from "./twilio-ingress.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category vocabulary for the integrations catalog, shared by the assistant
|
|
3
|
+
* daemon (which stamps it on OAuth providers and marketplace MCP entries) and
|
|
4
|
+
* the clients (which draw the category filter from it).
|
|
5
|
+
*
|
|
6
|
+
* The order here is the order clients show the categories in. The slug is the
|
|
7
|
+
* wire value; the user-facing label is the client's, so it can be translated.
|
|
8
|
+
*/
|
|
9
|
+
export const INTEGRATION_CATEGORIES = [
|
|
10
|
+
"productivity",
|
|
11
|
+
"communication",
|
|
12
|
+
"meetings",
|
|
13
|
+
"sales",
|
|
14
|
+
"marketing",
|
|
15
|
+
"finance",
|
|
16
|
+
"commerce",
|
|
17
|
+
"engineering",
|
|
18
|
+
"knowledge",
|
|
19
|
+
"recruiting",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type IntegrationCategory = (typeof INTEGRATION_CATEGORIES)[number];
|
|
23
|
+
|
|
24
|
+
const INTEGRATION_CATEGORY_SET: ReadonlySet<string> = new Set(
|
|
25
|
+
INTEGRATION_CATEGORIES,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether `value` names a category this build knows. A client reading a newer
|
|
30
|
+
* catalog treats an unknown slug as uncategorized rather than failing.
|
|
31
|
+
*/
|
|
32
|
+
export function isIntegrationCategory(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): value is IntegrationCategory {
|
|
35
|
+
return typeof value === "string" && INTEGRATION_CATEGORY_SET.has(value);
|
|
36
|
+
}
|
|
@@ -43,6 +43,26 @@ export type GatewayLogsTailRouteParams = z.infer<
|
|
|
43
43
|
typeof GatewayLogsTailRouteParamsSchema
|
|
44
44
|
>;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* `gateway_debug_export`: a consistent copy of the gateway database plus its
|
|
48
|
+
* recent log files, as one gzipped tar, for a debug bundle. No params.
|
|
49
|
+
*/
|
|
50
|
+
export const GatewayDebugExportIpcParamsSchema = z
|
|
51
|
+
.object({})
|
|
52
|
+
.strict()
|
|
53
|
+
.default({});
|
|
54
|
+
|
|
55
|
+
export const GatewayDebugExportIpcResponseSchema = z.object({
|
|
56
|
+
ok: z.literal(true),
|
|
57
|
+
/** `tar.gz` bytes, base64. Contains gateway.sqlite and gateway-logs/. */
|
|
58
|
+
archive_base64: z.string(),
|
|
59
|
+
size_bytes: z.number().int().nonnegative(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type GatewayDebugExportIpcResponse = z.infer<
|
|
63
|
+
typeof GatewayDebugExportIpcResponseSchema
|
|
64
|
+
>;
|
|
65
|
+
|
|
46
66
|
export const GatewayLogsTailIpcResponseSchema = z.object({
|
|
47
67
|
lines: z.array(z.record(z.string(), z.unknown())),
|
|
48
68
|
truncated: z.boolean(),
|
|
@@ -24,13 +24,15 @@ export {
|
|
|
24
24
|
PersistentIpcClient,
|
|
25
25
|
} from "./ipc-client.js";
|
|
26
26
|
|
|
27
|
-
// Outbound delivery contract
|
|
27
|
+
// Outbound delivery contract: Zod schemas + derived types
|
|
28
28
|
export {
|
|
29
29
|
ApprovalActionOptionSchema,
|
|
30
30
|
ApprovalUIMetadataSchema,
|
|
31
31
|
AttachmentMetadataSchema,
|
|
32
32
|
ChannelDeliveryResultSchema,
|
|
33
33
|
ChannelReplyPayloadSchema,
|
|
34
|
+
DELIVER_GATEWAY_REPLY_IPC_METHOD,
|
|
35
|
+
GatewayReplyRequestSchema,
|
|
34
36
|
MessageAudienceSchema,
|
|
35
37
|
PermissionRequestDetailsSchema,
|
|
36
38
|
StreamOpSchema,
|
|
@@ -45,6 +47,7 @@ export type {
|
|
|
45
47
|
AttachmentMetadata,
|
|
46
48
|
ChannelDeliveryResult,
|
|
47
49
|
ChannelReplyPayload,
|
|
50
|
+
GatewayReplyRequest,
|
|
48
51
|
MessageAudience,
|
|
49
52
|
PermissionRequestDetails,
|
|
50
53
|
StreamOp,
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Outbound channel delivery contract.
|
|
3
3
|
*
|
|
4
|
-
* Zod schemas
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Zod schemas for a reply to a channel chat. The daemon constructs these
|
|
5
|
+
* payloads in `deliverChannelReply()` and `deliverApprovalPrompt()` and hands
|
|
6
|
+
* them to the channel transport its callback URL names
|
|
7
|
+
* (`messaging/providers`). `/deliver/{channel}` is only that callback URL's
|
|
8
|
+
* addressing form: no service serves it. A callback URL no
|
|
9
|
+
* transport owns (a managed callback carrying a `callback_token`) is POSTed
|
|
10
|
+
* over HTTP by `http-delivery.ts` instead.
|
|
7
11
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* The gateway sends through the same transports, never to a provider itself:
|
|
13
|
+
* a reply it composes for a message it answered at ingress goes to the daemon
|
|
14
|
+
* as a {@link GatewayReplyRequest}.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import type { KnownBlock } from "@slack/types";
|
|
@@ -279,6 +283,33 @@ export const ChannelReplyPayloadSchema = z.object({
|
|
|
279
283
|
|
|
280
284
|
export type ChannelReplyPayload = z.infer<typeof ChannelReplyPayloadSchema>;
|
|
281
285
|
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Gateway reply: gateway to daemon
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Daemon IPC method that delivers a {@link GatewayReplyRequest}. IPC-only:
|
|
292
|
+
* it has no HTTP route.
|
|
293
|
+
*/
|
|
294
|
+
export const DELIVER_GATEWAY_REPLY_IPC_METHOD = "deliver_gateway_reply";
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A text reply the gateway composed for an inbound message it answered
|
|
298
|
+
* itself (a verification code, an invite redemption), which the daemon
|
|
299
|
+
* delivers through the channel transport `callbackUrl` names. The callback
|
|
300
|
+
* URL is the one the inbound message carried, so the reply lands in the chat
|
|
301
|
+
* and thread the person wrote from.
|
|
302
|
+
*/
|
|
303
|
+
export const GatewayReplyRequestSchema = ChannelReplyPayloadSchema.pick({
|
|
304
|
+
chatId: true,
|
|
305
|
+
assistantId: true,
|
|
306
|
+
}).extend({
|
|
307
|
+
callbackUrl: z.string().min(1),
|
|
308
|
+
text: z.string().min(1),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
export type GatewayReplyRequest = z.infer<typeof GatewayReplyRequestSchema>;
|
|
312
|
+
|
|
282
313
|
// ---------------------------------------------------------------------------
|
|
283
314
|
// Channel delivery result — gateway response
|
|
284
315
|
// ---------------------------------------------------------------------------
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The gateway resolves this verdict from its ACL DB and stamps it onto the
|
|
5
5
|
* inbound payload's `sourceMetadata`; the runtime consumes it. Keeping the
|
|
6
|
-
* type here avoids the runtime importing from `gateway/src
|
|
7
|
-
*
|
|
8
|
-
* one source of truth.
|
|
6
|
+
* type here avoids the runtime importing from `gateway/src`, and the daemon
|
|
7
|
+
* imports {@link TrustClass} from here rather than declaring its own.
|
|
9
8
|
*
|
|
10
9
|
* This contract carries ACL + identity keys + minimal labels only — never
|
|
11
10
|
* info fields (notes, userFile, contactType). `status` / `policy` / `type`
|
|
@@ -19,9 +18,9 @@ import { z } from "zod";
|
|
|
19
18
|
import { AdmissionPolicySchema } from "./admission-policy-contract.js";
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Trust classification of an inbound actor, ordered most- to least-trusted.
|
|
22
|
+
* The one declaration of the vocabulary: the gateway classifies with it and
|
|
23
|
+
* the daemon derives capabilities from it.
|
|
25
24
|
*/
|
|
26
25
|
export const TRUST_CLASS_VALUES = [
|
|
27
26
|
"guardian",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"./reactions": "./src/reactions.ts",
|
|
28
28
|
"./guardian-requests": "./src/guardian-requests.ts",
|
|
29
29
|
"./platform-credential": "./src/platform-credential.ts",
|
|
30
|
-
"./plan-credit": "./src/plan-credit.ts"
|
|
30
|
+
"./plan-credit": "./src/plan-credit.ts",
|
|
31
|
+
"./integration-categories": "./src/integration-categories.ts"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -29,6 +29,7 @@ export * from "./trust-rules.js";
|
|
|
29
29
|
export * from "./ingress.js";
|
|
30
30
|
export * from "./no-response.js";
|
|
31
31
|
export * from "./plan-credit.js";
|
|
32
|
+
export * from "./integration-categories.js";
|
|
32
33
|
export * from "./platform-credential.js";
|
|
33
34
|
export * from "./remote-web-pairing.js";
|
|
34
35
|
export * from "./twilio-ingress.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category vocabulary for the integrations catalog, shared by the assistant
|
|
3
|
+
* daemon (which stamps it on OAuth providers and marketplace MCP entries) and
|
|
4
|
+
* the clients (which draw the category filter from it).
|
|
5
|
+
*
|
|
6
|
+
* The order here is the order clients show the categories in. The slug is the
|
|
7
|
+
* wire value; the user-facing label is the client's, so it can be translated.
|
|
8
|
+
*/
|
|
9
|
+
export const INTEGRATION_CATEGORIES = [
|
|
10
|
+
"productivity",
|
|
11
|
+
"communication",
|
|
12
|
+
"meetings",
|
|
13
|
+
"sales",
|
|
14
|
+
"marketing",
|
|
15
|
+
"finance",
|
|
16
|
+
"commerce",
|
|
17
|
+
"engineering",
|
|
18
|
+
"knowledge",
|
|
19
|
+
"recruiting",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type IntegrationCategory = (typeof INTEGRATION_CATEGORIES)[number];
|
|
23
|
+
|
|
24
|
+
const INTEGRATION_CATEGORY_SET: ReadonlySet<string> = new Set(
|
|
25
|
+
INTEGRATION_CATEGORIES,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether `value` names a category this build knows. A client reading a newer
|
|
30
|
+
* catalog treats an unknown slug as uncategorized rather than failing.
|
|
31
|
+
*/
|
|
32
|
+
export function isIntegrationCategory(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): value is IntegrationCategory {
|
|
35
|
+
return typeof value === "string" && INTEGRATION_CATEGORY_SET.has(value);
|
|
36
|
+
}
|