@vellumai/assistant 0.4.2 → 0.4.4

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.
Files changed (221) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +124 -10
  3. package/README.md +43 -35
  4. package/docs/trusted-contact-access.md +20 -0
  5. package/package.json +1 -1
  6. package/scripts/ipc/generate-swift.ts +1 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  8. package/src/__tests__/access-request-decision.test.ts +0 -1
  9. package/src/__tests__/actor-token-service.test.ts +1099 -0
  10. package/src/__tests__/agent-loop.test.ts +51 -0
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
  14. package/src/__tests__/call-controller.test.ts +49 -0
  15. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  16. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  17. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  18. package/src/__tests__/call-routes-http.test.ts +0 -25
  19. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  21. package/src/__tests__/channel-guardian.test.ts +0 -86
  22. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  23. package/src/__tests__/checker.test.ts +33 -12
  24. package/src/__tests__/config-schema.test.ts +6 -0
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  27. package/src/__tests__/conversation-routes.test.ts +12 -3
  28. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  29. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  30. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  31. package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
  32. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  33. package/src/__tests__/guardian-outbound-http.test.ts +4 -5
  34. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  35. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  36. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  37. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  38. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  39. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  40. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  42. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  43. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  44. package/src/__tests__/non-member-access-request.test.ts +159 -9
  45. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  46. package/src/__tests__/notification-decision-strategy.test.ts +106 -2
  47. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  48. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  49. package/src/__tests__/relay-server.test.ts +1475 -33
  50. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  51. package/src/__tests__/session-agent-loop.test.ts +1 -0
  52. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  53. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  54. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  55. package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
  56. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  57. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  58. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  59. package/src/__tests__/tool-executor.test.ts +21 -2
  60. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  61. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  62. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  63. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  64. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  65. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  66. package/src/__tests__/twilio-config.test.ts +2 -13
  67. package/src/__tests__/twilio-routes.test.ts +4 -3
  68. package/src/__tests__/update-bulletin.test.ts +0 -1
  69. package/src/agent/loop.ts +1 -1
  70. package/src/approvals/guardian-decision-primitive.ts +12 -3
  71. package/src/approvals/guardian-request-resolvers.ts +169 -11
  72. package/src/calls/call-constants.ts +29 -0
  73. package/src/calls/call-controller.ts +11 -3
  74. package/src/calls/call-domain.ts +33 -11
  75. package/src/calls/call-pointer-message-composer.ts +154 -0
  76. package/src/calls/call-pointer-messages.ts +106 -27
  77. package/src/calls/guardian-dispatch.ts +4 -2
  78. package/src/calls/relay-server.ts +921 -112
  79. package/src/calls/twilio-config.ts +4 -11
  80. package/src/calls/twilio-routes.ts +4 -6
  81. package/src/calls/types.ts +3 -1
  82. package/src/calls/voice-session-bridge.ts +4 -3
  83. package/src/cli/core-commands.ts +7 -4
  84. package/src/cli.ts +5 -4
  85. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  86. package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
  87. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  88. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  89. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  90. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  91. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  92. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  93. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  94. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  96. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  97. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
  98. package/src/config/calls-schema.ts +36 -0
  99. package/src/config/env.ts +22 -0
  100. package/src/config/feature-flag-registry.json +8 -8
  101. package/src/config/schema.ts +2 -2
  102. package/src/config/skills.ts +11 -0
  103. package/src/config/system-prompt.ts +11 -1
  104. package/src/config/templates/SOUL.md +2 -0
  105. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  106. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
  107. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  108. package/src/daemon/call-pointer-generators.ts +59 -0
  109. package/src/daemon/computer-use-session.ts +2 -5
  110. package/src/daemon/handlers/apps.ts +76 -20
  111. package/src/daemon/handlers/config-channels.ts +9 -61
  112. package/src/daemon/handlers/config-inbox.ts +11 -3
  113. package/src/daemon/handlers/config-ingress.ts +28 -3
  114. package/src/daemon/handlers/config-telegram.ts +12 -0
  115. package/src/daemon/handlers/config.ts +2 -6
  116. package/src/daemon/handlers/index.ts +2 -1
  117. package/src/daemon/handlers/pairing.ts +2 -0
  118. package/src/daemon/handlers/publish.ts +11 -46
  119. package/src/daemon/handlers/sessions.ts +59 -5
  120. package/src/daemon/handlers/shared.ts +17 -2
  121. package/src/daemon/ipc-contract/apps.ts +1 -0
  122. package/src/daemon/ipc-contract/inbox.ts +4 -0
  123. package/src/daemon/ipc-contract/integrations.ts +1 -97
  124. package/src/daemon/ipc-contract/messages.ts +47 -1
  125. package/src/daemon/ipc-contract/notifications.ts +11 -0
  126. package/src/daemon/ipc-contract-inventory.json +2 -4
  127. package/src/daemon/lifecycle.ts +17 -0
  128. package/src/daemon/server.ts +16 -2
  129. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  130. package/src/daemon/session-agent-loop.ts +24 -12
  131. package/src/daemon/session-lifecycle.ts +1 -1
  132. package/src/daemon/session-process.ts +11 -1
  133. package/src/daemon/session-runtime-assembly.ts +6 -1
  134. package/src/daemon/session-surfaces.ts +32 -3
  135. package/src/daemon/session.ts +88 -1
  136. package/src/daemon/tool-side-effects.ts +22 -0
  137. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  138. package/src/home-base/prebuilt/index.html +40 -0
  139. package/src/inbound/platform-callback-registration.ts +157 -0
  140. package/src/memory/canonical-guardian-store.ts +1 -1
  141. package/src/memory/conversation-crud.ts +2 -1
  142. package/src/memory/conversation-title-service.ts +16 -2
  143. package/src/memory/db-init.ts +8 -0
  144. package/src/memory/delivery-crud.ts +2 -1
  145. package/src/memory/guardian-action-store.ts +2 -1
  146. package/src/memory/guardian-approvals.ts +3 -2
  147. package/src/memory/ingress-invite-store.ts +12 -2
  148. package/src/memory/ingress-member-store.ts +4 -3
  149. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  150. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  151. package/src/memory/migrations/index.ts +2 -0
  152. package/src/memory/schema.ts +26 -5
  153. package/src/messaging/provider-types.ts +24 -0
  154. package/src/messaging/provider.ts +7 -0
  155. package/src/messaging/providers/gmail/adapter.ts +127 -0
  156. package/src/messaging/providers/sms/adapter.ts +40 -37
  157. package/src/notifications/adapters/macos.ts +45 -2
  158. package/src/notifications/broadcaster.ts +16 -0
  159. package/src/notifications/copy-composer.ts +50 -2
  160. package/src/notifications/decision-engine.ts +22 -9
  161. package/src/notifications/destination-resolver.ts +16 -2
  162. package/src/notifications/emit-signal.ts +18 -9
  163. package/src/notifications/guardian-question-mode.ts +419 -0
  164. package/src/notifications/signal.ts +14 -3
  165. package/src/permissions/checker.ts +13 -1
  166. package/src/permissions/prompter.ts +14 -0
  167. package/src/providers/anthropic/client.ts +20 -0
  168. package/src/providers/provider-send-message.ts +15 -3
  169. package/src/runtime/access-request-helper.ts +82 -4
  170. package/src/runtime/actor-token-service.ts +234 -0
  171. package/src/runtime/actor-token-store.ts +236 -0
  172. package/src/runtime/actor-trust-resolver.ts +2 -2
  173. package/src/runtime/assistant-scope.ts +10 -0
  174. package/src/runtime/channel-approvals.ts +5 -3
  175. package/src/runtime/channel-readiness-service.ts +23 -64
  176. package/src/runtime/channel-readiness-types.ts +3 -4
  177. package/src/runtime/channel-retry-sweep.ts +4 -1
  178. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  179. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  180. package/src/runtime/guardian-context-resolver.ts +82 -0
  181. package/src/runtime/guardian-outbound-actions.ts +5 -7
  182. package/src/runtime/guardian-reply-router.ts +67 -30
  183. package/src/runtime/guardian-vellum-migration.ts +57 -0
  184. package/src/runtime/http-server.ts +75 -31
  185. package/src/runtime/http-types.ts +13 -0
  186. package/src/runtime/ingress-service.ts +14 -0
  187. package/src/runtime/invite-redemption-service.ts +10 -1
  188. package/src/runtime/local-actor-identity.ts +76 -0
  189. package/src/runtime/middleware/actor-token.ts +271 -0
  190. package/src/runtime/middleware/twilio-validation.ts +2 -4
  191. package/src/runtime/routes/approval-routes.ts +82 -7
  192. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  193. package/src/runtime/routes/call-routes.ts +2 -1
  194. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  195. package/src/runtime/routes/channel-route-shared.ts +3 -3
  196. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  197. package/src/runtime/routes/conversation-routes.ts +142 -53
  198. package/src/runtime/routes/events-routes.ts +22 -8
  199. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  200. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  201. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  202. package/src/runtime/routes/inbound-conversation.ts +4 -3
  203. package/src/runtime/routes/inbound-message-handler.ts +147 -5
  204. package/src/runtime/routes/ingress-routes.ts +2 -0
  205. package/src/runtime/routes/integration-routes.ts +7 -15
  206. package/src/runtime/routes/pairing-routes.ts +163 -0
  207. package/src/runtime/routes/twilio-routes.ts +934 -0
  208. package/src/runtime/tool-grant-request-helper.ts +3 -1
  209. package/src/security/oauth2.ts +27 -2
  210. package/src/security/token-manager.ts +46 -10
  211. package/src/tools/browser/browser-execution.ts +4 -3
  212. package/src/tools/browser/browser-handoff.ts +10 -18
  213. package/src/tools/browser/browser-manager.ts +80 -25
  214. package/src/tools/browser/browser-screencast.ts +35 -119
  215. package/src/tools/calls/call-start.ts +2 -1
  216. package/src/tools/permission-checker.ts +15 -4
  217. package/src/tools/terminal/parser.ts +12 -0
  218. package/src/tools/tool-approval-handler.ts +244 -19
  219. package/src/workspace/git-service.ts +19 -0
  220. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  221. package/src/daemon/handlers/config-twilio.ts +0 -1082
