@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
package/ARCHITECTURE.md CHANGED
@@ -433,31 +433,33 @@ External users who are not the guardian can gain access to the assistant through
433
433
 
434
434
  **Notification signals:** The flow emits signals at each lifecycle transition via `emitNotificationSignal()`:
435
435
 
436
- - `ingress.access_request` — non-member denied, guardian notified
436
+ - `ingress.access_request` — unknown contact denied, guardian notified
437
437
  - `ingress.trusted_contact.guardian_decision` — guardian approved or denied
438
438
  - `ingress.trusted_contact.verification_sent` — code created and delivered
439
- - `ingress.trusted_contact.activated` — requester verified, member active
439
+ - `ingress.trusted_contact.activated` — requester verified, contact active
440
440
  - `ingress.trusted_contact.denied` — guardian explicitly denied
441
441
 
442
442
  **HTTP API (for management):**
443
443
 
444
- | Endpoint | Method | Description |
445
- | ------------------------------- | ------ | ------------------------------------------------------------- |
446
- | `/v1/ingress/members` | GET | List trusted contacts (filterable by channel, status, policy) |
447
- | `/v1/ingress/members` | POST | Upsert a member (add/update trusted contact) |
448
- | `/v1/ingress/members/:id` | DELETE | Revoke a trusted contact |
449
- | `/v1/ingress/members/:id/block` | POST | Block a member |
444
+ | Endpoint | Method | Description |
445
+ | --------------------------- | ------ | ---------------------------------------------------------------- |
446
+ | `/v1/contacts` | GET | List contacts (filterable by role, search by query/channel/etc.) |
447
+ | `/v1/contacts` | POST | Create or update a contact |
448
+ | `/v1/contacts/:id` | GET | Get a contact by ID |
449
+ | `/v1/contacts/merge` | POST | Merge two contacts |
450
+ | `/v1/contacts/channels/:id` | PATCH | Update a contact channel's status/policy |
450
451
 
451
452
  **Key source files:**
452
453
 
453
454
  | File | Purpose |
454
455
  | ------------------------------------------------------ | ----------------------------------------------------------------------------- |
455
- | `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL, non-member rejection, verification code interception |
456
+ | `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL, unknown-contact rejection, verification code interception |
456
457
  | `src/runtime/routes/access-request-decision.ts` | Guardian decision → verification session creation |
457
458
  | `src/runtime/routes/guardian-approval-interception.ts` | Routes guardian decisions (button + conversational) to access request handler |
458
459
  | `src/runtime/channel-guardian-service.ts` | Verification challenge lifecycle, identity binding, rate limiting |
459
- | `src/runtime/routes/ingress-routes.ts` | HTTP API handlers for member/invite management |
460
- | `src/runtime/ingress-service.ts` | Business logic for member CRUD |
460
+ | `src/runtime/routes/contact-routes.ts` | HTTP API handlers for contact and channel management |
461
+ | `src/runtime/routes/invite-routes.ts` | HTTP API handlers for invite management |
462
+ | `src/runtime/invite-service.ts` | Business logic for invite operations |
461
463
  | `src/contacts/contact-store.ts` | Contact read queries — lookup, search, list, and channel operations |
462
464
  | `src/memory/channel-guardian-store.ts` | Approval request and verification challenge persistence |
463
465
  | `src/config/bundled-skills/contacts/SKILL.md` | Unified skill for contact management, access control, and invite links |
@@ -482,7 +484,7 @@ A complementary access-granting flow where the guardian proactively creates a sh
482
484
  ├─────────────────────────────────────────────────────────────┤
483
485
  │ Core Redemption Engine (invite-redemption-service.ts) │
484
486
  │ Channel-agnostic token validation, expiry, use-count, │
485
- │ channel-match enforcement, member creation/reactivation
487
+ │ channel-match enforcement, contact activation/reactivation
486
488
  │ Returns: InviteRedemptionOutcome (discriminated union) │
487
489
  │ Reply templates: invite-redemption-templates.ts │
488
490
  └─────────────────────────────────────────────────────────────┘
@@ -496,12 +498,12 @@ A complementary access-granting flow where the guardian proactively creates a sh
496
498
  4. Guardian shares the link with the invitee out-of-band.
497
499
  5. Invitee clicks the link, opening Telegram which sends `/start iv_<token>` to the bot.
