@vellumai/assistant 0.4.5 → 0.4.7
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/ARCHITECTURE.md +27 -10
- package/README.md +6 -6
- package/bun.lock +57 -2
- package/docs/architecture/memory.md +4 -4
- package/docs/trusted-contact-access.md +8 -0
- package/package.json +3 -2
- package/src/__tests__/actor-token-service.test.ts +9 -6
- package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/call-controller.test.ts +115 -0
- package/src/__tests__/call-domain.test.ts +148 -10
- package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
- package/src/__tests__/call-pointer-messages.test.ts +105 -43
- package/src/__tests__/canonical-guardian-store.test.ts +44 -10
- package/src/__tests__/channel-approval-routes.test.ts +67 -65
- package/src/__tests__/channel-delivery-store.test.ts +2 -2
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
- package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
- package/src/__tests__/gateway-client-managed-outbound.test.ts +147 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
- package/src/__tests__/guardian-dispatch.test.ts +39 -1
- package/src/__tests__/guardian-grant-minting.test.ts +24 -24
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
- package/src/__tests__/guardian-routing-state.test.ts +10 -32
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
- package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
- package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +57 -47
- package/src/__tests__/notification-decision-fallback.test.ts +232 -0
- package/src/__tests__/notification-decision-strategy.test.ts +304 -8
- package/src/__tests__/notification-guardian-path.test.ts +38 -1
- package/src/__tests__/relay-server.test.ts +136 -5
- package/src/__tests__/send-endpoint-busy.test.ts +35 -1
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
- package/src/__tests__/system-prompt.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +1 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +10 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +14 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +24 -24
- package/src/__tests__/trusted-contact-multichannel.test.ts +5 -5
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/approvals/guardian-decision-primitive.ts +29 -25
- package/src/approvals/guardian-request-resolvers.ts +9 -5
- package/src/calls/call-controller.ts +15 -0
- package/src/calls/call-pointer-message-composer.ts +27 -85
- package/src/calls/call-pointer-messages.ts +54 -21
- package/src/calls/guardian-dispatch.ts +30 -0
- package/src/calls/relay-server.ts +58 -24
- package/src/calls/types.ts +1 -0
- package/src/config/system-prompt.ts +10 -3
- package/src/config/templates/BOOTSTRAP.md +6 -5
- package/src/config/templates/USER.md +1 -0
- package/src/config/user-reference.ts +44 -0
- package/src/daemon/handlers/guardian-actions.ts +5 -2
- package/src/daemon/handlers/sessions.ts +8 -3
- package/src/daemon/lifecycle.ts +109 -3
- package/src/daemon/providers-setup.ts +0 -8
- package/src/daemon/server.ts +32 -24
- package/src/daemon/session-agent-loop.ts +4 -3
- package/src/daemon/session-lifecycle.ts +1 -9
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-runtime-assembly.ts +2 -0
- package/src/daemon/session-slash.ts +35 -2
- package/src/daemon/session-tool-setup.ts +10 -0
- package/src/daemon/session.ts +1 -0
- package/src/memory/canonical-guardian-store.ts +40 -0
- package/src/memory/conversation-crud.ts +26 -0
- package/src/memory/conversation-store.ts +1 -0
- package/src/memory/db-init.ts +12 -0
- package/src/memory/guardian-bindings.ts +4 -0
- package/src/memory/job-handlers/backfill.ts +2 -9
- package/src/memory/migrations/039-actor-refresh-token-records.ts +51 -0
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema.ts +22 -0
- package/src/notifications/README.md +8 -1
- package/src/notifications/copy-composer.ts +160 -30
- package/src/notifications/decision-engine.ts +98 -1
- package/src/runtime/access-request-helper.ts +43 -28
- package/src/runtime/actor-refresh-token-service.ts +309 -0
- package/src/runtime/actor-refresh-token-store.ts +157 -0
- package/src/runtime/actor-token-service.ts +3 -3
- package/src/runtime/actor-trust-resolver.ts +19 -14
- package/src/runtime/channel-guardian-service.ts +6 -0
- package/src/runtime/gateway-client.ts +239 -0
- package/src/runtime/guardian-context-resolver.ts +6 -2
- package/src/runtime/guardian-reply-router.ts +33 -16
- package/src/runtime/guardian-vellum-migration.ts +29 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/http-types.ts +0 -13
- package/src/runtime/local-actor-identity.ts +19 -13
- package/src/runtime/middleware/actor-token.ts +2 -2
- package/src/runtime/routes/channel-delivery-routes.ts +5 -5
- package/src/runtime/routes/conversation-routes.ts +45 -35
- package/src/runtime/routes/guardian-action-routes.ts +7 -1
- package/src/runtime/routes/guardian-approval-interception.ts +52 -52
- package/src/runtime/routes/guardian-bootstrap-routes.ts +11 -24
- package/src/runtime/routes/guardian-refresh-routes.ts +53 -0
- package/src/runtime/routes/inbound-conversation.ts +7 -7
- package/src/runtime/routes/inbound-message-handler.ts +105 -94
- package/src/runtime/routes/pairing-routes.ts +60 -50
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/types/qrcode.d.ts +10 -0
- package/src/util/logger.ts +10 -0
- package/src/daemon/call-pointer-generators.ts +0 -59
package/ARCHITECTURE.md
CHANGED
|
@@ -22,7 +22,7 @@ This document owns assistant-runtime architecture details. The repo-level archit
|
|
|
22
22
|
- Voice calls mirror the same prompt contract: `CallController` receives guardian context on setup and refreshes it immediately after successful voice challenge verification, so the first post-verification turn is grounded as `actor_role: guardian`.
|
|
23
23
|
- Voice-specific behavior (DTMF/speech verification flow, relay state machine) remains voice-local; only actor-role resolution is shared.
|
|
24
24
|
|
|
25
|
-
### Vellum Guardian Identity Model (Actor Tokens +
|
|
25
|
+
### Vellum Guardian Identity Model (Actor Tokens + Refresh Tokens)
|
|
26
26
|
|
|
27
27
|
The vellum channel (macOS desktop, iOS, CLI) uses an identity-bound actor token system to authenticate guardian identity on HTTP routes. This replaces the previous implicit trust model where all local connections were assumed to be the guardian.
|
|
28
28
|
|
|
@@ -30,16 +30,30 @@ The vellum channel (macOS desktop, iOS, CLI) uses an identity-bound actor token
|
|
|
30
30
|
|
|
31
31
|
1. **Startup migration** — On daemon start, `ensureVellumGuardianBinding()` (in `guardian-vellum-migration.ts`) backfills a `channel='vellum'` guardian binding with a stable `guardianPrincipalId` (format: `vellum-principal-<uuid>`). Existing installations get a binding with `verifiedVia: 'startup-migration'`; new installs get one via bootstrap. This migration is idempotent and preserves bindings for other channels (Telegram, SMS, etc.).
|
|
32
32
|
|
|
33
|
-
2. **
|
|
33
|
+
2. **Bootstrap (loopback-only, macOS) — initial issuance only** — On first launch (no existing actor token), the macOS client calls `POST /v1/integrations/guardian/vellum/bootstrap` with `{ platform: 'macos', deviceId }`. The endpoint is loopback-only: it rejects requests with `X-Forwarded-For` and verifies the peer IP is a loopback address (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`). The endpoint ensures a vellum guardian principal exists, revokes any prior token for the same device binding, mints a new HMAC-SHA256 signed actor token with a 30-day TTL and a rotating refresh token, stores only the SHA-256 hashes, and returns `{ guardianPrincipalId, actorToken, actorTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter, isNew }`. Bootstrap is only used for initial credential issuance — ongoing renewal is handled exclusively by the refresh endpoint.
|
|
34
34
|
|
|
35
|
-
3. **iOS pairing** — iOS devices obtain actor tokens exclusively through the QR pairing flow
|
|
35
|
+
3. **iOS pairing — initial issuance only** — iOS devices obtain actor tokens exclusively through the QR pairing flow. When an iOS device completes pairing, the pairing response includes an `actorToken` and `refreshToken` minted against the same vellum guardian principal. The pairing handler in `pairing-routes.ts` calls `mintPairingActorToken()` which looks up the vellum binding and mints a device-specific token pair. iOS does not call the bootstrap endpoint. Re-pairing is only needed if both the actor token and refresh token expire.
|
|
36
36
|
|
|
37
37
|
4. **IPC identity** — Local IPC connections (Unix domain socket from the macOS native app) do not send actor tokens. Instead, the daemon assigns a deterministic local actor identity via `resolveLocalIpcGuardianContext()` in `local-actor-identity.ts`. This looks up the vellum guardian binding and routes through the same `resolveGuardianContext` trust pipeline used by HTTP channel ingress. When no vellum binding exists yet (pre-bootstrap), a fallback guardian context is returned since the local macOS user is inherently the guardian of their own machine.
|
|
38
38
|
|
|
39
|
-
**Actor token format:** `base64url(JSON claims) + '.' + base64url(HMAC-SHA256 signature)`. Claims include `assistantId`, `platform`, `deviceId`, `guardianPrincipalId`, `iat`, `exp` (
|
|
39
|
+
**Actor token format:** `base64url(JSON claims) + '.' + base64url(HMAC-SHA256 signature)`. Claims include `assistantId`, `platform`, `deviceId`, `guardianPrincipalId`, `iat`, `exp` (30 days from issuance), and `jti`.
|
|
40
40
|
|
|
41
41
|
**Hash-only storage:** Only the SHA-256 hex digest of the raw token is persisted in the `actor_token_records` table. Token verification recomputes the hash and looks it up in the store to check revocation status. Tokens are scoped to `(assistantId, guardianPrincipalId, hashedDeviceId)` with a one-active-per-device invariant.
|
|
42
42
|
|
|
43
|
+
**Refresh token lifecycle:**
|
|
44
|
+
|
|
45
|
+
Refresh tokens provide a rotating credential renewal mechanism that avoids re-bootstrap or re-pairing for ongoing sessions.
|
|
46
|
+
|
|
47
|
+
- **Issuance:** A refresh token is minted alongside every actor token (during bootstrap or pairing). The response includes `refreshToken`, `refreshTokenExpiresAt`, and `refreshAfter` (the timestamp at which clients should proactively refresh, set to 80% of the actor token TTL).
|
|
48
|
+
- **Dual expiry:** Each refresh token has a 365-day absolute expiry (from issuance) and a 90-day inactivity expiry (from last use). The effective expiry is the earlier of the two. Using a refresh token resets the inactivity window.
|
|
49
|
+
- **Single-use rotation:** Each call to `POST /v1/integrations/guardian/vellum/refresh` consumes the presented refresh token and returns a new actor token + new refresh token pair. The old refresh token is marked as rotated and cannot be reused.
|
|
50
|
+
- **Token family tracking:** Refresh tokens are grouped into families (one family per initial issuance chain). All tokens in a family share a `familyId`.
|
|
51
|
+
- **Replay detection:** If a client presents a refresh token that has already been rotated (i.e., it was used once and a successor was issued), the server treats this as a potential token theft. The entire token family for that device is revoked, forcing re-bootstrap or re-pairing.
|
|
52
|
+
- **Device binding:** Refresh tokens are bound to `(assistantId, guardianPrincipalId, hashedDeviceId)`. A refresh request from a different device binding is rejected.
|
|
53
|
+
- **Hash-only storage:** Only the SHA-256 hex digest of the refresh token is stored in the `actor_refresh_token_records` table. The raw token is returned once and never persisted on the server.
|
|
54
|
+
|
|
55
|
+
**Refresh endpoint:** `POST /v1/integrations/guardian/vellum/refresh` accepts `{ refreshToken }` in the request body. It validates the token hash, checks expiry and device binding, performs replay detection, rotates the token, mints a new actor token, and returns `{ actorToken, actorTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter }`. This endpoint is only reachable through the gateway (bearer-authenticated).
|
|
56
|
+
|
|
43
57
|
**Signing key management:** A 32-byte random signing key is generated on first startup and persisted at `~/.vellum/protected/actor-token-signing-key` with `chmod 0o600`. The key is loaded on subsequent startups via `loadOrCreateSigningKey()`.
|
|
44
58
|
|
|
45
59
|
**Strict HTTP enforcement:** Vellum-channel HTTP routes (POST /v1/messages, POST /v1/confirm, POST /v1/guardian-actions/decision, etc.) require a valid actor token via the `X-Actor-Token` header. The middleware in `middleware/actor-token.ts` verifies the HMAC signature, checks the token is active in the store, and resolves a guardian context through the standard trust pipeline. For backward compatibility with the CLI, requests without an actor token that originate from a loopback address (no `X-Forwarded-For` header) fall back to `resolveLocalIpcGuardianContext()`. Gateway-proxied requests (which carry `X-Forwarded-For`) without an actor token are rejected.
|
|
@@ -52,11 +66,14 @@ The vellum channel (macOS desktop, iOS, CLI) uses an identity-bound actor token
|
|
|
52
66
|
|------|---------|
|
|
53
67
|
| `src/runtime/actor-token-service.ts` | HMAC-SHA256 mint/verify, signing key management, `hashToken` |
|
|
54
68
|
| `src/runtime/actor-token-store.ts` | Hash-only persistence: create, find by hash/device binding, revoke |
|
|
69
|
+
| `src/runtime/actor-refresh-token-service.ts` | Refresh token rotation, replay detection, family revocation |
|
|
70
|
+
| `src/runtime/actor-refresh-token-store.ts` | Refresh token hash-only persistence: create, find, rotate, revoke by family |
|
|
55
71
|
| `src/runtime/middleware/actor-token.ts` | HTTP middleware: `verifyHttpActorToken`, `verifyHttpActorTokenWithLocalFallback`, `isActorBoundGuardian` |
|
|
56
72
|
| `src/runtime/local-actor-identity.ts` | `resolveLocalIpcGuardianContext` — deterministic IPC identity |
|
|
57
73
|
| `src/runtime/guardian-vellum-migration.ts` | `ensureVellumGuardianBinding` — startup binding backfill |
|
|
58
|
-
| `src/runtime/routes/guardian-bootstrap-routes.ts` | `POST /v1/integrations/guardian/vellum/bootstrap` handler |
|
|
59
|
-
| `src/runtime/routes/
|
|
74
|
+
| `src/runtime/routes/guardian-bootstrap-routes.ts` | `POST /v1/integrations/guardian/vellum/bootstrap` handler (initial issuance only) |
|
|
75
|
+
| `src/runtime/routes/guardian-refresh-routes.ts` | `POST /v1/integrations/guardian/vellum/refresh` handler (token rotation) |
|
|
76
|
+
| `src/runtime/routes/pairing-routes.ts` | `mintPairingActorToken` — actor token + refresh token in pairing response |
|
|
60
77
|
| `src/memory/guardian-bindings.ts` | Guardian binding persistence (shared across all channels) |
|
|
61
78
|
|
|
62
79
|
### Channel-Agnostic Scoped Approval Grants
|
|
@@ -238,8 +255,8 @@ The SMS channel provides text-only messaging via Twilio, sharing the same teleph
|
|
|
238
255
|
3. `MessageSid` deduplication prevents reprocessing retried webhooks.
|
|
239
256
|
4. **MMS detection**: The gateway treats a message as MMS when any of: `NumMedia > 0`, any `MediaUrl<N>` key has a non-empty value, or any `MediaContentType<N>` key has a non-empty value. This catches media attachments even when Twilio omits `NumMedia`. The gateway replies with an unsupported notice and does not forward the payload. MMS payloads are explicitly rejected rather than silently dropped.
|
|
240
257
|
5. **`/new` command**: When the message body is exactly `/new` (case-insensitive, trimmed), the gateway resolves routing first. If routing is rejected, a rejection notice SMS is sent to the sender (matching Telegram `/new` rejection semantics — "This message could not be routed to an assistant"). If routing succeeds, the gateway calls `resetConversation(...)` on the runtime and sends a confirmation SMS. The message is never forwarded to the runtime.
|
|
241
|
-
6. The payload is normalized into a `
|
|
242
|
-
7. **Routing** — Phone-number-based routing is checked first: the inbound `To` number is reverse-looked-up in `assistantPhoneNumbers` (a `Record<string, string>` mapping assistant IDs to E.164 numbers, propagated from the assistant config file). If a match is found, that assistant handles the message. Otherwise, the standard routing chain (
|
|
258
|
+
6. The payload is normalized into a `GatewayInboundEvent` with `sourceChannel: "sms"` and `conversationExternalId` set to the sender's phone number (E.164).
|
|
259
|
+
7. **Routing** — Phone-number-based routing is checked first: the inbound `To` number is reverse-looked-up in `assistantPhoneNumbers` (a `Record<string, string>` mapping assistant IDs to E.164 numbers, propagated from the assistant config file). If a match is found, that assistant handles the message. Otherwise, the standard routing chain (conversation_id -> actor_id -> default/reject) is used. This allows multiple assistants to have dedicated phone numbers. The resolved route is passed as a `routingOverride` to `handleInbound()` so the already-resolved routing is used directly instead of re-running `resolveAssistant()` inside the handler.
|
|
243
260
|
8. The event is forwarded to the runtime via `POST /channels/inbound`, including SMS-specific transport hints (`chat-first-medium`, `sms-character-limits`, etc.) and a `replyCallbackUrl` pointing to `/deliver/sms`.
|
|
244
261
|
|
|
245
262
|
**Egress** (`POST /deliver/sms`):
|
|
@@ -272,7 +289,7 @@ The WhatsApp channel enables inbound and outbound messaging via the Meta WhatsAp
|
|
|
272
289
|
2. On `POST`, the gateway verifies the `X-Hub-Signature-256` header (HMAC-SHA256 of the raw request body using `WHATSAPP_APP_SECRET`) when the app secret is configured. Fail-closed: requests are rejected when the secret is set but the signature fails.
|
|
273
290
|
3. **Normalization**: Only `type=text` messages from `messages` change fields are forwarded. Delivery receipts, read receipts, and non-text message types (image, audio, video, document, sticker) are silently acknowledged with `{ ok: true }`.
|
|
274
291
|
4. **`/new` command**: When the message body is `/new` (case-insensitive), the gateway resolves routing, resets the conversation, and sends a confirmation message without forwarding to the runtime.
|
|
275
|
-
5. The payload is normalized into a `
|
|
292
|
+
5. The payload is normalized into a `GatewayInboundEvent` with `sourceChannel: "whatsapp"` and `conversationExternalId` set to the sender's WhatsApp phone number (E.164).
|
|
276
293
|
6. WhatsApp message IDs are deduplicated via `StringDedupCache` (24-hour TTL).
|
|
277
294
|
7. The gateway marks each inbound message as read (best-effort, fire-and-forget).
|
|
278
295
|
8. The event is forwarded to the runtime via `POST /channels/inbound` with WhatsApp-specific transport hints and a `replyCallbackUrl` pointing to `/deliver/whatsapp`.
|
|
@@ -354,7 +371,7 @@ External users who are not the guardian can gain access to the assistant through
|
|
|
354
371
|
6. Requester enters the code; identity binding is verified, the challenge is consumed, and an active member record is created in `assistant_ingress_members`.
|
|
355
372
|
7. All subsequent messages are accepted through the ingress ACL.
|
|
356
373
|
|
|
357
|
-
**Channel-agnostic design:** The entire flow operates on abstract `ChannelId` and `externalUserId`/`externalChatId`
|
|
374
|
+
**Channel-agnostic design:** The entire flow operates on abstract `ChannelId` and `actorExternalId`/`conversationExternalId` fields (DB column names `externalUserId`/`externalChatId` are unchanged). Identity binding adapts per channel: Telegram uses chat IDs, SMS/voice use E.164 phone numbers, HTTP API uses caller-provided identity. No channel-specific branching exists in the trusted contact code paths.
|
|
358
375
|
|
|
359
376
|
**Lifecycle states:** `requested → pending_guardian → verification_pending → active | denied | expired`
|
|
360
377
|
|
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ Guardian actor-role *classification* (determining whether a sender is guardian,
|
|
|
180
180
|
|-----------------|-------------|
|
|
181
181
|
| `forceStrictSideEffects` | Automatically set on runs triggered by non-guardian or unverified-channel senders so all side-effect tools require approval. |
|
|
182
182
|
| **Fail-closed no-binding** | When no guardian binding exists for a channel, the sender is classified as `unverified_channel`. Any sensitive action is auto-denied with a notice that no guardian has been configured. |
|
|
183
|
-
| **Fail-closed no-identity** | When `
|
|
183
|
+
| **Fail-closed no-identity** | When `actorExternalId` is absent, the actor is classified as `unverified_channel` (even if no guardian binding exists yet). |
|
|
184
184
|
| **Guardian-only approval** | Non-guardian senders cannot approve their own pending actions. Only the verified guardian can approve or deny. |
|
|
185
185
|
| **Expired approval auto-deny** | A proactive sweep runs every 60 seconds to find expired guardian approval requests (30-minute TTL). Expired approvals are auto-denied, and both the requester and guardian are notified. If a non-guardian interacts before the sweep runs, the expiry is also detected reactively. |
|
|
186
186
|
|
|
@@ -273,7 +273,7 @@ The channel guardian service generates verification challenge instructions with
|
|
|
273
273
|
|
|
274
274
|
### Vellum Guardian Identity (Actor Tokens)
|
|
275
275
|
|
|
276
|
-
The vellum channel (macOS, iOS, CLI) uses HMAC-SHA256 signed actor tokens to bind guardian identity to HTTP requests. This enables identity-based authentication for the local desktop/mobile channel, paralleling how external channels (Telegram, SMS) use `
|
|
276
|
+
The vellum channel (macOS, iOS, CLI) uses HMAC-SHA256 signed actor tokens to bind guardian identity to HTTP requests. This enables identity-based authentication for the local desktop/mobile channel, paralleling how external channels (Telegram, SMS) use `actorExternalId` for guardian identity.
|
|
277
277
|
|
|
278
278
|
- **Bootstrap**: After hatch, the macOS client calls `POST /v1/integrations/guardian/vellum/bootstrap` with `{ platform, deviceId }`. Returns `{ guardianPrincipalId, actorToken, isNew }`. The endpoint is idempotent -- repeated calls with the same device return the same principal but mint a fresh token (revoking the previous one).
|
|
279
279
|
- **iOS pairing**: The pairing response includes an `actorToken` automatically when a vellum guardian binding exists.
|
|
@@ -292,15 +292,15 @@ Guardian verification establishes a cryptographic trust binding between a human
|
|
|
292
292
|
1. **Challenge creation** — The owner initiates verification from the desktop UI, which sends a guardian-verification IPC message (`create_challenge` action) to the daemon. The daemon generates a random secret (32-byte hex for unbound inbound/bootstrap sessions, 6-digit numeric for identity-bound sessions), hashes it with SHA-256, stores the hash with a 10-minute TTL, and returns the raw secret to the desktop.
|
|
293
293
|
2. **Code sharing** — The desktop displays the code and instructs the owner to reply with that code in the target channel conversation (e.g., Telegram or SMS).
|
|
294
294
|
3. **Verification** — When the message arrives at `/channels/inbound`, the handler intercepts valid verification-code replies before normal message processing. It hashes the provided code, looks up a matching pending challenge, validates expiry, and consumes the challenge (preventing replay).
|
|
295
|
-
4. **Binding** — On success, any existing active binding for the `(assistantId, channel)` pair is revoked, and a new guardian binding is created with the verifier's `
|
|
295
|
+
4. **Binding** — On success, any existing active binding for the `(assistantId, channel)` pair is revoked, and a new guardian binding is created with the verifier's `actorExternalId` and `chatId` (DB columns: `externalUserId`, `chatId`). The verifier receives a confirmation message.
|
|
296
296
|
|
|
297
297
|
Rate limiting protects against brute-force attempts: 5 invalid attempts within 15 minutes trigger a 30-minute lockout per `(assistantId, channel, actor)` tuple. The same generic failure message is returned for both invalid codes and rate-limited attempts to avoid leaking state.
|
|
298
298
|
|
|
299
299
|
### Ingress ACL Enforcement
|
|
300
300
|
|
|
301
|
-
The ingress ACL runs at the top of the channel inbound handler, before guardian role resolution and message processing. When `
|
|
301
|
+
The ingress ACL runs at the top of the channel inbound handler, before guardian role resolution and message processing. When `actorExternalId` is present, the handler enforces this decision chain:
|
|
302
302
|
|
|
303
|
-
1. **Member lookup** — Look up the sender in `assistant_ingress_members` by `(sourceChannel,
|
|
303
|
+
1. **Member lookup** — Look up the sender in `assistant_ingress_members` by `(sourceChannel, actorExternalId)` or `(sourceChannel, conversationExternalId)`. The DB uses `externalUserId` and `externalChatId` column names internally.
|
|
304
304
|
2. **Non-member denial** — If no member record exists, the message is denied with `not_a_member`.
|
|
305
305
|
3. **Status check** — If the member exists but is not `active` (e.g., `revoked` or `blocked`), the message is denied.
|
|
306
306
|
4. **Policy check** — The member's `policy` field determines routing:
|
|
@@ -498,7 +498,7 @@ The image runs as non-root user `assistant` (uid 1001) and exposes port `3001`.
|
|
|
498
498
|
| 403 `GATEWAY_ORIGIN_REQUIRED` on `/channels/inbound` | Missing or invalid `X-Gateway-Origin` header | Ensure `RUNTIME_GATEWAY_ORIGIN_SECRET` is set to the same value on both gateway and runtime. If not using a dedicated secret, ensure the bearer token (`RUNTIME_BEARER_TOKEN` or `~/.vellum/http-token`) is shared. |
|
|
499
499
|
| Non-guardian actions silently denied | No guardian binding for the channel. The system is fail-closed for unverified channels. | Run the guardian verification flow from the desktop UI to bind a guardian. |
|
|
500
500
|
| Guardian approval expired | The 30-minute TTL elapsed. The proactive sweep auto-denied the approval and notified both parties. | The requester must re-trigger the action. |
|
|
501
|
-
| `forceStrictSideEffects` unexpectedly active | The sender is classified as `non-guardian` or `unverified_channel` | Verify the sender's `
|
|
501
|
+
| `forceStrictSideEffects` unexpectedly active | The sender is classified as `non-guardian` or `unverified_channel` | Verify the sender's `actorExternalId` matches the guardian binding, or set up a guardian binding for the channel. |
|
|
502
502
|
|
|
503
503
|
### Invalid RRULE set expressions
|
|
504
504
|
|
package/bun.lock
CHANGED
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
"drizzle-orm": "^0.38.4",
|
|
20
20
|
"ink": "^6.7.0",
|
|
21
21
|
"jszip": "^3.10.1",
|
|
22
|
-
"minimatch": "^10.
|
|
22
|
+
"minimatch": "^10.2.4",
|
|
23
23
|
"openai": "^6.18.0",
|
|
24
24
|
"pino": "^9.6.0",
|
|
25
25
|
"pino-pretty": "^13.1.3",
|
|
26
26
|
"playwright": "^1.58.2",
|
|
27
27
|
"postgres": "^3.4.8",
|
|
28
|
+
"qrcode": "^1.5.4",
|
|
28
29
|
"react": "^19.2.4",
|
|
29
30
|
"rrule": "^2.8.1",
|
|
30
31
|
"tldts": "^7.0.23",
|
|
@@ -594,6 +595,8 @@
|
|
|
594
595
|
|
|
595
596
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
|
596
597
|
|
|
598
|
+
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
|
599
|
+
|
|
597
600
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
|
598
601
|
|
|
599
602
|
"cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
|
|
@@ -658,6 +661,8 @@
|
|
|
658
661
|
|
|
659
662
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
660
663
|
|
|
664
|
+
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
|
665
|
+
|
|
661
666
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
|
662
667
|
|
|
663
668
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
|
@@ -666,6 +671,8 @@
|
|
|
666
671
|
|
|
667
672
|
"diff": ["diff@4.0.4", "", {}, "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ=="],
|
|
668
673
|
|
|
674
|
+
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
|
|
675
|
+
|
|
669
676
|
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
|
670
677
|
|
|
671
678
|
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="],
|
|
@@ -966,7 +973,7 @@
|
|
|
966
973
|
|
|
967
974
|
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
|
968
975
|
|
|
969
|
-
"minimatch": ["minimatch@10.2.
|
|
976
|
+
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
|
970
977
|
|
|
971
978
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
|
972
979
|
|
|
@@ -1014,6 +1021,8 @@
|
|
|
1014
1021
|
|
|
1015
1022
|
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
|
1016
1023
|
|
|
1024
|
+
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
|
1025
|
+
|
|
1017
1026
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
|
1018
1027
|
|
|
1019
1028
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
|
@@ -1060,6 +1069,8 @@
|
|
|
1060
1069
|
|
|
1061
1070
|
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
|
|
1062
1071
|
|
|
1072
|
+
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
|
1073
|
+
|
|
1063
1074
|
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
|
1064
1075
|
|
|
1065
1076
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
|
@@ -1090,6 +1101,8 @@
|
|
|
1090
1101
|
|
|
1091
1102
|
"pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="],
|
|
1092
1103
|
|
|
1104
|
+
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
|
1105
|
+
|
|
1093
1106
|
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
|
1094
1107
|
|
|
1095
1108
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
|
@@ -1120,6 +1133,8 @@
|
|
|
1120
1133
|
|
|
1121
1134
|
"require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
|
|
1122
1135
|
|
|
1136
|
+
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
|
|
1137
|
+
|
|
1123
1138
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
|
1124
1139
|
|
|
1125
1140
|
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
|
@@ -1152,6 +1167,8 @@
|
|
|
1152
1167
|
|
|
1153
1168
|
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
|
1154
1169
|
|
|
1170
|
+
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
|
1171
|
+
|
|
1155
1172
|
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
|
|
1156
1173
|
|
|
1157
1174
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
|
@@ -1282,6 +1299,8 @@
|
|
|
1282
1299
|
|
|
1283
1300
|
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
|
1284
1301
|
|
|
1302
|
+
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
|
|
1303
|
+
|
|
1285
1304
|
"widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
|
|
1286
1305
|
|
|
1287
1306
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
|
@@ -1324,6 +1343,8 @@
|
|
|
1324
1343
|
|
|
1325
1344
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
|
1326
1345
|
|
|
1346
|
+
"@eslint/config-array/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="],
|
|
1347
|
+
|
|
1327
1348
|
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
|
1328
1349
|
|
|
1329
1350
|
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
|
@@ -1568,6 +1589,8 @@
|
|
|
1568
1589
|
|
|
1569
1590
|
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
|
1570
1591
|
|
|
1592
|
+
"eslint/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="],
|
|
1593
|
+
|
|
1571
1594
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
1572
1595
|
|
|
1573
1596
|
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
|
@@ -1592,6 +1615,8 @@
|
|
|
1592
1615
|
|
|
1593
1616
|
"pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
|
|
1594
1617
|
|
|
1618
|
+
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
|
1619
|
+
|
|
1595
1620
|
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
|
1596
1621
|
|
|
1597
1622
|
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
|
@@ -1742,6 +1767,16 @@
|
|
|
1742
1767
|
|
|
1743
1768
|
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
|
1744
1769
|
|
|
1770
|
+
"qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
|
1771
|
+
|
|
1772
|
+
"qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
|
1773
|
+
|
|
1774
|
+
"qrcode/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
|
1775
|
+
|
|
1776
|
+
"qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
|
1777
|
+
|
|
1778
|
+
"qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
|
1779
|
+
|
|
1745
1780
|
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
|
1746
1781
|
|
|
1747
1782
|
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
|
@@ -1770,6 +1805,16 @@
|
|
|
1770
1805
|
|
|
1771
1806
|
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
1772
1807
|
|
|
1808
|
+
"qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
1809
|
+
|
|
1810
|
+
"qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
|
1811
|
+
|
|
1812
|
+
"qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
|
1813
|
+
|
|
1814
|
+
"qrcode/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
|
1815
|
+
|
|
1816
|
+
"qrcode/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
1817
|
+
|
|
1773
1818
|
"readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
1774
1819
|
|
|
1775
1820
|
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
|
@@ -1778,6 +1823,16 @@
|
|
|
1778
1823
|
|
|
1779
1824
|
"archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
1780
1825
|
|
|
1826
|
+
"qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
1827
|
+
|
|
1828
|
+
"qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
1829
|
+
|
|
1830
|
+
"qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
|
1831
|
+
|
|
1832
|
+
"qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
1833
|
+
|
|
1781
1834
|
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
1835
|
+
|
|
1836
|
+
"qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
|
1782
1837
|
}
|
|
1783
1838
|
}
|
|
@@ -197,15 +197,15 @@ Runtime profile flow (per turn):
|
|
|
197
197
|
|
|
198
198
|
### Provenance-Aware Memory Pipeline
|
|
199
199
|
|
|
200
|
-
Every persisted message carries provenance metadata (`
|
|
200
|
+
Every persisted message carries provenance metadata (`provenanceTrustClass`, `provenanceSourceChannel`, etc.) derived from the `GuardianRuntimeContext` resolved by `guardian-context-resolver.ts`. This metadata records the trust class of the actor who produced the message and through which channel, enabling downstream trust decisions without re-resolving identity at read time.
|
|
201
201
|
|
|
202
|
-
Two trust gates enforce
|
|
202
|
+
Two trust gates enforce trust-class-based access control over the memory pipeline:
|
|
203
203
|
|
|
204
|
-
- **Write gate** (`indexer.ts`): The `extract_items` and `resolve_conflicts` jobs only run for messages from trusted actors (guardian or
|
|
204
|
+
- **Write gate** (`indexer.ts`): The `extract_items` and `resolve_conflicts` jobs only run for messages from trusted actors (guardian or undefined provenance). Messages from untrusted actors (`trusted_contact`, `unknown`) are still segmented and embedded — so they appear in conversation context — but no profile extraction or conflict resolution is triggered. This prevents untrusted channels from injecting or mutating long-term memory items.
|
|
205
205
|
|
|
206
206
|
- **Read gate** (`session-memory.ts`): When the current session's actor is untrusted, the memory recall pipeline returns a no-op context — no recall injection, no dynamic profile, no conflict clarification prompts. This ensures untrusted actors cannot surface or exploit previously extracted memory.
|
|
207
207
|
|
|
208
|
-
Trust policy is **cross-channel and
|
|
208
|
+
Trust policy is **cross-channel and trust-class-based**: decisions use `guardianContext.trustClass`, not the channel string. Desktop/IPC sessions default to `trustClass: 'guardian'`. External channels (Telegram, SMS, WhatsApp, voice) provide explicit guardian context via the resolver. Messages without provenance metadata are treated as trusted (guardian); all new messages carry provenance.
|
|
209
209
|
|
|
210
210
|
---
|
|
211
211
|
|
|
@@ -16,6 +16,14 @@ Design doc defining how unknown users gain access to a Vellum assistant via chan
|
|
|
16
16
|
2. **Assistant rejects the message** via the ingress ACL in `inbound-message-handler.ts`. The user has no `assistant_ingress_members` record, so the handler replies: *"Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."* and returns `{ denied: true, reason: 'not_a_member' }`.
|
|
17
17
|
3. **Notification pipeline alerts the guardian.** The rejection triggers `notifyGuardianOfAccessRequest()` which creates a canonical access request and calls `emitNotificationSignal()` with `sourceEventName: 'ingress.access_request'`. The notification routes through the decision engine to all connected channels (vellum macOS app, Telegram, etc.). The guardian sees who is requesting access, including a request code for approve/reject and an `open invite flow` option to start the Trusted Contacts invite flow.
|
|
18
18
|
|
|
19
|
+
**Access-request copy contract:** Every guardian-facing access-request notification must contain:
|
|
20
|
+
1. **Requester context** — best-available identity (display name, username, external ID, source channel), sanitized to prevent control-character injection.
|
|
21
|
+
2. **Request-code decision directive** — e.g., `Reply "A1B2C3 approve" to grant access or "A1B2C3 reject" to deny.`
|
|
22
|
+
3. **Invite directive** — the exact phrase `Reply "open invite flow" to start Trusted Contacts invite flow.`
|
|
23
|
+
4. **Revoked-member warning** (when applicable) — `Note: this user was previously revoked.`
|
|
24
|
+
|
|
25
|
+
Model-generated phrasing is permitted for the surrounding copy, but a post-generation enforcement step in the decision engine validates that all required directive elements are present. If any are missing, the full deterministic contract text is appended. This ensures the guardian can always parse and act on the notification regardless of LLM output quality.
|
|
26
|
+
|
|
19
27
|
**Guardian binding resolution for access requests** uses a fallback strategy:
|
|
20
28
|
1. Source-channel active binding first (e.g., Telegram binding for a Telegram access request).
|
|
21
29
|
2. Any active binding for the assistant on another channel (deterministic: most recently verified first, then alphabetical by channel).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/assistant",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vellum": "./src/index.ts"
|
|
@@ -40,12 +40,13 @@
|
|
|
40
40
|
"drizzle-orm": "^0.38.4",
|
|
41
41
|
"ink": "^6.7.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
|
-
"minimatch": "^10.
|
|
43
|
+
"minimatch": "^10.2.4",
|
|
44
44
|
"openai": "^6.18.0",
|
|
45
45
|
"pino": "^9.6.0",
|
|
46
46
|
"pino-pretty": "^13.1.3",
|
|
47
47
|
"playwright": "^1.58.2",
|
|
48
48
|
"postgres": "^3.4.8",
|
|
49
|
+
"qrcode": "^1.5.4",
|
|
49
50
|
"react": "^19.2.4",
|
|
50
51
|
"rrule": "^2.8.1",
|
|
51
52
|
"tldts": "^7.0.23",
|
|
@@ -100,7 +100,7 @@ afterAll(() => {
|
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
102
|
describe('actor-token mint/verify', () => {
|
|
103
|
-
test('mint returns token, hash, and claims with default
|
|
103
|
+
test('mint returns token, hash, and claims with default 30-day TTL', () => {
|
|
104
104
|
const result = mintActorToken({
|
|
105
105
|
assistantId: 'self',
|
|
106
106
|
platform: 'macos',
|
|
@@ -115,10 +115,10 @@ describe('actor-token mint/verify', () => {
|
|
|
115
115
|
expect(result.claims.deviceId).toBe('device-123');
|
|
116
116
|
expect(result.claims.guardianPrincipalId).toBe('principal-abc');
|
|
117
117
|
expect(result.claims.iat).toBeGreaterThan(0);
|
|
118
|
-
// Default TTL is
|
|
118
|
+
// Default TTL is 30 days
|
|
119
119
|
expect(result.claims.exp).not.toBeNull();
|
|
120
|
-
const
|
|
121
|
-
expect(result.claims.exp! - result.claims.iat).toBe(
|
|
120
|
+
const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
|
|
121
|
+
expect(result.claims.exp! - result.claims.iat).toBe(thirtyDaysMs);
|
|
122
122
|
expect(result.claims.jti).toBeTruthy();
|
|
123
123
|
});
|
|
124
124
|
|
|
@@ -652,11 +652,14 @@ describe('resolveLocalIpcGuardianContext', () => {
|
|
|
652
652
|
expect(ctx.sourceChannel).toBe('vellum');
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
-
test('returns
|
|
656
|
-
// No binding created — fresh DB state
|
|
655
|
+
test('returns guardian context with principal when no vellum binding exists (pre-bootstrap self-heal)', () => {
|
|
656
|
+
// No binding created — fresh DB state. Pre-bootstrap path self-heals
|
|
657
|
+
// by creating a vellum binding, then resolves through the shared pipeline
|
|
658
|
+
// with correct field names (conversationExternalId, actorExternalId).
|
|
657
659
|
const ctx = resolveLocalIpcGuardianContext();
|
|
658
660
|
expect(ctx.trustClass).toBe('guardian');
|
|
659
661
|
expect(ctx.sourceChannel).toBe('vellum');
|
|
662
|
+
expect(ctx.guardianPrincipalId).toBeDefined();
|
|
660
663
|
});
|
|
661
664
|
|
|
662
665
|
test('respects custom sourceChannel parameter', () => {
|
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
getCanonicalGuardianRequest,
|
|
137
137
|
getPendingCanonicalRequestByCallSessionId,
|
|
138
138
|
} from '../memory/canonical-guardian-store.js';
|
|
139
|
+
import { getMessages } from '../memory/conversation-store.js';
|
|
139
140
|
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
140
141
|
import { conversations } from '../memory/schema.js';
|
|
141
142
|
|
|
@@ -238,6 +239,41 @@ function setupController(task?: string, opts?: { assistantId?: string; guardianC
|
|
|
238
239
|
return { session, relay, controller };
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
function getLatestAssistantText(conversationId: string): string | null {
|
|
243
|
+
const msgs = getMessages(conversationId).filter((m) => m.role === 'assistant');
|
|
244
|
+
if (msgs.length === 0) return null;
|
|
245
|
+
const latest = msgs[msgs.length - 1];
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(latest.content) as unknown;
|
|
248
|
+
if (Array.isArray(parsed)) {
|
|
249
|
+
return parsed
|
|
250
|
+
.filter((b): b is { type: string; text?: string } => typeof b === 'object' && b != null)
|
|
251
|
+
.filter((b) => b.type === 'text')
|
|
252
|
+
.map((b) => b.text ?? '')
|
|
253
|
+
.join('');
|
|
254
|
+
}
|
|
255
|
+
if (typeof parsed === 'string') return parsed;
|
|
256
|
+
} catch { /* fall through */ }
|
|
257
|
+
return latest.content;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function setupControllerWithOrigin(task?: string) {
|
|
261
|
+
ensureConversation('conv-ctrl-voice');
|
|
262
|
+
ensureConversation('conv-ctrl-origin');
|
|
263
|
+
const session = createCallSession({
|
|
264
|
+
conversationId: 'conv-ctrl-voice',
|
|
265
|
+
provider: 'twilio',
|
|
266
|
+
fromNumber: '+15551111111',
|
|
267
|
+
toNumber: '+15552222222',
|
|
268
|
+
task,
|
|
269
|
+
initiatedFromConversationId: 'conv-ctrl-origin',
|
|
270
|
+
});
|
|
271
|
+
updateCallSession(session.id, { status: 'in_progress', startedAt: Date.now() - 30_000 });
|
|
272
|
+
const relay = createMockRelay();
|
|
273
|
+
const controller = new CallController(session.id, relay as unknown as RelayConnection, task ?? null, {});
|
|
274
|
+
return { session, relay, controller };
|
|
275
|
+
}
|
|
276
|
+
|
|
241
277
|
describe('call-controller', () => {
|
|
242
278
|
beforeEach(() => {
|
|
243
279
|
resetTables();
|
|
@@ -364,6 +400,43 @@ describe('call-controller', () => {
|
|
|
364
400
|
controller.destroy();
|
|
365
401
|
});
|
|
366
402
|
|
|
403
|
+
test('markNextCallerTurnAsOpeningAck: tags the next caller turn with CALL_OPENING_ACK without requiring a prior CALL_OPENING', async () => {
|
|
404
|
+
let turnCount = 0;
|
|
405
|
+
mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
|
|
406
|
+
turnCount++;
|
|
407
|
+
|
|
408
|
+
if (turnCount === 1) {
|
|
409
|
+
// First caller utterance after markNextCallerTurnAsOpeningAck
|
|
410
|
+
expect(opts.content).toContain('[CALL_OPENING_ACK]');
|
|
411
|
+
expect(opts.content).toContain('I want to check my balance');
|
|
412
|
+
for (const token of ['Sure, let me check your balance.']) {
|
|
413
|
+
opts.onTextDelta(token);
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
// Subsequent utterance should NOT have the marker
|
|
417
|
+
expect(opts.content).not.toContain('[CALL_OPENING_ACK]');
|
|
418
|
+
for (const token of ['Your balance is $42.']) {
|
|
419
|
+
opts.onTextDelta(token);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
opts.onComplete();
|
|
423
|
+
return { turnId: `run-${turnCount}`, abort: () => {} };
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const { controller } = setupController();
|
|
427
|
+
|
|
428
|
+
// Simulate post-approval: call markNextCallerTurnAsOpeningAck directly
|
|
429
|
+
// without any prior startInitialGreeting / CALL_OPENING
|
|
430
|
+
controller.markNextCallerTurnAsOpeningAck();
|
|
431
|
+
|
|
432
|
+
await controller.handleCallerUtterance('I want to check my balance');
|
|
433
|
+
await controller.handleCallerUtterance('How much exactly?');
|
|
434
|
+
|
|
435
|
+
expect(turnCount).toBe(2);
|
|
436
|
+
|
|
437
|
+
controller.destroy();
|
|
438
|
+
});
|
|
439
|
+
|
|
367
440
|
// ── ASK_GUARDIAN pattern ──────────────────────────────────────────
|
|
368
441
|
|
|
369
442
|
test('ASK_GUARDIAN pattern: detects pattern, creates pending question, sets session to waiting_on_user', async () => {
|
|
@@ -1746,4 +1819,46 @@ describe('call-controller', () => {
|
|
|
1746
1819
|
|
|
1747
1820
|
controller.destroy();
|
|
1748
1821
|
});
|
|
1822
|
+
|
|
1823
|
+
// ── Pointer message regression tests ─────────────────────────────
|
|
1824
|
+
|
|
1825
|
+
test('END_CALL marker writes completed pointer to origin conversation', async () => {
|
|
1826
|
+
mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Goodbye! [END_CALL]']));
|
|
1827
|
+
const { controller } = setupControllerWithOrigin();
|
|
1828
|
+
|
|
1829
|
+
await controller.handleCallerUtterance('Bye');
|
|
1830
|
+
// Allow async pointer write to flush
|
|
1831
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1832
|
+
|
|
1833
|
+
const text = getLatestAssistantText('conv-ctrl-origin');
|
|
1834
|
+
expect(text).not.toBeNull();
|
|
1835
|
+
expect(text!).toContain('+15552222222');
|
|
1836
|
+
expect(text!).toContain('completed');
|
|
1837
|
+
|
|
1838
|
+
controller.destroy();
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
test('max duration timeout writes completed pointer to origin conversation', async () => {
|
|
1842
|
+
// Use a very short max duration to trigger the timeout quickly.
|
|
1843
|
+
// The real MAX_CALL_DURATION_MS mock is 12 minutes; override via
|
|
1844
|
+
// call-constants mock (already set to 12*60*1000). Instead, we
|
|
1845
|
+
// directly test the timer-based path by creating a session with
|
|
1846
|
+
// startedAt in the past and triggering the path manually.
|
|
1847
|
+
|
|
1848
|
+
// For this test, we check that when the session has an
|
|
1849
|
+
// initiatedFromConversationId and startedAt, the completion pointer
|
|
1850
|
+
// is written with a duration.
|
|
1851
|
+
mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Goodbye! [END_CALL]']));
|
|
1852
|
+
const { controller } = setupControllerWithOrigin();
|
|
1853
|
+
|
|
1854
|
+
await controller.handleCallerUtterance('End call');
|
|
1855
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1856
|
+
|
|
1857
|
+
const text = getLatestAssistantText('conv-ctrl-origin');
|
|
1858
|
+
expect(text).not.toBeNull();
|
|
1859
|
+
expect(text!).toContain('+15552222222');
|
|
1860
|
+
expect(text!).toContain('completed');
|
|
1861
|
+
|
|
1862
|
+
controller.destroy();
|
|
1863
|
+
});
|
|
1749
1864
|
});
|