package/.env.example CHANGED
@@ -20,3 +20,6 @@ OLLAMA_BASE_URL=
20
20
 
21
21
  # Enable the runtime HTTP server (required for web local mode).
22
22
  # RUNTIME_HTTP_PORT=7821
23
+
24
+ # Platform base URL (defaults to empty string)
25
+ # PLATFORM_BASE_URL=
package/ARCHITECTURE.md CHANGED
@@ -22,6 +22,43 @@ 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 + Bootstrap)
26
+
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
+
29
+ **Identity lifecycle:**
30
+
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
+
33
+ 2. **Hatch bootstrap (loopback-only, macOS)** — On every launch, 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 is idempotent: it 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 90-day TTL, stores only the SHA-256 hash, and returns `{ guardianPrincipalId, actorToken, isNew }`. The raw token is returned once and never persisted on the server. macOS re-bootstraps on each startup to refresh its token.
34
+
35
+ 3. **iOS pairing** — iOS devices obtain actor tokens exclusively through the QR pairing flow (re-pairs on credential loss). When an iOS device completes pairing, the pairing response includes an `actorToken` 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. iOS does not call the bootstrap endpoint.
36
+
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
+
39
+ **Actor token format:** `base64url(JSON claims) + '.' + base64url(HMAC-SHA256 signature)`. Claims include `assistantId`, `platform`, `deviceId`, `guardianPrincipalId`, `iat`, `exp` (default: 90 days from issuance), and `jti`.
40
+
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
+
43
+ **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
+
45
+ **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.
46
+
47
+ **Notification scoping:** Guardian-sensitive notifications (e.g., approval requests, access request alerts) are annotated with `targetGuardianPrincipalId` so the notification pipeline can scope delivery to the correct guardian identity across devices.
48
+
49
+ **Key source files:**
50
+
51
+ | File | Purpose |
52
+ |------|---------|
53
+ | `src/runtime/actor-token-service.ts` | HMAC-SHA256 mint/verify, signing key management, `hashToken` |
54
+ | `src/runtime/actor-token-store.ts` | Hash-only persistence: create, find by hash/device binding, revoke |
55
+ | `src/runtime/middleware/actor-token.ts` | HTTP middleware: `verifyHttpActorToken`, `verifyHttpActorTokenWithLocalFallback`, `isActorBoundGuardian` |
56
+ | `src/runtime/local-actor-identity.ts` | `resolveLocalIpcGuardianContext` — deterministic IPC identity |
57
+ | `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/pairing-routes.ts` | `mintPairingActorToken` — actor token in pairing response |
60
+ | `src/memory/guardian-bindings.ts` | Guardian binding persistence (shared across all channels) |
61
+
25
62
  ### Channel-Agnostic Scoped Approval Grants
26
63
 
27
64
  Scoped approval grants allow a guardian's approval decision on one channel (e.g., Telegram) to authorize a tool execution on a different channel (e.g., voice). Two scope modes exist: `request_id` (bound to a specific pending request) and `tool_signature` (bound to `toolName` + canonical `inputDigest`). Grants are one-time-use, exact-match, fail-closed, and TTL-bound. Full architecture details (lifecycle flow, security invariants, key files) live in [`docs/architecture/security.md`](docs/architecture/security.md#channel-agnostic-scoped-approval-grants).