498
500
  6. The gateway forwards the message to `/channels/inbound`. The inbound handler calls `getTransport('telegram').extractInboundToken()` to parse the `iv_` token.
499
- 7. The token is redeemed via `invite-redemption-service.ts`, which validates, creates an active member record, and returns a `redeemed` outcome.
501
+ 7. The token is redeemed via `invite-redemption-service.ts`, which validates, activates the contact, and returns a `redeemed` outcome.
500
502
  8. A deterministic welcome message is delivered to the invitee (bypasses the LLM pipeline).
501
503
 
502
504
  **Token prefix convention:** The `iv_` prefix distinguishes invite tokens from `gv_` (guardian verification) tokens. Both use the same Telegram `/start` deep-link mechanism but are routed to different handlers.
503
505
 
504
- **Inbound intercept points:** Invite token extraction runs early in the inbound handler, before ACL denial, so valid invites short-circuit the membership check. Two intercept branches handle: (a) non-members — the invite creates their first member record; (b) inactive members (revoked/pending) — the invite reactivates them.
506
+ **Inbound intercept points:** Invite token extraction runs early in the inbound handler, before ACL denial, so valid invites short-circuit the contact check. Two intercept branches handle: (a) unknown contacts — the invite creates their first contact record; (b) inactive contacts (revoked/pending) — the invite reactivates them.
505
507
 
506
508
  **Channel adapter status:**
507
509
 
@@ -518,8 +520,8 @@ Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL
518
520
 
519
521
  **Creation flow:**
520
522
 
521
- 1. Guardian creates a voice invite via `POST /v1/ingress/invites` with `sourceChannel: "voice"` and `expectedExternalUserId` (E.164 phone).
522
- 2. `ingress-service.ts` generates a cryptographically random numeric code (`generateVoiceCode`), hashes it with SHA-256 (`hashVoiceCode`), and stores only the hash.
523
+ 1. Guardian creates a voice invite via `POST /v1/contacts/invites` with `sourceChannel: "voice"` and `expectedExternalUserId` (E.164 phone).
524
+ 2. `invite-service.ts` generates a cryptographically random numeric code (`generateVoiceCode`), hashes it with SHA-256 (`hashVoiceCode`), and stores only the hash.
523
525
  3. The one-time plaintext `voiceCode` is returned in the creation response. The raw token is NOT returned for voice invites — redemption uses the identity-bound code flow exclusively.
524
526
  4. Guardian communicates the code to the invitee out-of-band.
525
527
 
@@ -528,7 +530,7 @@ Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL
528
530
  1. Unknown caller dials in. `relay-server.ts` resolves trust via `resolveActorTrust`. Caller is `unknown`, no pending guardian challenge.
529
531
  2. The relay checks `findActiveVoiceInvites` for invites bound to the caller's phone number.
