@vellumai/vellum-gateway 0.12.2 → 0.12.3-staging.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 +7 -5
- package/ARCHITECTURE.md +34 -38
- package/README.md +30 -32
- 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/__tests__/secret-detection.test.ts +1 -0
- 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/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -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/__tests__/secret-detection.test.ts +1 -0
- 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/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- 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/__tests__/secret-detection.test.ts +1 -0
- 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/service-contracts/src/secret-detection.ts +4 -1
- 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__/telegram-normalize.test.ts +123 -111
- package/src/__tests__/telegram-webhook-drop-visibility.test.ts +229 -0
- package/src/__tests__/telegram-webhook-handler.test.ts +160 -0
- package/src/__tests__/text-verification-reply.test.ts +131 -0
- package/src/__tests__/verification-bootstrap-purpose.test.ts +126 -0
- package/src/attachments/download.ts +5 -0
- package/src/attachments/ingest.test.ts +167 -3
- package/src/attachments/ingest.ts +139 -6
- package/src/channels/admission-drop-log.test.ts +47 -0
- package/src/channels/admission-drop-log.ts +94 -0
- package/src/channels/inbound-event.ts +9 -0
- package/src/channels/room-admission.test.ts +95 -0
- package/src/channels/room-admission.ts +149 -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/discord/admission-log.test.ts +15 -12
- package/src/discord/admission-log.ts +11 -95
- package/src/discord/admit.test.ts +8 -9
- package/src/discord/admit.ts +48 -93
- package/src/discord/forward.ts +1 -1
- package/src/discord/gateway-socket.ts +4 -2
- package/src/discord/normalize.test.ts +34 -1
- package/src/discord/normalize.ts +23 -3
- package/src/email/attachments.test.ts +40 -5
- package/src/email/attachments.ts +71 -67
- package/src/email/inbound-pipeline.test.ts +1 -0
- package/src/email/inbound-pipeline.ts +1 -1
- package/src/feature-flag-registry.json +16 -0
- package/src/handlers/handle-inbound.ts +3 -0
- package/src/http/browser-auth-cookies.ts +1 -5
- package/src/http/middleware/cors.ts +4 -2
- package/src/http/routes/email-webhook.ts +1 -1
- 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 +52 -23
- 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 +2 -7
- package/src/index.ts +4 -4
- 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 +81 -1
- package/src/risk/bash-risk-classifier.ts +1 -3
- package/src/risk/command-registry/commands/assistant.ts +1 -0
- package/src/risk/command-registry/commands/dig.ts +2 -1
- package/src/risk/command-registry/commands/host.ts +2 -1
- package/src/risk/command-registry/commands/mtr.ts +2 -1
- package/src/risk/command-registry/commands/nslookup.ts +2 -1
- package/src/risk/command-registry/commands/ping.ts +2 -1
- package/src/risk/command-registry/commands/tracepath.ts +2 -1
- package/src/risk/command-registry/commands/traceroute.ts +2 -1
- package/src/risk/risk-types.ts +1 -0
- package/src/risk/shell-parser.test.ts +105 -1
- package/src/risk/shell-parser.ts +111 -4
- package/src/runtime/client.ts +0 -122
- package/src/slack/message-normalizer.ts +14 -1
- package/src/slack/source-metadata.test.ts +5 -2
- package/src/slack/source-metadata.ts +2 -6
- package/src/telegram/admit.test.ts +155 -0
- package/src/telegram/admit.ts +149 -0
- package/src/telegram/api.ts +0 -33
- package/src/telegram/bot-identity.test.ts +124 -0
- package/src/telegram/bot-identity.ts +127 -0
- package/src/telegram/drop-log.ts +41 -0
- package/src/telegram/normalize.test.ts +343 -134
- package/src/telegram/normalize.ts +197 -197
- package/src/telegram/schemas.ts +217 -0
- package/src/telegram/send.test.ts +16 -117
- package/src/telegram/send.ts +4 -158
- 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/download.ts +1 -0
- package/src/whatsapp/send.ts +1 -198
- package/src/__tests__/telegram-send-attachments.test.ts +0 -502
package/AGENTS.md
CHANGED
|
@@ -51,7 +51,7 @@ Route-registration consequences (`gateway/src/index.ts`):
|
|
|
51
51
|
|
|
52
52
|
- **Contacts** (`/v1/contacts...`, `/v1/contact-channels...`) are served on **flat routes only** — both boundaries deliver flat, so no assistant-scoped contact mirrors exist. The self-hosted flattening covers the **entire** `contacts`/`contact-channels` segment family, not just the CRUD writes: invites requests land on the gateway's flat edge-scoped invite routes (the same gateway invite engine the daemon relays to), and daemon-only subpaths (`contacts/search`, `contacts/prompt`) match no flat registration and fall through the runtime-proxy catch-all to the daemon verbatim.
|
|
53
53
|
- **Other control-plane families** (channel-admission-policy, channel-permission-overrides, trust-rules, backups) register **flat + assistant-scoped variants**: the self-hosted rewrite forwards their paths verbatim, so a scoped mirror must catch that traffic. These stores are gateway-global — the scoped variant matches and discards the id.
|
|
54
|
-
- **Invariant: never remove a flat gateway control-plane route.** Cloud's prefix strip means the flat family is the load-bearing production path even when repo-local clients appear to emit only scoped URLs. The flat channel-admission-policy routes were removed once on that mistaken theory (`2fb435314f`) and had to be restored before #35150 merged (`53fcfa7cc2` re-added the schema entries the removal took out).
|
|
54
|
+
- **Invariant: never remove a flat gateway control-plane route.** Cloud's prefix strip means the flat family is the load-bearing production path even when repo-local clients appear to emit only scoped URLs. The flat channel-admission-policy routes were removed once on that mistaken theory ([`2fb435314f`](https://github.com/vellum-ai/vellum-assistant/commit/2fb435314f)) and had to be restored before #35150 merged ([`53fcfa7cc2`](https://github.com/vellum-ai/vellum-assistant/commit/53fcfa7cc2) re-added the schema entries the removal took out); both commits are squashed out of `main`'s history, so they resolve on GitHub and not in a clone.
|
|
55
55
|
|
|
56
56
|
### Trust Management in Docker Mode
|
|
57
57
|
|
|
@@ -90,6 +90,8 @@ Gateway inbound events use a channel-discriminated union model (`GatewayInboundE
|
|
|
90
90
|
|
|
91
91
|
Trust/guardian decisions must be keyed on `actorExternalId` only — never fall back to `conversationExternalId` for actor identity.
|
|
92
92
|
|
|
93
|
+
**Room admission is one verdict, two adapter halves.** Whether the gateway acts on an inbound message at all is decided by `channels/room-admission.ts` over a neutral candidate: self and bot authors drop first, a direct chat is admitted on its own lane without a mention, a room only when the message addresses the bot, a chat kind the product does not serve is refused, and an unknown bot identity drops rather than admits. Each channel builds the candidate from its own facts (`discord/admit.ts`, `telegram/admit.ts`): how the platform proves a chat is direct (guild absence, `chat.type`) and how it proves a message is addressed (a mentions array, text entities, a reply to the bot's post). Extend the verdict, never a copy of it; a platform-only rule rides a neutral fact (Discord's legacy allow-list is `roomAllowed`). A drop is never silent: every channel logs it through `channels/admission-drop-log.ts`, one promoted line per reason and conversation, with a drop that names no conversation promoted every time. Slack's socket filter predates this and still carries the same rules inline; converging it is queued with the Slack envelope work.
|
|
94
|
+
|
|
93
95
|
Physical DB column names (`externalUserId`, `externalChatId`) are unchanged; the rename is at the API/type layer only.
|
|
94
96
|
|
|
95
97
|
**Provider words that are not our words.** A Discord **guild** is what Discord's
|
|
@@ -193,7 +195,7 @@ Both the flat (`/v1/channel-admission-policy/...`) and assistant-scoped (`/v1/as
|
|
|
193
195
|
|
|
194
196
|
- **Gateway kill switch** — `handle-inbound.ts` enforces the `no_one` floor before forwarding. Zero contact-table lookups, zero daemon I/O, true kill.
|
|
195
197
|
- **Runtime floor** — every other policy flows through the gateway unchanged; the runtime evaluates rank-vs-floor inside `admission-policy.ts`. This keeps the canonical gateway classifier (`gateway/src/risk/trust-verdict-resolver.ts`) as the single source of `TrustClass` truth (no fork): the runtime consumes the stamped verdict; the daemon's `actor-trust-resolver.ts` is only a residual sync guardian-or-unknown view for the vellum reset-drift path.
|
|
196
|
-
- **Gateway vs runtime reciprocity
|
|
198
|
+
- **Gateway vs runtime reciprocity**: this section records _which channels the gateway enforces_; the runtime's evaluation lives in `assistant/src/runtime/routes/inbound-stages/admission-policy.ts`, which reads `TRUST_CLASS_RANK` and `ADMISSION_FLOOR` from the shared contract. Either side getting out of sync is a bug, not an over-defended boundary.
|
|
197
199
|
|
|
198
200
|
**Adding a new policy**: extend the `AdmissionPolicy` union in `packages/gateway-client/src/admission-policy-contract.ts`, add its floor in `ADMISSION_FLOOR`, update the openapi schema, and update `gateway/src/__tests__/channel-admission-policy-routes.test.ts` + `assistant/src/runtime/routes/inbound-stages/admission-policy.test.ts`. Do not add a 6th floor without also bumping the `TRUST_CLASS_RANK` ceiling to match.
|
|
199
201
|
|
|
@@ -208,9 +210,9 @@ The gateway owns channel-permission matrix storage (`gateway/src/db/channel-perm
|
|
|
208
210
|
- **Cascade, least → most specific:** `workspace` → `adapter` → `channel_type` (`dm | private | public`) → `channel` (external channel ID). `ChannelPermissionStore.resolve()` walks most-specific-first and returns the first cell set for the contact-type.
|
|
209
211
|
- **Vocabulary contract:** `packages/gateway-client/src/channel-permission-contract.ts` (selectors, thresholds, scopes, resolve request). The contact-type axis is the canonical `TrustClass` — granularity intentionally stops there; do not add per-individual-contact cells.
|
|
210
212
|
- **IPC surface:** `list_channel_permission_overrides`, `set_channel_permission_override`, `delete_channel_permission_override`, `resolve_channel_permission_threshold`. Writes validate the adapter against the gateway channel registry.
|
|
211
|
-
- **HTTP surface (configuration clients):** `GET`/`PUT /v1/channel-permission-overrides` + `POST /v1/channel-permission-overrides/delete` + `POST /v1/channel-permission-overrides/resolve` (`gateway/src/http/routes/channel-permission-overrides.ts`), published in the gateway OpenAPI spec for the web SDK. Mirrors the IPC list/set/delete/resolve with the same contract schemas and adapter validation. Resolve over HTTP is read-only (`settings.read`) and exists so configuration clients can display the effective fall-through without re-implementing the cascade walk client-side (which drifts
|
|
213
|
+
- **HTTP surface (configuration clients):** `GET`/`PUT /v1/channel-permission-overrides` + `POST /v1/channel-permission-overrides/delete` + `POST /v1/channel-permission-overrides/resolve` (`gateway/src/http/routes/channel-permission-overrides.ts`), published in the gateway OpenAPI spec for the web SDK. Mirrors the IPC list/set/delete/resolve with the same contract schemas and adapter validation. Resolve over HTTP is read-only (`settings.read`) and exists so configuration clients can display the effective fall-through without re-implementing the cascade walk client-side (which drifts as the cascade evolves); the runtime evaluator keeps using the IPC resolve. Flat + assistant-scoped variants, `settings.read`/`settings.write` scopes: same shape as channel-admission-policy.
|
|
212
214
|
- **Migration provenance:** `m0012-migrate-slack-channel-permissions` lifts Slack-skill `channelPermissions` profiles with `trustLevel: "restricted"` into channel-scoped Strict cells (non-guardian contact-types). Per-tool fields (`blockedTools` / `allowedToolCategories`) have no matrix representation — they stay in the Slack skill config, enforced by the legacy deterministic channel gate in `assistant/src/tools/tool-approval-handler.ts`.
|
|
213
|
-
- **Runtime consumption (per-tool-call evaluation):** the assistant's permission checker (`assistant/src/permissions/checker.ts`) builds a resolve query from the turn's `PolicyContext` (adapter = source channel, conversation type, external channel ID, contact-type = trust class) and threads it into the threshold cascade in `assistant/src/permissions/gateway-threshold-reader.ts`. The cell sits between the per-conversation override (most specific) and the global defaults; the winning threshold feeds `DefaultApprovalPolicy.evaluate` as `autoApproveUpTo`, composing cell RiskThreshold × tool RiskLevel with the untouched capability floor (`resolveSensitiveToolDecision`). Fail-safe semantics: a cell transport failure falls through to global on the cached read, but the pre-prompt refresh keeps its prompt (returns null) rather than falling through to a possibly-looser global. Slack
|
|
215
|
+
- **Runtime consumption (per-tool-call evaluation):** the assistant's permission checker (`assistant/src/permissions/checker.ts`) builds a resolve query from the turn's `PolicyContext` (adapter = source channel, conversation type, external channel ID, contact-type = trust class) and threads it into the threshold cascade in `assistant/src/permissions/gateway-threshold-reader.ts`. The cell sits between the per-conversation override (most specific) and the global defaults; the winning threshold feeds `DefaultApprovalPolicy.evaluate` as `autoApproveUpTo`, composing cell RiskThreshold × tool RiskLevel with the untouched capability floor (`resolveSensitiveToolDecision`). Fail-safe semantics: a cell transport failure falls through to global on the cached read, but the pre-prompt refresh keeps its prompt (returns null) rather than falling through to a possibly-looser global. Slack states `conversationType` for every room it can classify (`slackConversationVisibility` in `gateway/src/slack/message-normalizer.ts`: a public channel is `public`, a private channel or group DM is `private`, a DM is `dm`), so the `channel_type` tier matches Slack rooms as well as DMs.
|
|
214
216
|
|
|
215
217
|
### Trust Classes → Capabilities (what an actor may do)
|
|
216
218
|
|
|
@@ -219,7 +221,7 @@ Two orthogonal axes, do not conflate them:
|
|
|
219
221
|
- **Admission** (above) — _who gets in the door_. `TRUST_CLASS_RANK` vs `ADMISSION_FLOOR`, enforced across gateway + runtime.
|
|
220
222
|
- **Capabilities** — _what an actor may do once admitted_. Resolved in the runtime, never on the gateway.
|
|
221
223
|
|
|
222
|
-
**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`:
|
|
223
225
|
|
|
224
226
|
| Class | Rank | Meaning |
|
|
225
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.
|
|
@@ -106,15 +93,15 @@ The gateway normalizes Telegram `callback_query` updates (inline button clicks)
|
|
|
106
93
|
|
|
107
94
|
These fields are forwarded to the runtime in the `/channels/inbound` payload alongside the standard `conversationExternalId`, `externalMessageId`, and actor metadata. The runtime uses `callbackData` to route the click to the appropriate approval handler.
|
|
108
95
|
|
|
109
|
-
**Normalization constraints:**
|
|
96
|
+
**Normalization constraints:** Callback queries from private chats, groups, and supergroups are processed; a tap on a keyboard the bot posted is addressed to the bot by construction, and who may press it is the runtime's decision. Callbacks from broadcast channels are dropped and acknowledged with `answerCallbackQuery` so the Telegram button spinner clears. Callback queries with no `data` field or no associated `message` are also dropped. Every drop is logged with its reason before the update is acknowledged (`telegram/drop-log.ts`).
|
|
110
97
|
|
|
111
98
|
**Stale callback blocking:** When the runtime receives `callbackData` that does not match any pending approval (e.g., a button from an old prompt), it returns `stale_ignored` and does not process the payload as a regular message. This is enforced regardless of whether the callback has non-empty content. The gateway sends a best-effort `answerCallbackQuery` acknowledgment for normalized callback updates (including stale, rejected, and forward-failure paths) so the button spinner clears promptly. Transient forwarding failures may still return `500` so Telegram retries update delivery.
|
|
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",
|
|
@@ -63,6 +63,7 @@ const FIXTURES: Record<string, string> = {
|
|
|
63
63
|
"Perplexity API Key": `pplx-${filler(40)}`,
|
|
64
64
|
"Tavily API Key": `tvly-${filler(20)}`,
|
|
65
65
|
"Firecrawl API Key": `fc-${filler(20)}`,
|
|
66
|
+
"Resend API Key": `re_${filler(8)}_${filler(24)}`,
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
describe("subpath export", () => {
|
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
|
+
}
|
|
@@ -230,6 +230,9 @@ export const PREFIX_PATTERNS: SecretPrefixPattern[] = [
|
|
|
230
230
|
|
|
231
231
|
// -- Firecrawl --
|
|
232
232
|
{ label: "Firecrawl API Key", regex: /fc-[A-Za-z0-9]{20,}/ },
|
|
233
|
+
// Resend documents only the `re_` prefix; the segment shape is the observed
|
|
234
|
+
// key format. Strict on purpose: bare `re_` prefixes ordinary identifiers.
|
|
235
|
+
{ label: "Resend API Key", regex: /re_[A-Za-z0-9]{8}_[A-Za-z0-9]{24,}/ },
|
|
233
236
|
];
|
|
234
237
|
|
|
235
238
|
/**
|
|
@@ -362,7 +365,7 @@ export function isPlaceholderValue(value: string): boolean {
|
|
|
362
365
|
// Strip known prefixes to isolate the variable part
|
|
363
366
|
const variablePart = value
|
|
364
367
|
.replace(
|
|
365
|
-
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx
|
|
368
|
+
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx-|re_|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
|
|
366
369
|
"",
|
|
367
370
|
)
|
|
368
371
|
.replace(/[^A-Za-z0-9]/g, "");
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildShellInvocation,
|
|
5
|
+
buildShellSpawnFlags,
|
|
5
6
|
pathListDelimiter,
|
|
6
7
|
prependUniquePathEntries,
|
|
7
8
|
} from "./shell.js";
|
|
@@ -38,6 +39,26 @@ describe("buildShellInvocation", () => {
|
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
describe("buildShellSpawnFlags", () => {
|
|
43
|
+
test("creates a POSIX process group and hides Windows consoles", () => {
|
|
44
|
+
expect(buildShellSpawnFlags("linux")).toEqual({
|
|
45
|
+
detached: true,
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
});
|
|
48
|
+
expect(buildShellSpawnFlags("darwin")).toEqual({
|
|
49
|
+
detached: true,
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("does not detach Windows children that use piped stdio", () => {
|
|
55
|
+
expect(buildShellSpawnFlags("win32")).toEqual({
|
|
56
|
+
detached: false,
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
41
62
|
describe("path list handling", () => {
|
|
42
63
|
test("uses the platform delimiter", () => {
|
|
43
64
|
expect(pathListDelimiter("win32")).toBe(";");
|
|
@@ -5,6 +5,11 @@ export interface ShellInvocation {
|
|
|
5
5
|
args: string[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface ShellSpawnFlags {
|
|
9
|
+
detached: boolean;
|
|
10
|
+
windowsHide: true;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
const WINDOWS_UTF8_PREAMBLE =
|
|
9
14
|
"try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}; " +
|
|
10
15
|
"$OutputEncoding = [System.Text.Encoding]::UTF8; " +
|
|
@@ -43,6 +48,25 @@ export function buildShellInvocation(
|
|
|
43
48
|
return { command: "bash", args: ["-c", "--", command] };
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Spawn flags for assistant-owned shell children (sandbox bash, local
|
|
53
|
+
* host_bash fallback, sanitized CLI bash, skill runners, scheduled scripts).
|
|
54
|
+
*
|
|
55
|
+
* POSIX uses a new process group so timeout/abort can SIGKILL the tree via
|
|
56
|
+
* `-pid`. Windows process trees are torn down with `taskkill /T`, which does
|
|
57
|
+
* not need a detached process. Combining `DETACHED_PROCESS`,
|
|
58
|
+
* `CREATE_NO_WINDOW`, and piped stdio on Windows can emit `close` with exit
|
|
59
|
+
* 0 and empty pipes without running the encoded command.
|
|
60
|
+
*/
|
|
61
|
+
export function buildShellSpawnFlags(
|
|
62
|
+
hostPlatform: NodeJS.Platform = process.platform,
|
|
63
|
+
): ShellSpawnFlags {
|
|
64
|
+
return {
|
|
65
|
+
detached: hostPlatform !== "win32",
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
export function pathListDelimiter(
|
|
47
71
|
hostPlatform: NodeJS.Platform = process.platform,
|
|
48
72
|
): string {
|
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",
|
|
@@ -63,6 +63,7 @@ const FIXTURES: Record<string, string> = {
|
|
|
63
63
|
"Perplexity API Key": `pplx-${filler(40)}`,
|
|
64
64
|
"Tavily API Key": `tvly-${filler(20)}`,
|
|
65
65
|
"Firecrawl API Key": `fc-${filler(20)}`,
|
|
66
|
+
"Resend API Key": `re_${filler(8)}_${filler(24)}`,
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
describe("subpath export", () => {
|
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
|
+
}
|
|
@@ -230,6 +230,9 @@ export const PREFIX_PATTERNS: SecretPrefixPattern[] = [
|
|
|
230
230
|
|
|
231
231
|
// -- Firecrawl --
|
|
232
232
|
{ label: "Firecrawl API Key", regex: /fc-[A-Za-z0-9]{20,}/ },
|
|
233
|
+
// Resend documents only the `re_` prefix; the segment shape is the observed
|
|
234
|
+
// key format. Strict on purpose: bare `re_` prefixes ordinary identifiers.
|
|
235
|
+
{ label: "Resend API Key", regex: /re_[A-Za-z0-9]{8}_[A-Za-z0-9]{24,}/ },
|
|
233
236
|
];
|
|
234
237
|
|
|
235
238
|
/**
|
|
@@ -362,7 +365,7 @@ export function isPlaceholderValue(value: string): boolean {
|
|
|
362
365
|
// Strip known prefixes to isolate the variable part
|
|
363
366
|
const variablePart = value
|
|
364
367
|
.replace(
|
|
365
|
-
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx
|
|
368
|
+
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx-|re_|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
|
|
366
369
|
"",
|
|
367
370
|
)
|
|
368
371
|
.replace(/[^A-Za-z0-9]/g, "");
|