@@ -210,7 +247,7 @@ The SMS channel provides text-only messaging via Twilio, sharing the same teleph
210
247
  2. The gateway authenticates the request via bearer token (same fail-closed model as `/deliver/telegram`).
211
248
  3. The gateway sends the SMS via the Twilio Messages API using the configured `TWILIO_PHONE_NUMBER` as the `From` number.
212
249
 
213
- **Setup**: Twilio credentials (Account SID, Auth Token) and phone number are managed via the `twilio_config` IPC contract and the `twilio-setup` skill. A single phone number is shared across voice and SMS for each assistant. Both `provision_number` and `assign_number` auto-persist the number to config and secure storage, and auto-configure Twilio webhooks (voice URL, status callback, SMS URL) via the Twilio IncomingPhoneNumber API when a public ingress URL is available. When `assistantId` is provided, the number is persisted into the per-assistant mapping at `sms.assistantPhoneNumbers[assistantId]`, and the legacy `sms.phoneNumber` field is only set if it was previously empty/unset (acting as a fallback for single-assistant installs). This prevents multi-assistant assignments from clobbering each other's global outbound number. Without `assistantId`, the legacy field is always updated. Webhook configuration is best-effort — if ingress is not yet set up, the number is still assigned and webhooks can be configured later. Non-fatal webhook failures are surfaced as a `warning` field in the `twilio_config_response`.
250
+ **Setup**: Twilio credentials (Account SID, Auth Token) and phone number are managed via HTTP control-plane endpoints (`/v1/integrations/twilio/*`) exposed by the runtime and proxied by the gateway (see `src/runtime/routes/twilio-routes.ts`). A single phone number is shared across voice and SMS for each assistant. Both `provision` and `assign` endpoints auto-persist the number to config and secure storage, and auto-configure Twilio webhooks (voice URL, status callback, SMS URL) via the Twilio IncomingPhoneNumber API when a public ingress URL is available. When `assistantId` is provided, the number is persisted into the per-assistant mapping at `sms.assistantPhoneNumbers[assistantId]`, and the legacy `sms.phoneNumber` field is only set if it was previously empty/unset (acting as a fallback for single-assistant installs). This prevents multi-assistant assignments from clobbering each other's global outbound number. Without `assistantId`, the legacy field is always updated. Webhook configuration is best-effort — if ingress is not yet set up, the number is still assigned and webhooks can be configured later. Non-fatal webhook failures are surfaced as a `warning` field in the response.
214
251
 
215
252
  **Phone Number Resolution**: At runtime, `getTwilioConfig()` resolves the phone number using this priority chain: (1) `TWILIO_PHONE_NUMBER` env var — highest priority, explicit override; (2) `sms.phoneNumber` in config — primary source of truth written by `provision_number`/`assign_number`; (3) `credential:twilio:phone_number` secure key — backward-compatible fallback. An error is thrown if no number is found after all sources are checked.
216
253
 