530
532
  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.`
531
- 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.
533
+ 4. `redeemVoiceInviteCode` validates: identity match, code hash match, expiry, use count. On success, the contact is activated and the call transitions to the normal call flow.
532
534
  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.
533
535
 
534
536
  **Security invariants:**
@@ -536,24 +538,24 @@ Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL
536
538
  - The plaintext voice code is returned exactly once at creation time and never stored.
537
539
  - Voice invites are identity-bound: `expectedExternalUserId` must match the caller's E.164 number. An attacker with the code but the wrong phone number cannot redeem.
538
540
  - Failure responses are intentionally generic (`invalid_or_expired`) to prevent oracle attacks.
539
- - Blocked members cannot bypass the guardian's explicit block via invite redemption.
541
+ - Blocked contacts cannot bypass the guardian's explicit block via invite redemption.
540
542
 
541
543
  **Key source files:**
542
544
 
543
- | File | Purpose |
544
- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
545
- | `src/runtime/invite-redemption-service.ts` | Core redemption engine — token validation, voice code redemption, member creation, discriminated-union outcomes |
546
- | `src/runtime/invite-redemption-templates.ts` | Deterministic reply templates for each redemption outcome |
547
- | `src/runtime/channel-invite-transport.ts` | Transport adapter registry with `buildShareableInvite` / `extractInboundToken` interface |
548
- | `src/runtime/channel-invite-transports/telegram.ts` | Telegram adapter — `t.me/<bot>?start=iv_<token>` deep links, `/start iv_<token>` extraction |
549
- | `src/runtime/channel-invite-transports/voice.ts` | Voice transport adapter — code-based redemption metadata |
550
- | `src/daemon/guardian-invite-intent.ts` | Intent detection — routes create/list/revoke requests into the contacts skill |
551
- | `src/runtime/ingress-service.ts` | Shared business logic for invite/member operations (used by both HTTP routes and IPC) |
552
- | `src/runtime/routes/ingress-routes.ts` | HTTP API handlers for member/invite management including voice invite creation and redemption |
553
- | `src/runtime/routes/inbound-message-handler.ts` | Invite token intercept in the inbound flow (non-member and inactive-member branches) |
554
- | `src/calls/relay-server.ts` | Voice relay state machine — `invite_redemption_pending` subflow (always-on canonical behavior) |
555
- | `src/util/voice-code.ts` | Cryptographic voice code generation and SHA-256 hashing |
556
- | `src/memory/ingress-invite-store.ts` | Invite persistence including `findActiveVoiceInvites` for identity-bound lookup |
545
+ | File | Purpose |
546
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
547
+ | `src/runtime/invite-redemption-service.ts` | Core redemption engine — token validation, voice code redemption, contact activation, discriminated-union outcomes |
548
+ | `src/runtime/invite-redemption-templates.ts` | Deterministic reply templates for each redemption outcome |
549
+ | `src/runtime/channel-invite-transport.ts` | Transport adapter registry with `buildShareableInvite` / `extractInboundToken` interface |
550
+ | `src/runtime/channel-invite-transports/telegram.ts` | Telegram adapter — `t.me/<bot>?start=iv_<token>` deep links, `/start iv_<token>` extraction |
551
+ | `src/runtime/channel-invite-transports/voice.ts` | Voice transport adapter — code-based redemption metadata |
552
+ | `src/daemon/guardian-invite-intent.ts` | Intent detection — routes create/list/revoke requests into the contacts skill |
553
+ | `src/runtime/invite-service.ts` | Shared business logic for invite operations (used by both HTTP routes and IPC) |
554
+ | `src/runtime/routes/invite-routes.ts` | HTTP API handlers for invite management including voice invite creation and redemption |
555
+ | `src/runtime/routes/inbound-message-handler.ts` | Invite token intercept in the inbound flow (unknown-contact and inactive-contact branches) |
556
+ | `src/calls/relay-server.ts` | Voice relay state machine — `invite_redemption_pending` subflow (always-on canonical behavior) |
557
+ | `src/util/voice-code.ts` | Cryptographic voice code generation and SHA-256 hashing |
558
+ | `src/memory/invite-store.ts` | Invite persistence including `findActiveVoiceInvites` for identity-bound lookup |
557
559
 
558
560
  ### Voice Inbound Security Model (Canonical)
559
561
 
@@ -595,7 +597,7 @@ When no invite exists and no pending guardian challenge is active, the relay ent
595
597
  2. On name capture, `notifyGuardianOfAccessRequest` creates a canonical guardian request (`kind: 'access_request'`) and notifies the guardian via the notification pipeline.
596
598
  3. The relay transitions to `awaiting_guardian_decision` and plays hold music/messaging while polling the canonical request status.
597
599
  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`.
