@vellumai/vellum-gateway 0.10.8 → 0.10.9-dev.202607152225.ec6fdbf
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 +23 -1
- package/ARCHITECTURE.md +95 -135
- package/README.md +9 -4
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +575 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +687 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +82 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/package.json +3 -3
- package/src/__tests__/contact-rich-read.test.ts +2 -2
- package/src/__tests__/contacts-control-plane-proxy.test.ts +260 -6
- package/src/__tests__/contacts-scoped-route-fallthrough.test.ts +122 -0
- package/src/__tests__/credential-requests.test.ts +83 -12
- package/src/__tests__/data-migration-m0008-upsert-acl-columns-from-assistant.test.ts +96 -23
- package/src/__tests__/data-migration-m0015-guardian-requests-backfill.test.ts +583 -0
- package/src/__tests__/data-migration-m0016-drop-assistant-guardian-tables.test.ts +557 -0
- package/src/__tests__/data-migration-m0017-coerce-escalate-policy.test.ts +180 -0
- package/src/__tests__/guardian-request-decide.test.ts +534 -0
- package/src/__tests__/helpers/contact-route-table.ts +48 -0
- package/src/__tests__/invite-route-auth.test.ts +14 -37
- package/src/__tests__/ipc-contact-routes.test.ts +227 -3
- package/src/__tests__/live-voice-websocket.test.ts +179 -48
- package/src/__tests__/m0006-reconcile-contacts.test.ts +90 -20
- package/src/__tests__/route-schema-guard.test.ts +30 -1
- package/src/__tests__/runtime-client.test.ts +63 -1
- package/src/__tests__/speech-relay-websocket.test.ts +649 -0
- package/src/__tests__/twilio-webhooks.test.ts +21 -1
- package/src/approvals/guardian-request-service.ts +509 -0
- package/src/db/__tests__/guardian-request-store.test.ts +844 -0
- package/src/db/contact-store.ts +112 -54
- package/src/db/data-migrations/assistant-contact-acl-columns.ts +17 -0
- package/src/db/data-migrations/index.ts +7 -0
- package/src/db/data-migrations/m0006-reconcile-contacts-from-assistant.ts +32 -1
- package/src/db/data-migrations/m0008-upsert-acl-columns-from-assistant.ts +35 -2
- package/src/db/data-migrations/m0015-guardian-requests-backfill.ts +261 -0
- package/src/db/data-migrations/m0016-drop-assistant-guardian-tables.ts +233 -0
- package/src/db/data-migrations/m0017-coerce-escalate-policy.ts +61 -0
- package/src/db/guardian-request-store.ts +896 -0
- package/src/db/schema.ts +97 -0
- package/src/feature-flag-registry.json +15 -63
- package/src/http/routes/contacts-control-plane-proxy.ts +150 -24
- package/src/http/routes/contacts-control-plane-route-table.ts +126 -0
- package/src/http/routes/contacts-control-plane-routes.ts +6 -3
- package/src/http/routes/live-voice-websocket.ts +65 -5
- package/src/http/routes/speech-relay-websocket.ts +481 -0
- package/src/index.ts +103 -128
- package/src/ipc/__tests__/guardian-request-routes.test.ts +765 -0
- package/src/ipc/assistant-client.test.ts +42 -0
- package/src/ipc/assistant-client.ts +13 -11
- package/src/ipc/contact-handlers.ts +13 -1
- package/src/ipc/credential-request-handlers.ts +50 -7
- package/src/ipc/guardian-request-handlers.ts +248 -0
- package/src/risk/command-registry/commands/assistant.ts +5 -2
- package/src/runtime/client.ts +53 -0
- package/src/velay/client.test.ts +53 -1
- package/src/velay/client.ts +61 -3
- package/src/verification/contact-helpers.ts +242 -190
- package/src/__tests__/contacts-control-plane-route-match.test.ts +0 -107
- package/src/http/routes/contacts-control-plane-route-match.ts +0 -83
package/AGENTS.md
CHANGED
|
@@ -29,6 +29,19 @@ All assistant API requests from clients, CLI, skills, and user-facing tooling **
|
|
|
29
29
|
|
|
30
30
|
**SKILL.md gateway URL pattern:** For gateway control-plane writes/actions that are not exposed through a CLI read command, use `$INTERNAL_GATEWAY_BASE_URL` (injected by `bash` and `host_bash`). Do not hardcode `localhost`/ports in skill examples, and do not instruct users/agents to manually export the variable from Settings. For public ingress URLs (e.g. OAuth redirect URIs, webhook registration), use `assistant config get ingress.publicBaseUrl` or load the `public-ingress` skill — do not inject public URLs as environment variables.
|
|
31
31
|
|
|
32
|
+
### Control-Plane Route Shapes — Clients Emit Scoped, Boundaries Strip to Flat
|
|
33
|
+
|
|
34
|
+
Clients always **emit** assistant-scoped URLs (`/v1/assistants/{id}/...`) — multi-tenant cloud routing needs the id. A single-assistant boundary strips the prefix before the gateway routes the request:
|
|
35
|
+
|
|
36
|
+
- **Cloud/managed:** the platform's Django `RuntimeProxyView` strips `/v1/assistants/{id}/` and forwards the flat path to the gateway.
|
|
37
|
+
- **Self-hosted (macOS Electron / local / remote gateway):** `rewriteForSelfHostedIngress` (`clients/web/src/lib/api-interceptors.ts`) flattens contact-family paths (`contacts`, `contact-channels`) to the same flat shape; every other segment is forwarded scoped, verbatim.
|
|
38
|
+
|
|
39
|
+
Route-registration consequences (`gateway/src/index.ts`):
|
|
40
|
+
|
|
41
|
+
- **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.
|
|
42
|
+
- **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.
|
|
43
|
+
- **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).
|
|
44
|
+
|
|
32
45
|
### Trust Management in Docker Mode
|
|
33
46
|
|
|
34
47
|
In Docker mode, the gateway is the sole owner of trust rule storage. Trust files (`trust.json`, `actor-token-signing-key`) live on the gateway security volume (`/gateway-security`), configured via `GATEWAY_SECURITY_DIR`. No other container has access to this volume.
|
|
@@ -97,7 +110,7 @@ For exempt ids, `PUT /v1/assistants/:id/channel-admission-policy/:channelType` r
|
|
|
97
110
|
- They are **not** exempt — the runtime still evaluates rank-vs-floor, so a real inbound channel like `whatsapp` keeps its admission floor.
|
|
98
111
|
- Their floor is pinned to the seed default; `seedAdmissionPolicyDefaults` **overwrites** any drifted/legacy row at startup (e.g. a stale `whatsapp = no_one`), so a stranded floor can't silently block a channel the user can no longer see or reset. The guardian is always max-rank on `vellum`, so its `guardian_only` seed default never locks them out — there is **no** `no_one` picker or 422 kill-switch path for hidden channels.
|
|
99
112
|
|
|
100
|
-
|
|
113
|
+
Both the flat (`/v1/channel-admission-policy/...`) and assistant-scoped (`/v1/assistants/:id/channel-admission-policy/...`) route variants are registered. The flat routes carry all cloud traffic — the platform strips the scoped prefix before forwarding — and must never be removed (see "Control-Plane Route Shapes" above). Admission policy is gateway-global, so the scoped variant matches and discards the id.
|
|
101
114
|
|
|
102
115
|
**Split enforcement** (locked decision):
|
|
103
116
|
|
|
@@ -153,3 +166,12 @@ The gateway classifies the actor at ingress (keyed on `actorExternalId`) and for
|
|
|
153
166
|
**Adding a capability**: add the field to `CapabilitySet` + the three class records (`GUARDIAN_CAPABILITIES`, `CONTACT_CAPABILITIES`, `UNKNOWN_CAPABILITIES`) + the `MATRIX` in `capabilities.test.ts`. **Adding a trust class**: add a member to `TrustClass` (the `Record<TrustClass, …>` tables then fail to compile until every column is filled) and a matrix row.
|
|
154
167
|
|
|
155
168
|
**Intentionally NOT capability-gated** (these are identity / admission-flow decisions, not permissions, and stay raw class checks): `calls/*` guardian-identity call routing, `inbound-message-handler` heartbeat/timezone side-effects, `surface-action-routes` drift-heal re-resolution, and `channel-retry-sweep` trust-class parsing.
|
|
169
|
+
|
|
170
|
+
## Guardian Requests (gateway-owned)
|
|
171
|
+
|
|
172
|
+
The gateway owns guardian approval requests and their delivery records: the `guardian_requests` and `guardian_request_deliveries` tables (`src/db/guardian-request-store.ts`), fronted by `src/approvals/guardian-request-service.ts` and exposed to the daemon over the `guardian_requests_*` IPC routes (`src/ipc/guardian-request-handlers.ts`; shared contract in `packages/gateway-client/src/guardian-request-contract.ts`). The daemon holds no request state — it relays everything through `assistant/src/channels/gateway-guardian-requests.ts` and keeps the daemon-domain side effects: notifications, card delivery/withdrawal, pending-interaction resume, grant minting into its `scoped_approval_grants`.
|
|
173
|
+
|
|
174
|
+
- **Decide atomicity (the core invariant).** `guardian_requests_decide` commits the status CAS (`pending → approved|denied`, first-writer-wins) and the decision's ACL outcome (`activate_member`, `seed_unverified`, `block`, `mint_outbound_session`) in ONE gateway transaction. A failed outcome rolls back the CAS — the request stays `pending` and the guardian can decide again; an `approved` request without its ACL write cannot exist, and deciding twice never applies two outcomes. There is no reopen path.
|
|
175
|
+
- **Schema notes.** There is no `source_type` column — the wire DTO's `sourceType` is derived from `source_channel` (`phone` → voice, `vellum` → desktop, else channel), and the list route translates a `sourceType` filter into `source_channel` predicates. The requester-side conversation is `source_conversation_id`. Request ids are caller-supplied and load-bearing (deterministic access-request ids; `tool_approval` rows reuse the pending-interaction id as PK).
|
|
176
|
+
- **Expiry is daemon-keyed for interaction-bound kinds.** `tool_approval`/`pending_question` requests die with the daemon's in-memory `pendingInteractions` map, so the daemon calls `guardian_requests_expire_interaction_bound` at boot; the gateway never expires them on its own restart. Persistent kinds (`access_request`, `tool_grant_request`) expire past `expires_at` via the daemon's 60s `guardian_requests_sweep_expired` sweep, which returns the expired rows for daemon-side card withdrawal and requester notices.
|
|
177
|
+
- **Provenance.** The tables moved here from the assistant DB: gateway data migration m0015 backfilled them, and m0016 (checkpoint-gated on m0015, with a final catch-up copy) dropped the assistant-side tables.
|
package/ARCHITECTURE.md
CHANGED
|
@@ -44,11 +44,11 @@ The request carries base64-encoded WAV audio and a MIME type. The daemon resolve
|
|
|
44
44
|
|
|
45
45
|
**Key source files:**
|
|
46
46
|
|
|
47
|
-
| File
|
|
48
|
-
|
|
|
49
|
-
| `gateway/src/http/routes/runtime-proxy.ts`
|
|
50
|
-
| `assistant/src/runtime/routes/stt-routes.ts`
|
|
51
|
-
| `clients/web/src/domains/chat/voice/stt-api.ts`
|
|
47
|
+
| File | Purpose |
|
|
48
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------- |
|
|
49
|
+
| `gateway/src/http/routes/runtime-proxy.ts` | Assistant-scoped path rewriting (`/v1/assistants/:id/...` → `/v1/...`) |
|
|
50
|
+
| `assistant/src/runtime/routes/stt-routes.ts` | Daemon HTTP endpoint: validates audio, resolves transcriber, returns text |
|
|
51
|
+
| `clients/web/src/domains/chat/voice/stt-api.ts` | Web client: POSTs audio to the gateway, returns a typed result |
|
|
52
52
|
|
|
53
53
|
### STT Streaming WebSocket Proxy
|
|
54
54
|
|
|
@@ -73,13 +73,13 @@ Clients open WebSocket connections through the gateway to the daemon's real-time
|
|
|
73
73
|
|
|
74
74
|
**Key source files:**
|
|
75
75
|
|
|
76
|
-
| File
|
|
77
|
-
|
|
|
78
|
-
| `gateway/src/http/routes/stt-stream-websocket.ts`
|
|
79
|
-
| `gateway/src/index.ts`
|
|
80
|
-
| `assistant/src/runtime/http-server.ts`
|
|
81
|
-
| `assistant/src/stt/stt-stream-session.ts`
|
|
82
|
-
| `clients/web/src/domains/chat/voice/dictation-stream.ts` | Web client: opens the gateway WebSocket, parses transcript events, reports failures
|
|
76
|
+
| File | Purpose |
|
|
77
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
78
|
+
| `gateway/src/http/routes/stt-stream-websocket.ts` | WebSocket upgrade handler (`createSttStreamWebsocketHandler`) and proxy handlers (`getSttStreamWebsocketHandlers`) |
|
|
79
|
+
| `gateway/src/index.ts` | Route registration: wires upgrade handler to the gateway's Bun HTTP server |
|
|
80
|
+
| `assistant/src/runtime/http-server.ts` | Daemon-side WebSocket upgrade at `/v1/stt/stream`, session creation and registry |
|
|
81
|
+
| `assistant/src/stt/stt-stream-session.ts` | Runtime session orchestrator: drives the `StreamingTranscriber` from the WebSocket |
|
|
82
|
+
| `clients/web/src/domains/chat/voice/dictation-stream.ts` | Web client: opens the gateway WebSocket, parses transcript events, reports failures |
|
|
83
83
|
|
|
84
84
|
### Assistant Feature Flags API
|
|
85
85
|
|
|
@@ -174,7 +174,9 @@ Runtime health is exposed directly by the gateway at `GET /v1/health` and forwar
|
|
|
174
174
|
|
|
175
175
|
### Telegram + Contacts Control-Plane Proxies
|
|
176
176
|
|
|
177
|
-
Telegram integration setup/config endpoints and contacts/invites endpoints are also exposed directly by the gateway for dedicated auth handling. Contact endpoints
|
|
177
|
+
Telegram integration setup/config endpoints and contacts/invites endpoints are also exposed directly by the gateway for dedicated auth handling. Contact endpoints are gateway-native — the gateway DB is the ACL source of truth, with a best-effort identity/info mirror to the assistant DB; search-filtered list reads relay through the runtime (which owns filter/search + info) and get an authoritative gateway ACL overlay. Invite endpoints are gateway-native — they run against the gateway DB's `ingress_invites` table with no runtime round-trip (the outbound-call relay is the one exception: the gateway validates its invite row, then delegates the provider call to the assistant).
|
|
178
|
+
|
|
179
|
+
All contact paths are registered flat only (`/v1/contacts...`, `/v1/contact-channels...`); the gateway registers no assistant-scoped contact variants. Clients emit assistant-scoped URLs (`/v1/assistants/{id}/...`), and the deployment boundary strips the prefix before the gateway routes the request: cloud's Django `RuntimeProxyView` strips it for every route, and the self-hosted web client's `rewriteForSelfHostedIngress` (`clients/web/src/lib/api-interceptors.ts`) flattens the contact family the same way.
|
|
178
180
|
|
|
179
181
|
**Forwarded Telegram endpoints:**
|
|
180
182
|
|
|
@@ -184,14 +186,17 @@ Telegram integration setup/config endpoints and contacts/invites endpoints are a
|
|
|
184
186
|
| POST | `/v1/integrations/telegram/commands` |
|
|
185
187
|
| POST | `/v1/integrations/telegram/setup` |
|
|
186
188
|
|
|
187
|
-
**
|
|
189
|
+
**Contact endpoints (gateway-native):**
|
|
188
190
|
|
|
189
|
-
| Method
|
|
190
|
-
|
|
|
191
|
-
| GET/POST
|
|
192
|
-
| GET
|
|
193
|
-
| POST
|
|
194
|
-
|
|
|
191
|
+
| Method | Path |
|
|
192
|
+
| ---------- | ----------------------------------------------- |
|
|
193
|
+
| GET/POST | `/v1/contacts` |
|
|
194
|
+
| GET/DELETE | `/v1/contacts/:contactId` |
|
|
195
|
+
| POST | `/v1/contacts/merge` |
|
|
196
|
+
| POST | `/v1/contacts/prompt/submit` |
|
|
197
|
+
| POST | `/v1/contacts/guardian/channel` |
|
|
198
|
+
| PATCH | `/v1/contact-channels/:contactChannelId` |
|
|
199
|
+
| POST | `/v1/contact-channels/:contactChannelId/verify` |
|
|
195
200
|
|
|
196
201
|
**Gateway-native invite endpoints:**
|
|
197
202
|
|
|
@@ -205,16 +210,16 @@ Telegram integration setup/config endpoints and contacts/invites endpoints are a
|
|
|
205
210
|
**Authentication boundary:**
|
|
206
211
|
|
|
207
212
|
- Gateway validates the caller's JWT bearer token.
|
|
208
|
-
-
|
|
213
|
+
- Native contact/invite endpoints are served from the gateway DB after that bearer-auth check; runtime-relayed requests (Telegram control plane, contact search reads) reach the runtime with a minted JWT (`gateway_ingress_v1` or `gateway_service_v1` scope profile).
|
|
209
214
|
- Upstream 4xx/5xx responses are passed through, while connection errors return `502` and timeouts return `504`.
|
|
210
215
|
|
|
211
216
|
**Key source files:**
|
|
212
217
|
|
|
213
|
-
| File | Purpose
|
|
214
|
-
| --------------------------------------------------------- |
|
|
215
|
-
| `gateway/src/http/routes/telegram-control-plane-proxy.ts` | Telegram control-plane proxy handlers and upstream forwarding
|
|
216
|
-
| `gateway/src/http/routes/contacts-control-plane-proxy.ts` |
|
|
217
|
-
| `gateway/src/index.ts` | Route registration and bearer-auth enforcement for `/v1/integrations/telegram/*` and `/v1/contacts
|
|
218
|
+
| File | Purpose |
|
|
219
|
+
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
220
|
+
| `gateway/src/http/routes/telegram-control-plane-proxy.ts` | Telegram control-plane proxy handlers and upstream forwarding |
|
|
221
|
+
| `gateway/src/http/routes/contacts-control-plane-proxy.ts` | Gateway-native contact handlers (with runtime search relay) plus gateway-native invite handlers (mint, list, revoke, redeem, call relay) |
|
|
222
|
+
| `gateway/src/index.ts` | Route registration and bearer-auth enforcement for `/v1/integrations/telegram/*` and `/v1/contacts*` |
|
|
218
223
|
|
|
219
224
|
### Twilio Control-Plane Proxy
|
|
220
225
|
|
|
@@ -400,9 +405,9 @@ Runtime detects needs_confirmation
|
|
|
400
405
|
|
|
401
406
|
**Conversational approval turn:** When a text message arrives while an approval is pending (e.g., non-Telegram channels or user typing a reply instead of clicking a button), a **conversational approval turn** is run via `runApprovalConversationTurn()` from `approval-conversation-turn.ts`. The conversational engine uses LLM structured output (native `tool_use`) to classify user intent as: `keep_pending` (reply without deciding), `approve_once`, `approve_always`, or `reject`. Non-decision messages receive a natural assistant reply and the run stays pending — no reminder spam. The engine fails closed: any model failure returns `keep_pending` with a deterministic fallback asking the user to try again. Callback/button handling remains deterministic and unchanged. The `channelSupportsRichApprovalUI()` function determines whether to send the structured `promptText` (for rich channels like Telegram) or the `plainTextFallback` string (for all other channels). Currently only `telegram` is classified as a rich channel.
|
|
402
407
|
|
|
403
|
-
**Guardian-aware routing:** When a guardian binding exists for the channel, the approval flow resolves the sender's actor role (`guardian` vs `non-guardian`). Non-guardian actors have `forcePromptSideEffects` set on the session so all side-effect tools trigger approval prompts regardless of existing allow rules. Approval prompts for non-guardian actions are routed to the guardian's delivery chat (not the requester's chat), and a `
|
|
408
|
+
**Guardian-aware routing:** When a guardian binding exists for the channel, the approval flow resolves the sender's actor role (`guardian` vs `non-guardian`). Non-guardian actors have `forcePromptSideEffects` set on the session so all side-effect tools trigger approval prompts regardless of existing allow rules. Approval prompts for non-guardian actions are routed to the guardian's delivery chat (not the requester's chat), and a guardian request (kind `tool_approval`) is recorded in the gateway-owned `guardian_requests` table via `guardian_requests_create`. When the guardian approves or denies, the decision commits through `guardian_requests_decide`, is applied to the underlying run, and the requester's chat is notified of the outcome. Guardian actors follow the standard approval flow. Guardian approval follow-ups also use the conversational engine with role-specific context; `approve_always` is downgraded to `approve_once` for guardian approvals since permanent allow-rules require guardian authority. All guardian state (bindings, challenges, guardian requests) is scoped to the `(assistantId, channel)` pair -- the `assistantId` parameter flows through `handleChannelInbound`, `validateAndConsumeVerification`, `isGuardian`, and `getGuardianBinding`.
|
|
404
409
|
|
|
405
|
-
**Proactive expiry sweep:** The
|
|
410
|
+
**Proactive expiry sweep:** The daemon runs a periodic sweep every 60 seconds (`assistant/src/runtime/routes/guardian-expiry-sweep.ts`) that asks the gateway to CAS-expire pending guardian requests past their `expiresAt` (`guardian_requests_sweep_expired`), then fans out card withdrawals and requester expiry notices from the returned rows. This ensures expired approvals are closed without waiting for follow-up traffic from either party.
|
|
406
411
|
|
|
407
412
|
**Gateway-origin ingress contract:** The JWT token exchanged during gateway-to-runtime authentication proves gateway origin (via the `aud=vellum-daemon` claim). No separate header is required.
|
|
408
413
|
|
|
@@ -417,7 +422,7 @@ Runtime detects needs_confirmation
|
|
|
417
422
|
| `assistant/src/runtime/routes/channel-routes.ts` | Integration point: approval interception, actor role resolution, guardian approval routing, deliver-once guard, fail-closed prompt delivery |
|
|
418
423
|
| `assistant/src/runtime/channel-verification-service.ts` | Guardian binding lookups: `isGuardian()`, `getGuardianBinding()` |
|
|
419
424
|
| `assistant/src/memory/delivery-channels.ts` | `claimRunDelivery()` — in-memory deliver-once guard for terminal reply idempotency |
|
|
420
|
-
| `assistant/src/
|
|
425
|
+
| `assistant/src/channels/gateway-guardian-requests.ts` | Typed daemon client for the gateway-owned `guardian_requests` lifecycle (`guardian_requests_create` / `_decide` / `_sweep_expired`) |
|
|
421
426
|
| `assistant/src/runtime/gateway-client.ts` | `deliverApprovalPrompt()` — sends approval payload to gateway |
|
|
422
427
|
| `gateway/src/telegram/send.ts` | `buildInlineKeyboard()` — renders approval actions as Telegram inline buttons |
|
|
423
428
|
| `gateway/src/telegram/normalize.ts` | `callback_query` normalization into `GatewayInboundEvent` (DM-only, drops callbacks without data) |
|
|
@@ -505,14 +510,8 @@ flowchart TD
|
|
|
505
510
|
|
|
506
511
|
POLICY_CHECK -- deny --> DENY_POLICY["Deny: policy_deny"]
|
|
507
512
|
POLICY_CHECK -- allow --> RECORD
|
|
508
|
-
POLICY_CHECK -- escalate --> RECORD
|
|
509
|
-
|
|
510
|
-
RECORD --> ESCALATE_CHECK{"Policy = escalate?"}
|
|
511
|
-
ESCALATE_CHECK -- Yes --> HAS_BINDING{"Guardian binding<br/>exists?"}
|
|
512
|
-
HAS_BINDING -- No --> DENY_ESCALATE["Deny: escalate_no_guardian"]
|
|
513
|
-
HAS_BINDING -- Yes --> CREATE_APPROVAL["Create approval request<br/>+ notify guardian (dual-surface)"]
|
|
514
513
|
|
|
515
|
-
|
|
514
|
+
RECORD --> VERIFY_CHECK{"Guardian verify<br/>code?"}
|
|
516
515
|
VERIFY_CHECK -- Yes --> VERIFY["Validate challenge<br/>→ create guardian binding"]
|
|
517
516
|
VERIFY_CHECK -- No --> ROLE_RESOLVE["Resolve actor role<br/>(trust-context-resolver)"]
|
|
518
517
|
ROLE_RESOLVE --> APPROVAL_INTERCEPT["Approval interception<br/>+ message processing"]
|
|
@@ -560,23 +559,23 @@ sequenceDiagram
|
|
|
560
559
|
GW-->>Guardian: Confirmation of decision
|
|
561
560
|
```
|
|
562
561
|
|
|
563
|
-
|
|
562
|
+
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.
|
|
564
563
|
|
|
565
564
|
**Key modules:**
|
|
566
565
|
|
|
567
|
-
| Module | Purpose
|
|
568
|
-
| --------------------------------------------------------- |
|
|
569
|
-
| `
|
|
570
|
-
| `assistant/src/
|
|
571
|
-
| `assistant/src/
|
|
572
|
-
| `assistant/src/runtime/
|
|
573
|
-
| `assistant/src/runtime/
|
|
574
|
-
| `assistant/src/runtime/routes/
|
|
575
|
-
| `assistant/src/
|
|
576
|
-
| `assistant/src/calls/guardian-
|
|
577
|
-
| `assistant/src/
|
|
566
|
+
| Module | Purpose |
|
|
567
|
+
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
568
|
+
| `gateway/src/db/guardian-request-store.ts` | Gateway-side store for `guardian_requests` / `guardian_request_deliveries`; first-writer-wins decision via atomic status CAS |
|
|
569
|
+
| `assistant/src/channels/gateway-guardian-requests.ts` | Daemon-side client: typed wrappers over the gateway's `guardian_requests_*` IPC routes |
|
|
570
|
+
| `assistant/src/channels/gateway-verification-sessions.ts` | Guardian binding types and verification-session relay to the gateway store |
|
|
571
|
+
| `assistant/src/runtime/channel-verification-service.ts` | Challenge creation/validation, guardian identity checks (`isGuardian()`, `getGuardianBinding()`) -- all accept `assistantId` |
|
|
572
|
+
| `assistant/src/runtime/trust-context-resolver.ts` | Actor role classification: guardian / non-guardian / unverified_channel based on binding state + sender identity |
|
|
573
|
+
| `assistant/src/runtime/routes/inbound-message-handler.ts` | Ingress ACL enforcement, verification-code intercept, actor role resolution |
|
|
574
|
+
| `assistant/src/runtime/routes/guardian-expiry-sweep.ts` | Proactive 60s expiry sweep: gateway CAS-expiry plus card-withdrawal and requester-notice fan-out |
|
|
575
|
+
| `assistant/src/calls/guardian-dispatch.ts` | Cross-channel ASK_GUARDIAN dispatch: creates gateway guardian requests, fans out to mac/telegram, records deliveries |
|
|
576
|
+
| `assistant/src/calls/guardian-action-sweep.ts` | Expiry notice delivery to guardian destinations (vellum conversation message or direct channel reply) |
|
|
578
577
|
|
|
579
|
-
### Ingress Membership
|
|
578
|
+
### Ingress Membership
|
|
580
579
|
|
|
581
580
|
The ingress membership system extends the guardian security model to support controlled cross-user access. External users interact with the assistant through channels (Telegram, WhatsApp) under an invite-based membership system with per-member access policies.
|
|
582
581
|
|
|
@@ -587,85 +586,46 @@ The channel inbound handler (`inbound-message-handler.ts`) enforces an access co
|
|
|
587
586
|
1. When `actorExternalId` is present, the handler looks up the sender in the `contacts` table via `findContactChannel` by `(channelType, externalUserId)` or `(channelType, externalChatId)`.
|
|
588
587
|
2. If no member record exists, the message is denied (`not_a_member`).
|
|
589
588
|
3. If a member exists but is not `active` (e.g., `revoked`, `blocked`), the message is denied.
|
|
590
|
-
4. If the member's `policy` is `deny`, the message is rejected. If `allow`, the message proceeds to normal processing.
|
|
589
|
+
4. If the member's `policy` is `deny`, the message is rejected. If `allow`, the message proceeds to normal processing.
|
|
591
590
|
|
|
592
591
|
**Invite-based onboarding:** Invite tokens are minted by the gateway via the invite HTTP API and stored SHA-256 hashed on the gateway DB's `ingress_invites` row -- the raw token is returned exactly once at creation time. External users redeem invites by sending the token as a channel message, which atomically creates a member record with `active` status and `allow` policy.
|
|
593
592
|
|
|
594
|
-
**Relationship to guardian verification:** Guardian verification and ingress contact management are independent systems. Guardian verification establishes who controls the assistant on a channel (the trust anchor for approvals
|
|
595
|
-
|
|
596
|
-
#### Escalation Data Flow
|
|
597
|
-
|
|
598
|
-
When a member's policy is `escalate`:
|
|
599
|
-
|
|
600
|
-
```mermaid
|
|
601
|
-
sequenceDiagram
|
|
602
|
-
participant Ext as External User
|
|
603
|
-
participant GW as Gateway
|
|
604
|
-
participant RT as Runtime (channel-routes)
|
|
605
|
-
participant DB as SQLite
|
|
606
|
-
participant Guardian as Guardian (Channel)
|
|
607
|
-
|
|
608
|
-
Ext->>GW: Send message via channel
|
|
609
|
-
GW->>RT: POST /channels/inbound
|
|
610
|
-
RT->>DB: Look up ingress member -> policy = escalate
|
|
611
|
-
RT->>DB: Store raw payload (delivery-crud)
|
|
612
|
-
RT->>DB: Create channel_guardian_approval_request
|
|
613
|
-
RT->>RT: emitNotificationSignal (escalation alert)
|
|
614
|
-
RT->>GW: Notify guardian via notification pipeline
|
|
615
|
-
GW->>Guardian: Deliver escalation notice (Telegram/desktop)
|
|
616
|
-
|
|
617
|
-
alt Guardian approves
|
|
618
|
-
Guardian->>RT: Approve decision
|
|
619
|
-
RT->>DB: Resolve approval request
|
|
620
|
-
RT->>DB: Recover stored payload
|
|
621
|
-
RT->>RT: Process message through agent pipeline
|
|
622
|
-
RT->>GW: Deliver assistant reply
|
|
623
|
-
GW->>Ext: Reply via channel
|
|
624
|
-
else Guardian denies
|
|
625
|
-
Guardian->>RT: Deny decision
|
|
626
|
-
RT->>GW: Deliver refusal message
|
|
627
|
-
GW->>Ext: Refusal via channel
|
|
628
|
-
end
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
Escalation alerts are routed through the canonical notification pipeline (`emitNotificationSignal`), which delivers to all configured channels (Telegram push, desktop notification). The guardian can approve or deny from any channel. All decisions write to the same `channel_guardian_approval_requests` table.
|
|
632
|
-
|
|
633
|
-
If no guardian binding exists for the channel, escalation fails closed -- the message is denied with `escalate_no_guardian`.
|
|
593
|
+
**Relationship to guardian verification:** Guardian verification and ingress contact management are independent systems. Guardian verification establishes who controls the assistant on a channel (the trust anchor for approvals). Ingress contacts control who can interact with the assistant.
|
|
634
594
|
|
|
635
595
|
#### SQLite Tables
|
|
636
596
|
|
|
637
|
-
**Assistant DB** (`assistant.db` —
|
|
597
|
+
**Assistant DB** (`assistant.db` — owner of contact identity/info; ACL fields are a best-effort mirror of the gateway DB):
|
|
638
598
|
|
|
639
|
-
| Table | Purpose
|
|
640
|
-
| ------------------ |
|
|
641
|
-
| `contacts` | Contact records with role, relationship, and per-contact metadata
|
|
642
|
-
| `contact_channels` | Channel bindings per contact with access policy (allow/deny
|
|
599
|
+
| Table | Purpose |
|
|
600
|
+
| ------------------ | ----------------------------------------------------------------- |
|
|
601
|
+
| `contacts` | Contact records with role, relationship, and per-contact metadata |
|
|
602
|
+
| `contact_channels` | Channel bindings per contact with access policy (allow/deny) |
|
|
643
603
|
|
|
644
|
-
**Gateway DB** (`gateway.sqlite` — canonical owner of invites
|
|
604
|
+
**Gateway DB** (`gateway.sqlite` — canonical owner of invites and contact auth/authz):
|
|
645
605
|
|
|
646
|
-
| Table | Purpose
|
|
647
|
-
| ------------------ |
|
|
648
|
-
| `contacts` | Contact auth/authz: id, display_name, role, principal_id
|
|
649
|
-
| `contact_channels` | Channel bindings with policy, status, external IDs, verification state
|
|
606
|
+
| Table | Purpose |
|
|
607
|
+
| ------------------ | ------------------------------------------------------------------------------------ |
|
|
608
|
+
| `contacts` | Contact auth/authz: id, display_name, role, principal_id |
|
|
609
|
+
| `contact_channels` | Channel bindings with policy, status, external IDs, verification state |
|
|
650
610
|
| `ingress_invites` | Canonical invite store — token/code hashes, expiry, use counts, voice/display fields |
|
|
651
611
|
|
|
652
612
|
The gateway's `ingress_invites` table is the sole invite store: mint, list, revoke, and redemption all run gateway-natively, and the daemon relays its invite surfaces here over IPC. The gateway data migrations `m0007`/`m0009` reference `assistant_ingress_invites` — a legacy assistant table absent from the current assistant schema — only as a one-time backfill source.
|
|
653
613
|
|
|
654
|
-
The gateway declares `contacts` and `contact_channels` tables and exposes them via IPC (`list_contacts`, `get_contact`, `get_contact_by_channel`, `get_channels_for_contact`).
|
|
614
|
+
The gateway declares `contacts` and `contact_channels` tables and exposes them via IPC (`list_contacts`, `get_contact`, `get_contact_by_channel`, `get_channels_for_contact`). The gateway DB is the canonical owner of contact ACL (role, principal, channel status/policy/verification): contact writes land gateway-first, with identity/info fields mirrored to the assistant DB best-effort.
|
|
655
615
|
|
|
656
616
|
#### Key Modules
|
|
657
617
|
|
|
658
|
-
| Module | Purpose
|
|
659
|
-
| --------------------------------------------------------- |
|
|
660
|
-
| `gateway/src/http/routes/contacts-control-plane-proxy.ts` | Gateway-native invite lifecycle (mint, list, revoke, redeem) shared by HTTP and IPC
|
|
661
|
-
| `gateway/src/ipc/invite-handlers.ts` | IPC routes relaying the daemon's invite surfaces to the native functions
|
|
662
|
-
| `gateway/src/verification/invite-redemption.ts` | Redemption engine — validation, atomic claim, ACL activation
|
|
663
|
-
| `assistant/src/contacts/contact-store.ts` | Contact and channel lookups (findContactChannel, guardian bindings)
|
|
618
|
+
| Module | Purpose |
|
|
619
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
620
|
+
| `gateway/src/http/routes/contacts-control-plane-proxy.ts` | Gateway-native invite lifecycle (mint, list, revoke, redeem) shared by HTTP and IPC |
|
|
621
|
+
| `gateway/src/ipc/invite-handlers.ts` | IPC routes relaying the daemon's invite surfaces to the native functions |
|
|
622
|
+
| `gateway/src/verification/invite-redemption.ts` | Redemption engine — validation, atomic claim, ACL activation |
|
|
623
|
+
| `assistant/src/contacts/contact-store.ts` | Contact and channel lookups (findContactChannel, guardian bindings) |
|
|
664
624
|
| `assistant/src/contacts/contacts-write.ts` | Contact and channel identity/info writes (upsert, redemption info mirror — ACL is gateway-owned) |
|
|
665
|
-
| `assistant/src/ipc/routes/invite-ipc-routes.ts` | `invite_redeemed` info mirror — local contact/channel identity upsert
|
|
666
|
-
| `assistant/src/runtime/routes/inbound-message-handler.ts` | ACL enforcement point -- member lookup, policy check
|
|
667
|
-
| `gateway/src/db/contact-store.ts` | Gateway-side ContactStore — contact/channel reads and invite CRUD
|
|
668
|
-
| `gateway/src/ipc/contact-handlers.ts` | IPC route handlers for contact reads
|
|
625
|
+
| `assistant/src/ipc/routes/invite-ipc-routes.ts` | `invite_redeemed` info mirror — local contact/channel identity upsert |
|
|
626
|
+
| `assistant/src/runtime/routes/inbound-message-handler.ts` | ACL enforcement point -- member lookup, policy check |
|
|
627
|
+
| `gateway/src/db/contact-store.ts` | Gateway-side ContactStore — contact/channel reads and invite CRUD |
|
|
628
|
+
| `gateway/src/ipc/contact-handlers.ts` | IPC route handlers for contact reads |
|
|
669
629
|
|
|
670
630
|
### Telegram Credential Flow
|
|
671
631
|
|
|
@@ -985,8 +945,8 @@ sequenceDiagram
|
|
|
985
945
|
| `assistant/src/calls/call-store.ts` | CRUD operations for call sessions, call events, and pending questions in SQLite via Drizzle ORM |
|
|
986
946
|
| `assistant/src/calls/call-domain.ts` | Shared domain functions (`startCall`, `getCallStatus`, `cancelCall`, `answerCall`, `relayInstruction`) used by both tools and HTTP routes |
|
|
987
947
|
| `assistant/src/calls/guardian-dispatch.ts` | Cross-channel dispatch engine: fans out ASK_GUARDIAN questions to mac/telegram, creates server-side guardian conversations, manages deliveries |
|
|
988
|
-
| `
|
|
989
|
-
| `assistant/src/calls/guardian-action-sweep.ts` |
|
|
948
|
+
| `gateway/src/db/guardian-request-store.ts` | Gateway-side store for guardian requests and deliveries; first-writer-wins resolution via atomic status CAS (`guardian_requests_decide`) |
|
|
949
|
+
| `assistant/src/calls/guardian-action-sweep.ts` | Expiry notices for expired guardian requests, sent to all delivery destinations |
|
|
990
950
|
| `assistant/src/calls/call-domain.ts:createInboundVoiceSession()` | Creates or reuses a voice session for an inbound call keyed by CallSid (idempotent replay protection) |
|
|
991
951
|
| `assistant/src/runtime/channel-verification-service.ts` | Channel verification session lifecycle: create session with six-digit code, find pending sessions, validate and consume on match |
|
|
992
952
|
| `assistant/src/calls/call-state-machine.ts` | Deterministic state transition validator with allowed-transition table and terminal-state enforcement |
|
|
@@ -1038,22 +998,22 @@ The `validateTransition(current, next)` function is called by `updateCallSession
|
|
|
1038
998
|
|
|
1039
999
|
When the LLM emits `[ASK_GUARDIAN: question]` during a voice call, the controller creates a pending question and calls `dispatchGuardianQuestion()` on the guardian dispatch engine. The dispatch engine handles the full cross-channel fan-out:
|
|
1040
1000
|
|
|
1041
|
-
1. **Request creation**: A `
|
|
1001
|
+
1. **Request creation**: A `guardian_requests` row (kind `pending_question`) is created gateway-side via `guardian_requests_create` with a unique 6-character hex request code, the question text, a `pending` status, and an expiry timestamp.
|
|
1042
1002
|
|
|
1043
1003
|
2. **Delivery fan-out via notification pipeline**: The guardian dispatch calls `emitNotificationSignal()` and uses the same notification decision + broadcaster path as every other producer.
|
|
1044
1004
|
- **Vellum**: Conversation pairing happens in the notification broadcaster. The resulting `notification_conversation_created` event surfaces the conversation in the desktop UI.
|
|
1045
1005
|
- **Telegram**: Delivery is handled by channel adapters selected by the notification decision and guarded by configured bindings.
|
|
1046
|
-
- Guardian dispatch records `
|
|
1006
|
+
- Guardian dispatch records `guardian_request_deliveries` (through the `guardian-delivery-recorder.ts` single write sink) from pipeline delivery results. It also uses the per-dispatch `onConversationCreated` callback so vellum delivery rows are created as soon as conversation pairing occurs (without waiting for slower channels).
|
|
1047
1007
|
|
|
1048
|
-
3. **Answer resolution**: The first channel to respond wins.
|
|
1008
|
+
3. **Answer resolution**: The first channel to respond wins. Decisions commit via the gateway's `guardian_requests_decide` route, which performs an atomic `WHERE status = 'pending'` compare-and-set on the `guardian_requests` table -- only the first writer succeeds in transitioning the request to a decided status. The winning answer text and deciding identity are recorded on the request row.
|
|
1049
1009
|
|
|
1050
1010
|
4. **Stale responses**: Channels that lose the race (respond after another channel has already answered) receive a "already answered" notice informing them that the question was resolved by another channel.
|
|
1051
1011
|
|
|
1052
1012
|
5. **Request-code disambiguation**: When a guardian has multiple pending requests across concurrent calls, they prefix their answer with the 6-character hex request code to indicate which question they are answering. This allows unambiguous routing even when questions arrive on the same channel in quick succession.
|
|
1053
1013
|
|
|
1054
|
-
6. **Expiry sweep**: The `guardian-
|
|
1014
|
+
6. **Expiry sweep**: The daemon's `guardian-expiry-sweep.ts` runs a periodic 60-second sweep that asks the gateway to CAS-expire pending requests past their expiry timestamp (`guardian_requests_sweep_expired`). Expiry notices are sent to all delivery destinations associated with each expired request (`guardian-action-sweep.ts`).
|
|
1055
1015
|
|
|
1056
|
-
7. **
|
|
1016
|
+
7. **One store, many kinds**: Voice questions share the gateway's `guardian_requests` store with every other guardian request, distinguished by `kind` — `tool_approval` and `tool_grant_request` rows gate specific tool invocations, `pending_question` rows carry free-form questions from voice calls that require human input to continue the conversation, and `access_request` rows gate unknown-sender access.
|
|
1057
1017
|
|
|
1058
1018
|
#### macOS Notification + Deep-Link Flow
|
|
1059
1019
|
|
|
@@ -1061,7 +1021,7 @@ When a guardian question is dispatched while the macOS app is backgrounded, the
|
|
|
1061
1021
|
|
|
1062
1022
|
### SQLite Tables
|
|
1063
1023
|
|
|
1064
|
-
|
|
1024
|
+
The three call tables live in `$VELLUM_WORKSPACE_DIR/data/db/assistant.db` alongside existing tables; guardian requests and their deliveries live in the gateway DB (the assistant-side copies were dropped by gateway migration m0016):
|
|
1065
1025
|
|
|
1066
1026
|
- **`call_sessions`** — One row per call (inbound or outbound). Tracks conversation association, provider info (Twilio CallSid), phone numbers, task description (null for inbound calls), status lifecycle (`initiated` -> `ringing` -> `in_progress` -> `waiting_on_user` -> `completed`/`failed`), and timestamps. For inbound calls, the session is keyed by CallSid via `createInboundVoiceSession()` with idempotent replay protection. Foreign key to `conversations(id)` with cascade delete.
|
|
1067
1027
|
|
|
@@ -1069,9 +1029,9 @@ All five tables live in `$VELLUM_WORKSPACE_DIR/data/db/assistant.db` alongside e
|
|
|
1069
1029
|
|
|
1070
1030
|
- **`call_pending_questions`** — Tracks questions the AI asks the user during a call (via the `[ASK_GUARDIAN: ...]` pattern). Status lifecycle: `pending` -> `answered`/`expired`/`cancelled`. Foreign key to `call_sessions(id)` with cascade delete.
|
|
1071
1031
|
|
|
1072
|
-
- **`
|
|
1032
|
+
- **`guardian_requests`** (gateway DB) — Guardian requests of every kind, including one row per ASK_GUARDIAN question from a voice call (kind `pending_question`). Tracks question/activity text, request code (6-char hex), status lifecycle (`pending` -> `approved`/`denied`/`expired`/`cancelled`), answer text, deciding identity, and expiry timestamp.
|
|
1073
1033
|
|
|
1074
|
-
- **`
|
|
1034
|
+
- **`guardian_request_deliveries`** (gateway DB) — Per-surface delivery tracking for guardian requests. One row per (request, surface) pair. Tracks delivery status, destination channel, and destination conversation/chat/message IDs — the addressing that lets a delivered card be withdrawn or matched to an inbound reaction/reply.
|
|
1075
1035
|
|
|
1076
1036
|
### Gateway Twilio Webhook Ingress
|
|
1077
1037
|
|
|
@@ -1154,20 +1114,20 @@ Malformed or unprocessable provider callback payloads are logged as dead-letter
|
|
|
1154
1114
|
|
|
1155
1115
|
Call behavior is controlled via the `calls` config block in the assistant configuration (`config/schema.ts`). All values have sensible defaults and are validated via Zod:
|
|
1156
1116
|
|
|
1157
|
-
| Field | Type | Default
|
|
1158
|
-
| --------------------------------- | -------- |
|
|
1159
|
-
| `calls.enabled` | boolean | `true`
|
|
1160
|
-
| `calls.provider` | enum | `'twilio'`
|
|
1161
|
-
| `calls.maxDurationSeconds` | int | `3600`
|
|
1162
|
-
| `calls.userConsultTimeoutSeconds` | int | `120`
|
|
1163
|
-
| `calls.disclosure.enabled` | boolean | `true`
|
|
1164
|
-
| `calls.disclosure.text` | string | _(default disclosure prompt)_
|
|
1165
|
-
| `calls.safety.denyCategories` | string[] | `[]`
|
|
1166
|
-
| `llm.callSites.callAgent.model` | string | _(unset — falls back to
|
|
1167
|
-
| `calls.voice.language` | string | `'en-US'`
|
|
1168
|
-
| `services.stt.provider` | enum | `'deepgram'`
|
|
1169
|
-
| `services.tts.provider` | enum | `'elevenlabs'`
|
|
1170
|
-
| `services.tts.providers.<id>.*` | object | _(per-provider defaults)_
|
|
1117
|
+
| Field | Type | Default | Description |
|
|
1118
|
+
| --------------------------------- | -------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1119
|
+
| `calls.enabled` | boolean | `true` | Master toggle for the calls feature. When `false`, call routes return 403 and tools return errors. |
|
|
1120
|
+
| `calls.provider` | enum | `'twilio'` | Voice provider to use (currently only Twilio is supported). |
|
|
1121
|
+
| `calls.maxDurationSeconds` | int | `3600` | Maximum allowed duration per call. |
|
|
1122
|
+
| `calls.userConsultTimeoutSeconds` | int | `120` | How long to wait for a user answer before timing out a pending question. |
|
|
1123
|
+
| `calls.disclosure.enabled` | boolean | `true` | Whether the AI should disclose it is an AI at the start of the call. |
|
|
1124
|
+
| `calls.disclosure.text` | string | _(default disclosure prompt)_ | The disclosure instruction included in the system prompt. |
|
|
1125
|
+
| `calls.safety.denyCategories` | string[] | `[]` | Categories of calls to deny (e.g., emergency numbers are always denied regardless of this setting). |
|
|
1126
|
+
| `llm.callSites.callAgent.model` | string | _(unset — falls back to the resolved call-site default)_ | Optional override for the LLM model used in voice call conversations. |
|
|
1127
|
+
| `calls.voice.language` | string | `'en-US'` | Language code for TTS and transcription. |
|
|
1128
|
+
| `services.stt.provider` | enum | `'deepgram'` | STT provider for all boundaries including telephony. The daemon transcribes media-stream call audio with this provider (streaming when supported, batch otherwise). |
|
|
1129
|
+
| `services.tts.provider` | enum | `'elevenlabs'` | Active TTS provider for speech synthesis (catalog-driven; see [TTS Provider Abstraction](../assistant/ARCHITECTURE.md#tts-provider-abstraction-servicestts)). |
|
|
1130
|
+
| `services.tts.providers.<id>.*` | object | _(per-provider defaults)_ | Provider-specific settings block. One block per catalog entry (e.g. `elevenlabs`, `fish-audio`). |
|
|
1171
1131
|
|
|
1172
1132
|
### Caller Identity Resolution
|
|
1173
1133
|
|
package/README.md
CHANGED
|
@@ -145,6 +145,8 @@ This can be sent as an action-only payload (without `text` or `attachments`) whe
|
|
|
145
145
|
|
|
146
146
|
The gateway serves as the single public ingress point for all external callbacks. The following routes are handled directly by the gateway before any proxy forwarding:
|
|
147
147
|
|
|
148
|
+
Control-plane routes are listed with their flat paths. Clients emit assistant-scoped URLs (`/v1/assistants/{id}/...`) because multi-tenant cloud routing needs the id; the deployment boundary strips the prefix before the request reaches the gateway — cloud's Django `RuntimeProxyView` strips it for every route, and the self-hosted web client's ingress rewrite (`rewriteForSelfHostedIngress` in `clients/web/src/lib/api-interceptors.ts`) flattens the contact family (`contacts`, `contact-channels`) while forwarding other segments scoped, verbatim. Contacts are therefore served flat-only, while gateway-global families that self-hosted clients still reach with scoped paths (trust-rules, channel-admission-policy, channel-permission-overrides, backups) also register assistant-scoped variants that match and discard the id. The flattening spans the whole `contacts`/`contact-channels` segment family: self-hosted invites requests land on the gateway's flat edge-scoped invite routes (the same invite engine the assistant relays to), and assistant-only subpaths (`contacts/search`, `contacts/prompt`) fall through the runtime-proxy catch-all to the assistant verbatim. Never remove a flat control-plane route — the flat family is the load-bearing cloud path.
|
|
149
|
+
|
|
148
150
|
| Route | Method | Description |
|
|
149
151
|
| ------------------------------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
150
152
|
| `/webhooks/telegram` | POST | Telegram bot webhook (validated via `TELEGRAM_WEBHOOK_SECRET`) |
|
|
@@ -161,10 +163,13 @@ The gateway serves as the single public ingress point for all external callbacks
|
|
|
161
163
|
| `/v1/integrations/telegram/config` | GET/POST/DELETE | Authenticated control-plane proxy for Telegram integration config |
|
|
162
164
|
| `/v1/integrations/telegram/commands` | POST | Authenticated control-plane proxy for Telegram command registration |
|
|
163
165
|
| `/v1/integrations/telegram/setup` | POST | Authenticated control-plane proxy for Telegram setup orchestration |
|
|
164
|
-
| `/v1/contacts` | GET/POST |
|
|
165
|
-
| `/v1/contacts/:id` | GET
|
|
166
|
-
| `/v1/contacts/merge` | POST |
|
|
167
|
-
| `/v1/
|
|
166
|
+
| `/v1/contacts` | GET/POST | Gateway-native contact list and upsert (search-filtered lists relay through the assistant runtime with a gateway ACL overlay) |
|
|
167
|
+
| `/v1/contacts/:id` | GET/DELETE | Gateway-native contact retrieval and deletion |
|
|
168
|
+
| `/v1/contacts/merge` | POST | Gateway-native transactional contact merge |
|
|
169
|
+
| `/v1/contacts/prompt/submit` | POST | Contact info submitted in response to a `contact_request` prompt — gateway-first upsert, then IPC to unblock the waiting flow |
|
|
170
|
+
| `/v1/contacts/guardian/channel` | POST | Guardian-authenticated guardian channel creation (platform auto-verify) |
|
|
171
|
+
| `/v1/contact-channels/:contactChannelId` | PATCH | Gateway-native contact-channel status/policy update |
|
|
172
|
+
| `/v1/contact-channels/:contactChannelId/verify` | POST | Guardian-authenticated manual channel verification |
|
|
168
173
|
| `/v1/contacts/invites` | GET/POST | Gateway-native invite list/create against the gateway DB's `ingress_invites` table |
|
|
169
174
|
| `/v1/contacts/invites/:id` | DELETE | Gateway-native invite revoke |
|
|
170
175
|
| `/v1/contacts/invites/:id/call` | POST | Gateway-native invite-call relay — validates the invite row, then delegates the provider call to the assistant |
|