@@ -256,9 +293,9 @@ These can be set via environment variables or stored in the credential vault (ke
256
293
 
257
294
  **Limitations (v1)**: Text-only — non-text message types are acknowledged but not forwarded; rich approval UI (inline buttons) is not supported.
258
295
 
259
- **Channel Readiness**: The `channel_readiness` IPC contract (`ChannelReadinessService` in `src/runtime/channel-readiness-service.ts`) provides a unified readiness subsystem for all channels. Each channel registers a `ChannelProbe` that runs synchronous local checks (credential presence, phone number, ingress config) and optional async remote checks with a 5-minute TTL cache. Built-in probes: SMS (Twilio credentials, phone number, ingress; remote checks query Twilio toll-free verification status for toll-free numbers) and Telegram (bot token, webhook secret, ingress). The `get` action returns cached snapshots; `refresh` invalidates the cache first. Unknown channels return `unsupported_channel`.
296
+ **Channel Readiness**: The channel readiness HTTP endpoints (`GET /v1/channels/readiness`, `POST /v1/channels/readiness/refresh`) backed by `ChannelReadinessService` in `src/runtime/channel-readiness-service.ts` provide a unified readiness subsystem for all channels. Each channel registers a `ChannelProbe` that runs synchronous local checks (credential presence, phone number, ingress config) and optional async remote checks with a 5-minute TTL cache. Built-in probes: SMS (Twilio credentials, phone number, ingress; remote checks query Twilio toll-free verification status for toll-free numbers) and Telegram (bot token, webhook secret, ingress). The GET endpoint returns cached snapshots; the refresh endpoint invalidates the cache first. Unknown channels return `unsupported_channel`. Route handlers live in `src/runtime/routes/channel-readiness-routes.ts`.
260
297
 
261
- **SMS Compliance & Admin**: The `twilio_config` IPC contract extends beyond credential and number management with compliance and admin actions: `sms_compliance_status` detects toll-free vs local number type and fetches verification status; `sms_submit_tollfree_verification`, `sms_update_tollfree_verification`, and `sms_delete_tollfree_verification` manage the Twilio toll-free verification lifecycle; `release_number` removes a phone number from the Twilio account and clears all local references. All compliance actions validate required fields and Twilio enum values before calling the API.
298
+ **SMS Compliance & Admin**: The Twilio HTTP control-plane endpoints extend beyond credential and number management with compliance and admin routes: `GET /v1/integrations/twilio/sms/compliance` detects toll-free vs local number type and fetches verification status; `POST/PATCH/DELETE /v1/integrations/twilio/sms/compliance/tollfree` manage the Twilio toll-free verification lifecycle; `POST /v1/integrations/twilio/numbers/release` removes a phone number from the Twilio account and clears all local references. All compliance actions validate required fields and Twilio enum values before calling the API.
262
299
 
263
300
  ### Slack Channel (Socket Mode)
264
301
 
@@ -397,7 +434,7 @@ A complementary access-granting flow where the guardian proactively creates a sh
397
434
  | Channel | Status | Prerequisites |
398
435
  |---------|--------|--------------|
399
436
  | Telegram | Shipped | Bot username resolved from credential metadata or `TELEGRAM_BOT_USERNAME` env |
400
- | Voice | Shipped | Identity-bound voice code redemption via DTMF/speech in the relay state machine. Gated behind `feature_flags.voice-invite-redemption.enabled` (default OFF). |
437
+ | Voice | Shipped | Identity-bound voice code redemption via DTMF/speech in the relay state machine. Always-on canonical behavior with personalized friend/guardian name prompts. |
401
438
  | SMS | Deferred | Needs a deep-link strategy compatible with SMS (short URL or web redemption page) |
402
439
  | Slack | Deferred | Needs DM-safe ingress — Socket Mode handles channel messages but DM-initiated invite flows need routing |
403
440
 
@@ -413,10 +450,10 @@ Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL
413
450
 
414
451
  **Call-time redemption subflow (`invite_redemption_pending`):**
415
452
  1. Unknown caller dials in. `relay-server.ts` resolves trust via `resolveActorTrust`. Caller is `unknown`, no pending guardian challenge.
416
- 2. If `feature_flags.voice-invite-redemption.enabled` is ON, the relay checks `findActiveVoiceInvites` for invites bound to the caller's phone number.
417
- 3. If active, non-expired invites exist, the relay enters the `invite_redemption_pending` state (reuses the `verification_pending` connection state) and prompts the caller to enter their invite code via DTMF or speech.
453
+ 2. The relay checks `findActiveVoiceInvites` for invites bound to the caller's phone number.
454
+ 3. If active, non-expired invites exist, the relay enters the `invite_redemption_pending` state (reuses the `verification_pending` connection state) and prompts the caller with personalized copy: `Welcome <friend-name>. Please enter the 6-digit code that <guardian-name> provided you to verify your identity.`
418
455
  4. `redeemVoiceInviteCode` validates: identity match, code hash match, expiry, use count. On success, an active member record is upserted and the call transitions to the normal call flow.
419
- 5. On failure, the caller gets up to 3 attempts. After max attempts, the call is terminated.
456
+ 5. On invalid/expired code, the caller hears deterministic failure copy: `Sorry, the code you provided is incorrect or has since expired. Please ask <guardian-name> for a new code. Goodbye.` and the call ends immediately.
420
457
 
421
458
  **Security invariants:**
422
459
  - The plaintext voice code is returned exactly once at creation time and never stored.
@@ -424,8 +461,6 @@ Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL
424
461
  - Failure responses are intentionally generic (`invalid_or_expired`) to prevent oracle attacks.
425
462
  - Blocked members cannot bypass the guardian's explicit block via invite redemption.
426
463
 
427
- **Feature flag:** `feature_flags.voice-invite-redemption.enabled` (default OFF). When disabled, unknown callers with active voice invites are denied normally — the invite check is skipped entirely.
428
-
429
464
  **Key source files:**
430
465
 
431
466
  | File | Purpose |
@@ -439,10 +474,76 @@ Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL
439
474
  | `src/runtime/ingress-service.ts` | Shared business logic for invite/member operations (used by both HTTP routes and IPC) |
440
475
  | `src/runtime/routes/ingress-routes.ts` | HTTP API handlers for member/invite management including voice invite creation and redemption |
441
476
  | `src/runtime/routes/inbound-message-handler.ts` | Invite token intercept in the inbound flow (non-member and inactive-member branches) |
442
- | `src/calls/relay-server.ts` | Voice relay state machine — `invite_redemption_pending` subflow, feature flag gate |
477
+ | `src/calls/relay-server.ts` | Voice relay state machine — `invite_redemption_pending` subflow (always-on canonical behavior) |
443
478
  | `src/util/voice-code.ts` | Cryptographic voice code generation and SHA-256 hashing |
444
479
  | `src/memory/ingress-invite-store.ts` | Invite persistence including `findActiveVoiceInvites` for identity-bound lookup |
445
480
 
481
+ ### Voice Inbound Security Model (Canonical)
482
+
483
+ The voice inbound security model determines how unknown callers are handled when they dial in. Three paths exist, evaluated in priority order by `relay-server.ts` during the `handleSetup` phase. All guardian decisions route through `applyCanonicalGuardianDecision` in the canonical guardian request system.
484
+
485
+ **Decision tree for inbound unknown callers:**
486
+
487
+ ```
488
+ Unknown caller dials in
489
+ |
490
+ v
491
+ resolveActorTrust() → trustClass
492
+ |
493
+ ├── guardian / trusted_contact → normal call flow
494
+ ├── blocked → immediate denial + disconnect
495
+ ├── policy: deny → immediate denial + disconnect
496
+ ├── policy: escalate → denial (voice cannot hold for async approval)
497
+ |
498
+ └── unknown (no binding) ──┐
499
+ |
500
+ ┌────────────────────┼──────────────────────┐
501
+ | | |
502
+ pendingChallenge? activeVoiceInvites? no invite, no challenge
503
+ | | |
504
+ v v v
505
+ Guardian verification Invite redemption Name capture +
506
+ (DTMF/speech code) (personalized code) guardian approval wait
507
+ ```
508
+
509
+ **Path 1: Voice invite code redemption (guardian-initiated)**
510
+
511
+ The guardian proactively creates a voice invite bound to the caller's E.164 phone number. When the unknown caller dials in and has an active, non-expired invite, the relay enters the `invite_redemption_pending` subflow with personalized prompts using the friend's and guardian's names. This is always-on canonical behavior (no feature flag). See [Voice Invite Flow](#voice-invite-flow-invite_redemption_pending) above.
512
+
513
+ **Path 2: Live in-call guardian approval (friend-initiated)**
514
+
515
+ When no invite exists and no pending guardian challenge is active, the relay enters the name capture + guardian approval wait flow:
516
+
517
+ 1. The relay transitions to `awaiting_name` state and prompts the caller for their name with a timeout.
518
+ 2. On name capture, `notifyGuardianOfAccessRequest` creates a canonical guardian request (`kind: 'access_request'`) and notifies the guardian via the notification pipeline.
519
+ 3. The relay transitions to `awaiting_guardian_decision` and plays hold music/messaging while polling the canonical request status.
520
+ 4. The guardian approves or denies via any channel (Telegram, SMS, desktop). All decisions route through `applyCanonicalGuardianDecision`, which dispatches to the `access_request` resolver in `guardian-request-resolvers.ts`.
521
+ 5. On approval: the resolver directly activates the caller as a trusted contact (upserts member with `status: 'active'`, `policy: 'allow'`), the poll detects the approved status, the relay transitions to the normal call flow with the caller's guardian context updated.
522
+ 6. On denial or timeout: the caller hears a denial message and the call ends.
523
+
524
+ **Path 3: Inbound guardian verification (pending challenge)**
525
+
526
+ When a pending voice guardian challenge exists (`getPendingChallenge`), the caller enters the DTMF/speech verification flow to complete an outbound-initiated guardian binding. This path is for guardian identity verification, not trusted-contact access.
527
+
528
+ **Canonical decision routing:**
529
+
530
+ All guardian decisions for voice access requests flow through:
531
+ - `applyCanonicalGuardianDecision` (canonical guardian request system)
532
+ - `accessRequestResolver` in `guardian-request-resolvers.ts` (kind-specific resolver)
533
+ - For voice approvals: direct trusted-contact activation (no verification session needed since the caller is already on the line)
534
+ - For text-channel access requests: verification session creation with 6-digit code (existing `access-request-decision.ts` path for legacy `channel_guardian_approval_requests`)
535
+
536
+ **Key source files:**
537
+
538
+ | File | Purpose |
539
+ |------|---------|
540
+ | `src/calls/relay-server.ts` | Inbound call decision tree, name capture, guardian approval wait polling |
541
+ | `src/runtime/access-request-helper.ts` | Creates canonical access request and notifies guardian |
542
+ | `src/approvals/guardian-decision-primitive.ts` | `applyCanonicalGuardianDecision` — unified decision primitive |
543
+ | `src/approvals/guardian-request-resolvers.ts` | `access_request` resolver — voice direct activation, text-channel verification session |
544
+ | `src/runtime/actor-trust-resolver.ts` | `resolveActorTrust` — caller trust classification |
545
+ | `src/memory/canonical-guardian-store.ts` | Canonical request persistence and CAS resolution |
546
+
446
547
  ### Update Bulletin System
447
548
 
448
549
  Release-driven update notification system that surfaces release notes to the assistant via the system prompt.
@@ -1954,3 +2055,16 @@ Key files: `src/tools/sensitive-output-placeholders.ts`, `src/tools/executor.ts`
1954
2055
  ### Notifications
1955
2056
 
1956
2057
  For full notification developer guidance and lifecycle details, see [`assistant/src/notifications/README.md`](src/notifications/README.md).
2058
+
2059
+ ### Assistant Identity Boundary
2060
+
2061
+ The daemon uses a single fixed internal scope constant — `DAEMON_INTERNAL_ASSISTANT_ID` (`'self'`), exported from `src/runtime/assistant-scope.ts` — for all assistant-scoped storage and routing within the daemon process. Public/external assistant IDs (e.g., those assigned during hatch, invite links, or platform registration) are an **edge concern** owned by the gateway and platform layers.
2062
+
2063
+ **Boundary rule:** Daemon code must never derive internal scoping decisions from externally-provided assistant IDs. When a daemon path needs an assistant scope and none is provided, it defaults to `DAEMON_INTERNAL_ASSISTANT_ID`. The gateway is responsible for mapping public assistant IDs to internal routing before forwarding requests to the daemon.
2064
+
2065
+ **Key files:**
2066
+
2067
+ | File | Purpose |
2068
+ |------|---------|
2069
+ | `src/runtime/assistant-scope.ts` | Exports `DAEMON_INTERNAL_ASSISTANT_ID` constant |
2070
+ | `src/__tests__/assistant-id-boundary-guard.test.ts` | Guard tests enforcing the identity boundary |
package/README.md CHANGED
@@ -201,29 +201,29 @@ The `/channels/inbound` endpoint requires a valid `X-Gateway-Origin` header to p
201
201
 
202
202
  ## Twilio Setup Primitive
203
203
 
204
- Twilio is the shared telephony provider for both voice calls and SMS messaging. Configuration is managed through the `twilio_config` IPC contract and the `twilio-setup` skill. For SMS-specific onboarding (including compliance verification and test sending), the `sms-setup` skill provides a guided conversational flow that layers on top of `twilio-setup`.
205
-
206
- ### `twilio_config` IPC Contract
207
-
208
- The daemon handles `twilio_config` messages with the following actions:
209
-
210
- | Action | Description |
211
- |--------|-------------|
212
- | `get` | Returns current state: `hasCredentials` (boolean) and `phoneNumber` (if assigned) |
213
- | `set_credentials` | Validates and stores Account SID and Auth Token in secure storage (Keychain / encrypted file). Credentials are retrieved from the credential store internally. |
214
- | `clear_credentials` | Removes stored Account SID and Auth Token from secure storage. Preserves the phone number in both config (`sms.phoneNumber`) and secure key (`credential:twilio:phone_number`) so that re-entering credentials resumes working without needing to reassign the number. |
215
- | `provision_number` | Purchases a new phone number via the Twilio API. Accepts optional `areaCode` and `country` (ISO 3166-1 alpha-2, default `US`). Auto-assigns the number to the assistant (persists to config and secure storage) and configures Twilio webhooks (voice, status callback, SMS) when a public ingress URL is available. |
216
- | `assign_number` | Assigns an existing Twilio phone number (E.164 format) to the assistant and auto-configures webhooks when ingress is available |
217
- | `list_numbers` | Lists all incoming phone numbers on the Twilio account with their capabilities (voice, SMS) |
218
- | `sms_compliance_status` | Returns the SMS compliance posture for the assigned phone number. Determines number type (toll-free vs local 10DLC) and retrieves toll-free verification status from Twilio. |
219
- | `sms_submit_tollfree_verification` | Submits a new toll-free verification request to Twilio. Validates required fields and enum values. Defaults `businessType` to `SOLE_PROPRIETOR`. |
220
- | `sms_update_tollfree_verification` | Updates an existing toll-free verification by SID. Requires `verificationSid`. |
221
- | `sms_delete_tollfree_verification` | Deletes a toll-free verification by SID. Includes warning about queue priority reset. |
222
- | `release_number` | Releases (deletes) a phone number from the Twilio account. Clears the number from config and secure storage. Includes warning about toll-free verification context loss. |
223
- | `sms_send_test` | Sends a test SMS to the specified `phoneNumber` with the given `text`, polls Twilio for delivery status (up to 3 retries at 2-second intervals), and returns the result in `testResult`. Stores the last result in memory for use by `sms_doctor`. |
224
- | `sms_doctor` | Runs a comprehensive SMS health diagnostic. Checks channel readiness, compliance/toll-free verification status, and the last `sms_send_test` result. Returns structured diagnostics in `diagnostics` with an overall `status` ("healthy", "degraded", or "unhealthy") and actionable `items`. |
225
-
226
- Response type: `twilio_config_response` with `success`, `hasCredentials`, optional `phoneNumber`, optional `numbers` array, optional `error`, optional `warning` (for non-fatal webhook sync failures), optional `compliance` object (for compliance status actions, containing `numberType`, `verificationSid`, `verificationStatus`, `rejectionReason`, `rejectionReasons`, `errorCode`, `editAllowed`, `editExpiration`), optional `testResult` (for `sms_send_test`), and optional `diagnostics` (for `sms_doctor`).
204
+ Twilio is the shared telephony provider for both voice calls and SMS messaging. Configuration is managed through HTTP control-plane endpoints exposed by the runtime and proxied by the gateway. For SMS-specific onboarding (including compliance verification and test sending), the `sms-setup` skill provides a guided conversational flow that layers on top of `twilio-setup`.
205
+
206
+ ### Twilio HTTP Control-Plane Endpoints
207
+
208
+ The runtime exposes a RESTful HTTP API for Twilio configuration, credential management, phone number operations, and SMS compliance:
209
+
210
+ | Method | Path | Description |
211
+ |--------|------|-------------|
212
+ | GET | `/v1/integrations/twilio/config` | Returns current state: `hasCredentials` (boolean) and `phoneNumber` (if assigned) |
213
+ | POST | `/v1/integrations/twilio/credentials` | Validates and stores Account SID and Auth Token in secure storage (Keychain / encrypted file) |
214
+ | DELETE | `/v1/integrations/twilio/credentials` | Removes stored credentials. Preserves the phone number in both config and secure key so re-entering credentials resumes working without reassigning the number. |
215
+ | GET | `/v1/integrations/twilio/numbers` | Lists all incoming phone numbers on the Twilio account with their capabilities (voice, SMS) |
216
+ | POST | `/v1/integrations/twilio/numbers/provision` | Purchases a new phone number. Accepts optional `areaCode` and `country`. Auto-assigns and configures webhooks when ingress is available. |
217
+ | POST | `/v1/integrations/twilio/numbers/assign` | Assigns an existing Twilio phone number (E.164) and auto-configures webhooks when ingress is available |
218
+ | POST | `/v1/integrations/twilio/numbers/release` | Releases a phone number from the Twilio account and clears local references |
219
+ | GET | `/v1/integrations/twilio/sms/compliance` | Returns SMS compliance posture: number type (toll-free vs 10DLC) and toll-free verification status |
220
+ | POST | `/v1/integrations/twilio/sms/compliance/tollfree` | Submits a new toll-free verification request |
221
+ | PATCH | `/v1/integrations/twilio/sms/compliance/tollfree/:sid` | Updates an existing toll-free verification by SID |
222
+ | DELETE | `/v1/integrations/twilio/sms/compliance/tollfree/:sid` | Deletes a toll-free verification by SID |
223
+ | POST | `/v1/integrations/twilio/sms/test` | Sends a test SMS and polls for delivery status |
224
+ | POST | `/v1/integrations/twilio/sms/doctor` | Runs comprehensive SMS health diagnostics |
225
+
226
+ All endpoints are bearer-authenticated via the runtime HTTP token. Skills and clients should call the gateway URL (default `http://localhost:7830`) rather than the runtime port directly, as the gateway proxies all `/v1/integrations/twilio/*` routes.
227
227
 
228
228
  ### Ingress Webhook Reconciliation
229
229
 
@@ -241,9 +241,9 @@ Each assistant is assigned a single Twilio phone number that is shared between v
241
241
 
242
242
  #### Assistant-Scoped Phone Numbers
243
243
 
244
- When `assistantId` is provided in the `twilio_config` request, the `provision_number` and `assign_number` actions persist the phone number into a per-assistant mapping at `sms.assistantPhoneNumbers` (a `Record<string, string>` keyed by assistant ID). The legacy `sms.phoneNumber` field is always updated for backward compatibility.
244
+ When `assistantId` is provided in the Twilio control-plane request, the provision and assign endpoints persist the phone number into a per-assistant mapping at `sms.assistantPhoneNumbers` (a `Record<string, string>` keyed by assistant ID). The legacy `sms.phoneNumber` field is always updated for backward compatibility.
245
245
 
246
- The `get` action, when called with `assistantId`, resolves the phone number by checking `sms.assistantPhoneNumbers[assistantId]` first, falling back to `sms.phoneNumber`. This allows multiple assistants to have distinct phone numbers while preserving existing behavior for single-assistant setups.
246
+ The config endpoint (`GET /v1/integrations/twilio/config`), when called with `assistantId`, resolves the phone number by checking `sms.assistantPhoneNumbers[assistantId]` first, falling back to `sms.phoneNumber`. This allows multiple assistants to have distinct phone numbers while preserving existing behavior for single-assistant setups.
247
247
 
248
248
  The per-assistant mapping is propagated to the gateway via the config file watcher, enabling phone-number-based routing at the gateway boundary (see Gateway README).
249
249
 
@@ -271,6 +271,16 @@ The channel guardian service generates verification challenge instructions with
271
271
  - **Rebind requirement:** Creating a new guardian challenge when a binding already exists requires `rebind: true` in the IPC request. Without it, the daemon returns `already_bound`. This prevents accidental guardian replacement.
272
272
  - **Takeover prevention:** Verification is rejected when an active binding exists for a different external user. Same-user re-verification is allowed.
273
273
 
274
+ ### Vellum Guardian Identity (Actor Tokens)
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 `externalUserId` for guardian identity.
277
+
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
+ - **iOS pairing**: The pairing response includes an `actorToken` automatically when a vellum guardian binding exists.
280
+ - **IPC fallback**: Local IPC (Unix socket) connections resolve identity server-side via `resolveLocalIpcGuardianContext()` without requiring an actor token. CLI connections that pass bearer auth but lack an actor token also use this fallback (as long as the request is direct, not proxied through the gateway).
281
+ - **HTTP enforcement**: Vellum HTTP routes require either an `X-Actor-Token` header or a provably-local connection (no `X-Forwarded-For` header). Gateway-proxied requests without an actor token are rejected.
282
+ - **Startup migration**: On daemon start, `ensureVellumGuardianBinding()` backfills a vellum guardian binding for existing installations so the identity system works without requiring a manual bootstrap step.
283
+
274
284
  ## Guardian Verification and Ingress ACL
275
285
 
276
286
  This section documents the end-to-end flow from guardian verification through ingress membership enforcement, showing how the two systems work together to gate channel access.
@@ -352,18 +362,16 @@ These endpoints share the same business logic as the IPC-based verification flow
352
362
 
353
363
  ## Channel Readiness
354
364
 
355
- The `channel_readiness` IPC contract provides a unified way to check whether a channel (SMS, Telegram, etc.) is fully configured and operational. It runs local checks (credential presence, phone number assignment, ingress config) synchronously and optional remote checks (API reachability) asynchronously with a 5-minute TTL cache.
356
-
357
- ### `channel_readiness` IPC Contract
365
+ Channel readiness is exposed via HTTP control-plane endpoints that provide a unified way to check whether a channel (SMS, Telegram, etc.) is fully configured and operational. Local checks (credential presence, phone number assignment, ingress config) run synchronously; optional remote checks (API reachability) run asynchronously with a 5-minute TTL cache.
358
366
 
359
- | Action | Description |
360
- |--------|-------------|
361
- | `get` | Returns readiness snapshots for the specified channel (or all channels if omitted). Local checks always run; remote checks run only when `includeRemote=true` and cache is stale. |
362
- | `refresh` | Invalidates the cache for the specified channel (or all channels), then returns fresh snapshots. |
367
+ ### Channel Readiness HTTP Endpoints
363
368
 
364
- Request fields: `action` (required), `channel` (optional filter), `assistantId` (optional), `includeRemote` (optional boolean).
369
+ | Method | Path | Description |
370
+ |--------|------|-------------|
371
+ | GET | `/v1/channels/readiness` | Returns readiness snapshots for the specified channel (query param `channel`, optional) or all channels. Local checks always run; remote checks run only when `includeRemote=true` and cache is stale. |
372
+ | POST | `/v1/channels/readiness/refresh` | Invalidates the cache for the specified channel (or all channels), then returns fresh snapshots. Body: `{ channel?: ChannelId, includeRemote?: boolean }` |
365
373
 
366
- Response type: `channel_readiness_response` with `success`, optional `snapshots` array (each with `channel`, `ready`, `checkedAt`, `stale`, `reasons`, `localChecks`, optional `remoteChecks`), and optional `error`.
374
+ All endpoints are bearer-authenticated. Skills and clients should call the gateway URL (default `http://localhost:7830`) rather than the runtime port directly, as the gateway proxies all `/v1/channels/readiness*` routes.
367
375
 
368
376
  ### Built-in Channel Probes
369
377
 
@@ -376,7 +384,7 @@ Response type: `channel_readiness_response` with `success`, optional `snapshots`
376
384
  |------|---------|
377
385
  | `src/runtime/channel-readiness-types.ts` | Shared types: `ChannelId`, `ReadinessCheckResult`, `ChannelReadinessSnapshot`, `ChannelProbe` |
378
386
  | `src/runtime/channel-readiness-service.ts` | Service class with probe registration, cached readiness evaluation, and built-in SMS/Telegram probes |
379
- | `src/daemon/handlers/config.ts` | `handleChannelReadiness` IPC handler for `channel_readiness` messages |
387
+ | `src/runtime/routes/channel-readiness-routes.ts` | HTTP route handlers for `/v1/channels/readiness` and `/v1/channels/readiness/refresh` |
380
388
 
381
389
  ## Ingress Membership + Escalation
382
390
 
@@ -112,6 +112,26 @@ The `assistant_ingress_invites` table supports a parallel invite-based onboardin
112
112
  |-------|--------------------------------|
113
113
  | `assistant_ingress_invites` | Not used in the guardian-mediated flow. Available as an alternative for direct invite links (e.g., guardian shares a URL instead of going through the approval + verification flow). |
114
114
 
115
+ ### Voice In-Call Guardian Approval (friend-initiated)
116
+
117
+ Voice calls have a dedicated in-call guardian approval flow that differs from the text-channel flow. Since the caller is actively on the line, the voice flow captures the caller's name, creates a canonical access request, and holds the call while awaiting the guardian's decision.
118
+
119
+ **Flow:**
120
+ 1. Unknown caller dials in. `relay-server.ts` resolves trust — caller is `unknown`, no pending challenge, no active invite.
121
+ 2. Relay enters `awaiting_name` state and prompts the caller for their name (with a timeout).
122
+ 3. On name capture, `notifyGuardianOfAccessRequest` creates a canonical guardian request (`kind: 'access_request'`) and notifies the guardian.
123
+ 4. Relay transitions to `awaiting_guardian_decision` and polls `canonical_guardian_requests` for status changes.
124
+ 5. Guardian approves or denies via any channel. All decisions route through `applyCanonicalGuardianDecision`.
125
+ 6. On approval: the `access_request` resolver directly activates the caller as a trusted contact (`upsertMember` with `status: 'active'`, `policy: 'allow'`) — no verification session needed since the caller is already authenticated by their phone number.
126
+ 7. On denial or timeout: the caller hears a denial message and the call ends.
127
+
128
+ **Key difference from text-channel flow:** Voice approvals skip the verification session step because the caller's phone identity is already known from the active call. Text-channel approvals still mint a 6-digit verification code for out-of-band identity confirmation.
129
+
130
+ | Store | Table | Record |
131
+ |-------|-------|--------|
132
+ | `canonical-guardian-store.ts` | `canonical_guardian_requests` | `kind: 'access_request'`, `status: 'pending'` -> `'approved'` or `'denied'` |
133
+ | `ingress-member-store.ts` | `assistant_ingress_members` | On approval: upserted with `status: 'active'`, `policy: 'allow'` |
134
+
115
135
  ## Sequence Diagram
116
136
 
117
137
  ```mermaid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -198,6 +198,7 @@ const INT_PATTERNS = [
198
198
  /[Ii]ndex$/,
199
199
  /[Ee]xpected$/,
200
200
  /[Uu]ndos$/,
201
+ /[Vv]ersion$/,
201
202
  ];
202
203
 
203
204
  function shouldBeInt(propName: string): boolean {
@@ -668,22 +668,6 @@ exports[`IPC message snapshots ClientMessage types telegram_config serializes to
668
668
  }