598
- 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.
600
+ 5. On approval: the resolver directly activates the caller as a trusted contact (sets channel `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.
599
601
  6. On denial or timeout: the caller hears a denial message and the call ends.
600
602
 
601
603
  **Path 3: Inbound guardian verification (pending challenge)**
@@ -888,7 +890,7 @@ graph LR
888
890
  C12["tool_permission_simulate<br/>toolName, input, workingDir?,<br/>isInteractive?, forcePromptSideEffects?,<br/>executionTarget?"]
889
891
  C13["conversation_search<br/>query, limit?,<br/>maxMessagesPerConversation?"]
890
892
  C14["ingress_invite<br/>create / list / revoke / redeem"]
891
- C15["ingress_member<br/>list / upsert / revoke / block"]
893
+ C15["contacts<br/>list / get / update_channel"]
892
894
  end
893
895
 
894
896
  SOCKET["Unix Socket<br/>~/.vellum/vellum.sock<br/>───────────────<br/>Newline-delimited JSON<br/>Max 96MB per message<br/>Ping/pong every 30s<br/>Auto-reconnect<br/>1s → 30s backoff"]
@@ -920,7 +922,7 @@ graph LR
920
922
  S22["tool_permission_simulate_response<br/>decision, riskLevel, reason?,<br/>promptPayload?, matchedRuleId?"]
921
923
  S23["conversation_search_response<br/>query, results[]: conversationId,<br/>title, updatedAt, matchingMessages[]"]
922
924
  S24["ingress_invite_response<br/>invite / invites"]
923
- S25["ingress_member_response<br/>member / members"]
925
+ S25["contacts_response<br/>contact / contacts"]
924
926
  end
925
927
 
926
928
  C0 --> SOCKET
@@ -2204,8 +2206,8 @@ Connected channels are resolved at signal emission time: vellum is always includ
2204
2206
  | Guardian bindings | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; revoked bindings retained |
2205
2207
  | Guardian verification challenges | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; consumed/expired challenges retained |
2206
2208
  | Guardian approval requests | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; decision outcome retained |
2207
- | Ingress invites | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; token hash stored, raw token never persisted |
2208
- | Ingress members | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; revoked/blocked members retained |
2209
+ | Contact invites | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; token hash stored, raw token never persisted |
2210
+ | Contacts & channels | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; revoked/blocked contacts retained |
2209
2211
  | Notification events | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; deduplicated by dedupeKey |
2210
2212
  | Notification decisions | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; FK to notification_events |
2211
2213
  | Notification deliveries | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent; FK to notification_decisions |
package/README.md CHANGED
@@ -337,7 +337,7 @@ Guardian verification and ingress contact management are complementary but indep
337
337
  | `src/runtime/trust-context-resolver.ts` | Actor role classification: guardian / non-guardian / unverified_channel |
338
338
  | `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL enforcement, verification-code intercept, escalation creation |
339
339
  | `src/contacts/contact-store.ts` | Contact + channel CRUD: `findContactChannel`, `upsertContact`, `updateChannelStatus`, `searchContacts` |
340
- | `src/memory/ingress-invite-store.ts` | Invite lifecycle: `createInvite`, `redeemInvite` (atomically creates member record) |
340
+ | `src/memory/invite-store.ts` | Invite lifecycle: `createInvite`, `redeemInvite` (atomically creates member record) |
341
341
  | `src/memory/channel-guardian-store.ts` | Persistence for guardian bindings, verification challenges, and approval requests |
342
342
  | `src/runtime/guardian-outbound-actions.ts` | Shared business logic for outbound verification (start/resend/cancel) |
343
343
  | `src/runtime/routes/integration-routes.ts` | HTTP route handlers for outbound guardian verification endpoints |
@@ -432,7 +432,7 @@ Redemption auto-creates a **member** record with an access policy:
432
432
  - **`deny`** — Messages are rejected with a refusal notice.
433
433
  - **`escalate`** — Messages are held for guardian (owner) approval before processing.
434
434
 
435
- Non-members (senders with no invite redemption) are denied by default. Members can be listed, updated, revoked, or blocked via the `ingress_member` IPC contract.
435
+ Non-members (senders with no invite redemption) are denied by default. Contacts can be listed, updated, revoked, or blocked via the HTTP API (`/v1/contacts` and `/v1/contacts/channels`).
436
436
 
437
437
  ### Escalation Flow
438
438
 
@@ -447,15 +447,14 @@ If no guardian binding exists, escalation fails closed — the message is denied
447
447
  | Message Type | Actions | Description |
448
448
  | ---------------- | ---------------------------- | ------------------------------------------------------------------------ |
449
449
  | `ingress_invite` | create, list, revoke, redeem | Manage invite tokens (SHA-256 hashed, raw token returned once on create) |
450
- | `ingress_member` | list, upsert, revoke, block | Manage member records and access policies |
451
450
 
452
451
  ### Key Modules
453
452
 
454
453
  | File | Purpose |
455
454
  | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
456
- | `src/memory/ingress-invite-store.ts` | CRUD for invite tokens with SHA-256 hashing and expiry |
455
+ | `src/memory/invite-store.ts` | CRUD for invite tokens with SHA-256 hashing and expiry |
457
456
  | `src/contacts/contact-store.ts` | Contact + channel CRUD with policy enforcement |
458
- | `src/daemon/handlers/config-inbox.ts` | IPC handlers for ingress invite and member contracts |
457
+ | `src/daemon/handlers/config-inbox.ts` | IPC handlers for invite contract |
459
458
  | `src/daemon/ipc-contract/inbox.ts` | TypeScript type definitions for ingress IPC messages |
460
459
  | `src/runtime/routes/channel-routes.ts` | ACL enforcement point — member lookup, policy check, escalation creation |
461
460
  | `src/runtime/invite-redemption-service.ts` | Core redemption engine — token validation, member creation, discriminated-union outcomes |
@@ -463,7 +462,7 @@ If no guardian binding exists, escalation fails closed — the message is denied
463
462
  | `src/runtime/channel-invite-transport.ts` | Transport adapter registry — `buildShareableInvite` / `extractInboundToken` per channel |
464
463
  | `src/runtime/channel-invite-transports/telegram.ts` | Telegram adapter — builds `t.me/<bot>?start=iv_<token>` deep links, extracts `iv_` tokens from `/start` commands |
465
464
  | `src/daemon/guardian-invite-intent.ts` | Intent detection — routes guardian invite management requests into the `contacts` skill |
466
- | `src/runtime/ingress-service.ts` | Shared business logic for invite/member operations (HTTP + IPC) |
465
+ | `src/runtime/invite-service.ts` | Shared business logic for invite and contact operations (HTTP + IPC) |
467
466
 
468
467
  ## Database
469
468
 
@@ -1,12 +1,17 @@
1
1
  # Trusted Contacts — Operator Runbook
2
2
 
3
- Operational procedures for inspecting, managing, and debugging the trusted contact access flow. All HTTP commands use the gateway API (default `http://localhost:7830`) with bearer authentication.
3
+ Operational procedures for inspecting, managing, and debugging the trusted contact access flow. HTTP commands use the assistant runtime API (default `http://localhost:7821`) with bearer authentication.
4
+
5
+ > **Note:** The `/v1/contacts` endpoints are served by the assistant runtime, not
6
+ > the gateway. If you prefer to route through the gateway (`localhost:7830`), set
7
+ > `GATEWAY_RUNTIME_PROXY_ENABLED=true` in the gateway environment — the proxy is
8
+ > disabled by default and these routes will 404 without it.
4
9
 
5
10
  ## Prerequisites
6
11
 
7
12
  ```bash
8
- # Base URL (adjust if using a non-default port)
9
- BASE=http://localhost:7830
13
+ # Base URL — assistant runtime (adjust if using a non-default port)
14
+ BASE=http://localhost:7821
10
15
 
11
16
  # Bearer token: if running via the assistant's shell tools, $GATEWAY_AUTH_TOKEN
12
17
  # is injected automatically. For manual operator use, mint a token via the CLI
@@ -14,51 +19,74 @@ BASE=http://localhost:7830
14
19
  TOKEN=$GATEWAY_AUTH_TOKEN
15
20
  ```
16
21
 
17
- ## 1. Inspect Trusted Contacts (Members)
22
+ ## 1. Inspect Trusted Contacts
18
23
 
19
24
  ### List all active trusted contacts
20
25
 
21
26
  ```bash
22
- curl -s "$BASE/v1/ingress/members?status=active" \
27
+ curl -s "$BASE/v1/contacts?role=contact" \
23
28
  -H "Authorization: Bearer $TOKEN" | jq
24
29
  ```
25
30
 
26
- ### Filter by channel
31
+ ### Filter by channel type
27
32
 
28
33
  ```bash
29
34
  # Telegram contacts only
30
- curl -s "$BASE/v1/ingress/members?sourceChannel=telegram&status=active" \
35
+ curl -s "$BASE/v1/contacts?channelType=telegram" \
31
36
  -H "Authorization: Bearer $TOKEN" | jq
32
37
 
33
38
  # SMS contacts only
34
- curl -s "$BASE/v1/ingress/members?sourceChannel=sms&status=active" \
39
+ curl -s "$BASE/v1/contacts?channelType=sms" \
35
40
  -H "Authorization: Bearer $TOKEN" | jq
36
41
  ```
37
42
 
38
- ### List all members (including revoked and blocked)
43
+ ### List all contacts (including revoked and blocked)
39
44
 
40
45
  ```bash
41
- curl -s "$BASE/v1/ingress/members" \
46
+ curl -s "$BASE/v1/contacts" \
42
47
  -H "Authorization: Bearer $TOKEN" | jq
43
48
  ```
44
49
 
50
+ ### Via CLI
51
+
52
+ ```bash
53
+ vellum contacts list --role contact
54
+ ```
55
+
45
56
  Response shape:
46
57
 
47
58
  ```json
48
59
  {
49
60
  "ok": true,
50
- "members": [
61
+ "contacts": [
51
62
  {
52
63
  "id": "uuid",
53
- "sourceChannel": "telegram",
54
- "externalUserId": "123456789",
55
- "externalChatId": "123456789",
56
64
  "displayName": "Alice",
57
- "username": "alice_handle",
58
- "status": "active",
59
- "policy": "allow",
60
- "lastSeenAt": 1700000000000,
61
- "createdAt": 1699000000000
65
+ "relationship": "friend",
66
+ "importance": 0.5,
67
+ "responseExpectation": null,
68
+ "preferredTone": null,
69
+ "lastInteraction": 1700000000000,
70
+ "interactionCount": 12,
71
+ "createdAt": 1699000000000,
72
+ "updatedAt": 1700000000000,
73
+ "role": "contact",
74
+ "channels": [
75
+ {
76
+ "id": "channel-uuid",
77
+ "contactId": "uuid",
78
+ "type": "telegram",
79
+ "address": "alice_handle",
80
+ "isPrimary": true,
81
+ "externalUserId": "123456789",
82
+ "externalChatId": "123456789",
83
+ "status": "active",
84
+ "policy": "allow",
85
+ "verifiedAt": 1699500000000,
86
+ "lastSeenAt": 1700000000000,
87
+ "createdAt": 1699000000000
88
+ }
89
+ ]
62
90
  }
63
91
  ]
64
92
  }
@@ -109,29 +137,29 @@ sqlite3 ~/.vellum/workspace/data/db/assistant.db \
109
137
 
110
138
  ### Via HTTP API
111
139
 
112
- First, find the member's `id` from the list endpoint, then revoke:
140
+ First, find the contact and its channel ID from the list endpoint, then revoke the channel:
113
141
 
114
142
  ```bash
115
- # Find the member
116
- MEMBER_ID=$(curl -s "$BASE/v1/ingress/members?sourceChannel=telegram&status=active" \
117
- -H "Authorization: Bearer $TOKEN" | jq -r '.members[] | select(.externalUserId == "TARGET_USER_ID") | .id')
143
+ # Find the contact's channel ID
144
+ CHANNEL_ID=$(curl -s "$BASE/v1/contacts?channelType=telegram" \
145
+ -H "Authorization: Bearer $TOKEN" | jq -r '.contacts[] | select(.channels[] | select(.externalUserId == "TARGET_USER_ID")) | .channels[] | select(.externalUserId == "TARGET_USER_ID") | .id')
118
146
 
119
147
  # Revoke with reason
120
- curl -s -X DELETE "$BASE/v1/ingress/members/$MEMBER_ID" \
148
+ curl -s -X PATCH "$BASE/v1/contacts/channels/$CHANNEL_ID" \
121
149
  -H "Authorization: Bearer $TOKEN" \
122
150
  -H "Content-Type: application/json" \
123
- -d '{"reason": "Revoked by operator"}' | jq
151
+ -d '{"status": "revoked", "reason": "Revoked by operator"}' | jq
124
152
  ```
125
153
 
126
- ### Block a member (stronger than revoke)
154
+ ### Block a contact channel (stronger than revoke)
127
155
 
128
- Blocking prevents the member from re-entering the flow without explicit unblocking.
156
+ Blocking prevents the contact from re-entering the flow without explicit unblocking.
129
157
 
130
158
  ```bash
131
- curl -s -X POST "$BASE/v1/ingress/members/$MEMBER_ID/block" \
159
+ curl -s -X PATCH "$BASE/v1/contacts/channels/$CHANNEL_ID" \
132
160
  -H "Authorization: Bearer $TOKEN" \
133
161
  -H "Content-Type: application/json" \
134
- -d '{"reason": "Blocked by operator"}' | jq
162
+ -d '{"status": "blocked", "reason": "Blocked by operator"}' | jq
135
163
  ```
136
164
 
137
165
  ### Via SQLite (emergency)
@@ -229,35 +257,43 @@ sqlite3 ~/.vellum/workspace/data/db/assistant.db \
229
257
 
230
258
  ## 7. Manually Add a Trusted Contact (Bypass Verification)
231
259
 
232
- If the verification flow cannot be completed, an operator can directly create an active member:
260
+ If the verification flow cannot be completed, an operator can directly create an active contact:
233
261
 
234
262
  ```bash
235
- curl -s -X POST "$BASE/v1/ingress/members" \
263
+ curl -s -X POST "$BASE/v1/contacts" \
236
264
  -H "Authorization: Bearer $TOKEN" \
237
265
  -H "Content-Type: application/json" \
238
266
  -d '{
239
- "sourceChannel": "telegram",
240
- "externalUserId": "123456789",
241
- "externalChatId": "123456789",
242
267
  "displayName": "Alice",
243
- "policy": "allow",
244
- "status": "active"
268
+ "role": "contact",
269
+ "channels": [{
270
+ "type": "telegram",
271
+ "address": "alice_handle",
272
+ "externalUserId": "123456789",
273
+ "externalChatId": "123456789",
274
+ "status": "active",
275
+ "policy": "allow"
276
+ }]
245
277
  }' | jq