669
669
  `;
670
670
 
671
- exports[`IPC message snapshots ClientMessage types twilio_config serializes to expected JSON 1`] = `
672
- {
673
- "action": "get",
674
- "type": "twilio_config",
675
- }
676
- `;
677
-
678
- exports[`IPC message snapshots ClientMessage types channel_readiness serializes to expected JSON 1`] = `
679
- {
680
- "action": "get",
681
- "channel": "sms",
682
- "includeRemote": true,
683
- "type": "channel_readiness",
684
- }
685
- `;
686
-
687
671
  exports[`IPC message snapshots ClientMessage types guardian_verification serializes to expected JSON 1`] = `
688
672
  {
689
673
  "action": "create_challenge",
@@ -1157,6 +1141,40 @@ exports[`IPC message snapshots ClientMessage types generate_avatar serializes to
1157
1141
  }
1158
1142
  `;
1159
1143
 
1144
+ exports[`IPC message snapshots ClientMessage types guardian_actions_pending_request serializes to expected JSON 1`] = `
1145
+ {
1146
+ "conversationId": "conv-guardian-001",
1147
+ "type": "guardian_actions_pending_request",
1148
+ }
1149
+ `;
1150
+
1151
+ exports[`IPC message snapshots ClientMessage types guardian_action_decision serializes to expected JSON 1`] = `
1152
+ {
1153
+ "action": "approve_once",
1154
+ "conversationId": "conv-guardian-001",
1155
+ "requestId": "req-guardian-001",
1156
+ "type": "guardian_action_decision",
1157
+ }
1158
+ `;
1159
+
1160
+ exports[`IPC message snapshots ClientMessage types reorder_threads serializes to expected JSON 1`] = `
1161
+ {
1162
+ "type": "reorder_threads",
1163
+ "updates": [
1164
+ {
1165
+ "displayOrder": 0,
1166
+ "isPinned": false,
1167
+ "sessionId": "sess-001",
1168
+ },
1169
+ {
1170
+ "displayOrder": 1,
1171
+ "isPinned": true,
1172
+ "sessionId": "sess-002",
1173
+ },
1174
+ ],
1175
+ }
1176
+ `;
1177
+
1160
1178
  exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
1161
1179
  {
1162
1180
  "success": true,
@@ -1288,6 +1306,30 @@ exports[`IPC message snapshots ServerMessage types confirmation_request serializ
1288
1306
  }
1289
1307
  `;
1290
1308
 
1309
+ exports[`IPC message snapshots ServerMessage types confirmation_state_changed serializes to expected JSON 1`] = `
1310
+ {
1311
+ "causedByRequestId": "req-003",
1312
+ "decisionText": "approve",
1313
+ "requestId": "req-002",
1314
+ "sessionId": "sess-001",
1315
+ "source": "inline_nl",
1316
+ "state": "approved",
1317
+ "type": "confirmation_state_changed",
1318
+ }
1319
+ `;
1320
+
1321
+ exports[`IPC message snapshots ServerMessage types assistant_activity_state serializes to expected JSON 1`] = `
1322
+ {
1323
+ "activityVersion": 1,
1324
+ "anchor": "assistant_turn",
1325
+ "phase": "thinking",
1326
+ "reason": "message_dequeued",
1327
+ "requestId": "req-003",
1328
+ "sessionId": "sess-001",
1329
+ "type": "assistant_activity_state",
1330
+ }
1331
+ `;
1332
+
1291
1333
  exports[`IPC message snapshots ServerMessage types message_complete serializes to expected JSON 1`] = `
1292
1334
  {
1293
1335
  "attachments": [
@@ -2269,76 +2311,6 @@ exports[`IPC message snapshots ServerMessage types telegram_config_response seri
2269
2311
  }
2270
2312
  `;
2271
2313
 
2272
- exports[`IPC message snapshots ServerMessage types twilio_config_response serializes to expected JSON 1`] = `
2273
- {
2274
- "compliance": {
2275
- "numberType": "toll_free",
2276
- "verificationSid": "TF_VER_001",
2277
- "verificationStatus": "TWILIO_APPROVED",
2278
- },
2279
- "diagnostics": {
2280
- "actionItems": [],
2281
- "compliance": {
2282
- "detail": "Toll-free verification: TWILIO_APPROVED",
2283
- "status": "TWILIO_APPROVED",
2284
- },
2285
- "overallStatus": "healthy",
2286
- "readiness": {
2287
- "issues": [],
2288
- "ready": true,
2289
- },
2290
- },
2291
- "hasCredentials": true,
2292
- "phoneNumber": "+15551234567",
2293
- "success": true,
2294
- "testResult": {
2295
- "finalStatus": "delivered",
2296
- "initialStatus": "queued",
2297
- "messageSid": "SM-test-001",
2298
- "to": "+15559876543",
2299
- },
2300
- "type": "twilio_config_response",
2301
- }
2302
- `;
2303
-
2304
- exports[`IPC message snapshots ServerMessage types channel_readiness_response serializes to expected JSON 1`] = `
2305
- {
2306
- "snapshots": [
2307
- {
2308
- "channel": "sms",
2309
- "checkedAt": 1700000000000,
2310
- "localChecks": [
2311
- {
2312
- "message": "Twilio credentials are not configured",
2313
- "name": "twilio_credentials",
2314
- "passed": false,
2315
- },
2316
- {
2317
- "message": "Phone number is assigned",
2318
- "name": "phone_number",
2319
- "passed": true,
2320
- },
2321
- {
2322
- "message": "Public ingress URL is configured",
2323
- "name": "ingress",
2324
- "passed": true,
2325
- },
2326
- ],
2327
- "ready": false,
2328
- "reasons": [
2329
- {
2330
- "code": "twilio_credentials",
2331
- "text": "Twilio credentials are not configured",
2332
- },
2333
- ],
2334
- "stale": false,
2335
- },
2336
- ],
2337
- "success": true,
2338
- "type": "channel_readiness_response",
2339
- }
2340
- `;
2341
-
2342
2314
  exports[`IPC message snapshots ServerMessage types guardian_verification_response serializes to expected JSON 1`] = `
2343
2315
  {
2344
2316
  "instruction": "Send this code to the Telegram bot",
@@ -3122,40 +3094,6 @@ exports[`IPC message snapshots ServerMessage types generate_avatar_response seri
3122
3094
  }
3123
3095
  `;
3124
3096
 
3125
- exports[`IPC message snapshots ClientMessage types guardian_actions_pending_request serializes to expected JSON 1`] = `
3126
- {
3127
- "conversationId": "conv-guardian-001",
3128
- "type": "guardian_actions_pending_request",
3129
- }
3130
- `;
3131
-
3132
- exports[`IPC message snapshots ClientMessage types guardian_action_decision serializes to expected JSON 1`] = `
3133
- {
3134
- "action": "approve_once",
3135
- "conversationId": "conv-guardian-001",
3136
- "requestId": "req-guardian-001",
3137
- "type": "guardian_action_decision",
3138
- }
3139
- `;
3140
-
3141
- exports[`IPC message snapshots ClientMessage types reorder_threads serializes to expected JSON 1`] = `
3142
- {
3143
- "type": "reorder_threads",
3144
- "updates": [
3145
- {
3146
- "displayOrder": 0,
3147
- "isPinned": false,
3148
- "sessionId": "sess-001",
3149
- },
3150
- {
3151
- "displayOrder": 1,
3152
- "isPinned": true,
3153
- "sessionId": "sess-002",
3154
- },
3155
- ],
3156
- }
3157
- `;
3158
-
3159
3097
  exports[`IPC message snapshots ServerMessage types guardian_actions_pending_response serializes to expected JSON 1`] = `
3160
3098
  {
3161
3099
  "conversationId": "conv-guardian-001",
@@ -31,7 +31,6 @@ mock.module('../util/platform.js', () => ({
31
31
  getDbPath: () => join(testDir, 'test.db'),
32
32
  getLogPath: () => join(testDir, 'test.log'),
33
33
  ensureDataDir: () => {},
34
- normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
35
34
  readHttpToken: () => 'test-bearer-token',
36
35
  }));
37
36