246
278
  ```
247
279
 
248
- For SMS contacts, use the E.164 phone number as the external user/chat ID:
280
+ For SMS contacts, use the E.164 phone number as the address and external user/chat ID:
249
281
 
250
282
  ```bash
251
- curl -s -X POST "$BASE/v1/ingress/members" \
283
+ curl -s -X POST "$BASE/v1/contacts" \
252
284
  -H "Authorization: Bearer $TOKEN" \
253
285
  -H "Content-Type: application/json" \
254
286
  -d '{
255
- "sourceChannel": "sms",
256
- "externalUserId": "+15551234567",
257
- "externalChatId": "+15551234567",
258
287
  "displayName": "Bob",
259
- "policy": "allow",
260
- "status": "active"
288
+ "role": "contact",
289
+ "channels": [{
290
+ "type": "sms",
291
+ "address": "+15551234567",
292
+ "externalUserId": "+15551234567",
293
+ "externalChatId": "+15551234567",
294
+ "status": "active",
295
+ "policy": "allow"
296
+ }]
261
297
  }' | jq
262
298
  ```
263
299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.29",
3
+ "version": "0.4.30",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -49,9 +49,8 @@ const SWIFT_OMIT_ALLOWLIST = new Set<string>([
49
49
  "heartbeat_alert",
50
50
  // Guardian verification — daemon-internal for Telegram channel setup
51
51
  "guardian_verification_response",
52
- // Ingress invite/member management — not yet consumed by the macOS client
53
- "ingress_invite_response",
54
- "ingress_member_response",
52
+ // Contacts invite management — not yet consumed by the macOS client
53
+ "contacts_invite_response",
55
54
  // Inbox escalation — not yet consumed by the macOS client
56
55
  "assistant_inbox_escalation_response",
57
56
  // Work item messages — not yet consumed by the macOS client
package/scripts/test.sh CHANGED
@@ -166,7 +166,7 @@ printf '%s\n' "${test_files[@]}" | xargs -P "${WORKERS}" -I {} bash -c '
166
166
  if grep -q "^(fail)" "${out_file}" 2>/dev/null; then
167
167
  echo "${test_file}" >> "${results_dir}/failures"
168
168
  echo " ✗ ${base} (killed after ${per_test_timeout}s — tests failed and process hung)"
169
- elif grep -qE "^Ran [0-9]+ tests across" "${out_file}" 2>/dev/null; then
169
+ elif grep -qE "^Ran [0-9]+ tests? across" "${out_file}" 2>/dev/null; then
170
170
  echo " ⚠ ${base} (tests passed but process hung after ${per_test_timeout}s — likely open handles)"
171
171
  else
172
172
  echo "${test_file}" >> "${results_dir}/failures"
@@ -967,28 +967,14 @@ exports[`IPC message snapshots ClientMessage types dictation_request serializes
967
967
  }
968
968
  `;
969
969
 
970
- exports[`IPC message snapshots ClientMessage types ingress_invite serializes to expected JSON 1`] = `
970
+ exports[`IPC message snapshots ClientMessage types contacts_invite serializes to expected JSON 1`] = `
971
971
  {
972
972
  "action": "create",
973
973
  "expiresInMs": 86400000,
974
974
  "maxUses": 5,
975
975
  "note": "Test invite",
976
976
  "sourceChannel": "telegram",
977
- "type": "ingress_invite",
978
- }
979
- `;
980
-
981
- exports[`IPC message snapshots ClientMessage types ingress_member serializes to expected JSON 1`] = `
982
- {
983
- "action": "upsert",
984
- "displayName": "Test User",
985
- "externalChatId": "chat-456",
986
- "externalUserId": "user-123",
987
- "policy": "allow",
988
- "sourceChannel": "telegram",
989
- "status": "active",
990
- "type": "ingress_member",
991
- "username": "testuser",
977
+ "type": "contacts_invite",
992
978
  }
993
979
  `;
994
980
 
@@ -2844,7 +2830,7 @@ exports[`IPC message snapshots ServerMessage types dictation_response serializes
2844
2830
  }
2845
2831
  `;
2846
2832
 
2847
- exports[`IPC message snapshots ServerMessage types ingress_invite_response serializes to expected JSON 1`] = `
2833
+ exports[`IPC message snapshots ServerMessage types contacts_invite_response serializes to expected JSON 1`] = `
2848
2834
  {
2849
2835
  "invite": {
2850
2836
  "createdAt": 1700000000,
@@ -2859,26 +2845,7 @@ exports[`IPC message snapshots ServerMessage types ingress_invite_response seria
2859
2845
  "useCount": 0,
2860
2846
  },
2861
2847
  "success": true,
2862
- "type": "ingress_invite_response",
2863
- }
2864
- `;
2865
-
2866
- exports[`IPC message snapshots ServerMessage types ingress_member_response serializes to expected JSON 1`] = `
2867
- {
2868
- "member": {
2869
- "createdAt": 1700000000,
2870
- "displayName": "Test User",
2871
- "externalChatId": "chat-456",
2872
- "externalUserId": "user-123",
2873
- "id": "mem-001",
2874
- "lastSeenAt": 1700000000,
2875
- "policy": "allow",
2876
- "sourceChannel": "telegram",
2877
- "status": "active",
2878
- "username": "testuser",
2879
- },
2880
- "success": true,
2881
- "type": "ingress_member_response",
2848
+ "type": "contacts_invite_response",
2882
2849
  }
2883
2850
  `;
2884
2851
 
@@ -449,14 +449,15 @@ describe("resolveLocalIpcAuthContext", () => {
449
449
  );
450
450
  });
451
451
 
452
- test("actorPrincipalId is undefined when no vellum binding exists", () => {
452
+ test("actorPrincipalId is auto-created via self-heal when no vellum binding exists", () => {
453
453
  // Reset DB to ensure no binding
454
454
  resetDb();
455
455
  initializeDb();
456
456
 
457
457
  const ctx = resolveLocalIpcAuthContext("session-123");
458
- // When no binding exists, actorPrincipalId is not set
459
- expect(ctx.actorPrincipalId).toBeUndefined();
458
+ // Self-heal creates a vellum guardian binding automatically
459
+ expect(ctx.actorPrincipalId).toBeDefined();
460
+ expect(ctx.actorPrincipalId).toMatch(/^vellum-principal-/);
460
461
  });
461
462
 
462
463
  test("sessionId matches the provided argument", () => {
@@ -180,28 +180,18 @@ describe("executeAppCreate", () => {
180
180
  expect(capturedPages).toEqual({ "settings.html": "<div/>" });
181
181
  });
182
182
 
183
- test('maps type "site" to appType "site"', async () => {
184
- let capturedType: "app" | "site" | undefined;
183
+ test("defaults html to minimal scaffold when omitted", async () => {
184
+ let capturedHtml: string | undefined;
185
185
  const store = makeMockStore({
186
186
  createApp: (params) => {
187
- capturedType = params.appType;
187
+ capturedHtml = params.htmlDefinition;
188
188
  return makeApp({ name: params.name });
189
189
  },
190
190
  });
191
- await executeAppCreate({ name: "Site", html: "<p/>", type: "site" }, store);
192
- expect(capturedType).toBe("site");
193
- });
194
-
195
- test('defaults appType to "app" when type is not "site"', async () => {
196
- let capturedType: "app" | "site" | undefined;
197
- const store = makeMockStore({
198
- createApp: (params) => {
199
- capturedType = params.appType;
200
- return makeApp({ name: params.name });
201
- },
202
- });
203
- await executeAppCreate({ name: "App", html: "<p/>" }, store);
204
- expect(capturedType).toBe("app");
191
+ await executeAppCreate({ name: "No HTML" }, store);
192
+ expect(capturedHtml).toBe(
193
+ "<!DOCTYPE html><html><head></head><body></body></html>",
194
+ );
205
195
  });
206
196
  });
207
197