@vellumai/assistant 0.4.45 → 0.4.48

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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
package/ARCHITECTURE.md CHANGED
@@ -263,7 +263,7 @@ When a voice call's ASK_GUARDIAN consultation times out before the guardian resp
263
263
  |
264
264
  | (conversation engine classifies intent)
265
265
  v
266
- call_back / message_back / decline
266
+ call_back / decline
267
267
  | |
268
268
  v v
269
269
  [dispatching] [declined] (terminal)
@@ -347,8 +347,8 @@ Both tokens are stored in the secure key store (macOS Keychain with encrypted fi
347
347
 
348
348
  | Secure key | Content |
349
349
  | ------------------------------------ | -------------------------------------------------------------------------- |
350
- | `credential:slack_channel:bot_token` | Slack bot token (used for `chat.postMessage` and `auth.test`) |
351
- | `credential:slack_channel:app_token` | Slack app token (`xapp-...`, used for Socket Mode `apps.connections.open`) |
350
+ | `credential/slack_channel/bot_token` | Slack bot token (used for `chat.postMessage` and `auth.test`) |
351
+ | `credential/slack_channel/app_token` | Slack app token (`xapp-...`, used for Socket Mode `apps.connections.open`) |
352
352
 
353
353
  Workspace metadata (team ID, team name, bot user ID, bot username) is stored as JSON in the credential metadata store under `('slack_channel', 'bot_token')`.
354
354
 
@@ -641,7 +641,7 @@ The assistant feature-flag resolver (`src/config/assistant-feature-flags.ts`) is
641
641
  | **3. `skill_load` tool** | `executeSkillLoad()` in `tools/skills/load.ts` | If the model attempts to load a flagged-off skill by name, the tool returns an error: `"skill is currently unavailable (disabled by feature flag)"`. |
642
642
  | **4. Runtime tool projection** | `projectSkillTools()` in `daemon/session-skill-tools.ts` | Even if a skill was previously active in a session (has `<loaded_skill>` markers in history), the per-turn projection drops it when the flag is OFF. Already-registered tools are unregistered. |
643
643
  | **5. Included child skills** | `executeSkillLoad()` in `tools/skills/load.ts` | When a parent skill includes children via the `includes` directive, each child is independently checked against its feature flag. Flagged-off children are silently excluded from the loaded skill content. |
644
- | **6. Skill install gate** | `handleInstallSkill()` in `daemon/handlers/skills.ts` | When a client requests skill installation, the handler checks the skill's feature flag before proceeding. If the flag is OFF, the install is rejected with an error. |
644
+ | **6. Skill install gate** | `handleSkillsInstall()` in `daemon/handlers/skills.ts` | When a client requests skill installation, the handler checks the skill's feature flag before proceeding. If the flag is OFF, the install is rejected with an error. |
645
645
 
646
646
  All six enforcement points derive the flag key via `skillFlagKey(skill)` — which returns `undefined` for ungated skills, short-circuiting the check — and then call `isAssistantFeatureFlagEnabled(flagKey, config)` for consistency.
647
647
 
@@ -657,7 +657,7 @@ All six enforcement points derive the flag key via `skillFlagKey(skill)` — whi
657
657
  | `src/tools/skills/load.ts` | `executeSkillLoad()` — enforcement points 3 and 5 |
658
658
  | `src/daemon/session-skill-tools.ts` | `projectSkillTools()` — enforcement point 4 |
659
659
  | `src/config/schema.ts` | `assistantFeatureFlagValues` field definition in `AssistantConfig` (Zod schema) |
660
- | `src/daemon/handlers/skills.ts` | `handleSkillsList()` — uses `resolveSkillStates()` for client responses; `handleInstallSkill()` — enforcement point 6 |
660
+ | `src/daemon/handlers/skills.ts` | `handleSkillsList()` — uses `resolveSkillStates()` for client responses; `handleSkillsInstall()` — enforcement point 6 |
661
661
  | `meta/feature-flags/feature-flag-registry.json` | Unified feature flag registry (repo root) — all declared flags with scope, label, default values, and descriptions |
662
662
  | `src/config/feature-flag-registry.json` | Bundled copy of the unified registry for compiled binary resolution |
663
663
 
@@ -669,7 +669,7 @@ All six enforcement points derive the flag key via `skillFlagKey(skill)` — whi
669
669
  graph LR
670
670
  subgraph "macOS Keychain"
671
671
  K1["API Key<br/>service: vellum-assistant<br/>account: anthropic<br/>stored via /usr/bin/security CLI"]
672
- K2["Credential Secrets<br/>key: credential:{service}:{field}<br/>stored via secure-keys.ts<br/>(encrypted file fallback if Keychain unavailable)"]
672
+ K2["Credential Secrets<br/>key: credential/{service}/{field}<br/>stored via secure-keys.ts<br/>(encrypted file fallback if Keychain unavailable)"]
673
673
  end
674
674
 
675
675
  subgraph "UserDefaults (plist)"
@@ -242,7 +242,7 @@ Two trust gates enforce trust-class-based access control over the memory pipelin
242
242
 
243
243
  - **Read gate** (`session-memory.ts`): When the current session's actor is untrusted, the memory recall pipeline returns a no-op context — no recall injection, no dynamic profile, no conflict resolution. This ensures untrusted actors cannot surface or exploit previously extracted memory.
244
244
 
245
- Trust policy is **cross-channel and trust-class-based**: decisions use `trustContext.trustClass`, not the channel string. Desktop sessions default to `trustClass: 'guardian'`. External channels (Telegram, SMS, WhatsApp, phone) provide explicit trust context via the resolver. Messages without provenance metadata are treated as trusted (guardian); all new messages carry provenance.
245
+ Trust policy is **cross-channel and trust-class-based**: decisions use `trustContext.trustClass`, not the channel string. Desktop sessions default to `trustClass: 'guardian'`. External channels (Telegram, WhatsApp, phone) provide explicit trust context via the resolver. Messages without provenance metadata are treated as trusted (guardian); all new messages carry provenance.
246
246
 
247
247
  ---
248
248
 
@@ -45,7 +45,7 @@ The database column is named `cron_expression` and the Drizzle table is `cronJob
45
45
 
46
46
  ## Reminder Routing — Trigger-Time Multi-Channel Delivery
47
47
 
48
- Reminders support optional routing metadata that controls how the notification pipeline fans out delivery across channels when a reminder fires. This allows a single reminder to reach the user on multiple channels (desktop, Telegram, SMS) without requiring duplicate reminders.
48
+ Reminders support optional routing metadata that controls how the notification pipeline fans out delivery across channels when a reminder fires. This allows a single reminder to reach the user on multiple channels (desktop, Telegram) without requiring duplicate reminders.
49
49
 
50
50
  ### Routing Metadata Model
51
51
 
@@ -69,7 +69,7 @@ sequenceDiagram
69
69
  participant Engine as Decision Engine<br/>(LLM)
70
70
  participant Enforce as enforceRoutingIntent
71
71
  participant Broadcaster as Broadcaster
72
- participant Adapters as Channel Adapters<br/>(Vellum, Telegram, SMS)
72
+ participant Adapters as Channel Adapters<br/>(Vellum, Telegram)
73
73
 
74
74
  Scheduler->>Store: claimDueReminders(now)
75
75
  Store-->>Scheduler: ReminderRow[] (with routingIntent, routingHints)
@@ -106,7 +106,6 @@ Channel availability is resolved when the signal is emitted (not when the remind
106
106
 
107
107
  - **Vellum** — always connected (local HTTP)
108
108
  - **Telegram** — connected when an active guardian binding exists
109
- - **SMS** — connected when an active guardian binding exists
110
109
 
111
110
  If a channel becomes unavailable between reminder creation and fire time, it is silently excluded from delivery. The routing intent enforcement operates only on channels that are connected at fire time.
112
111
 
@@ -179,7 +179,7 @@ File tool candidates include canonical (symlink-resolved) absolute paths via `no
179
179
  | `assistant/src/permissions/checker.ts` | `classifyRisk()`, `check()`, `buildCommandCandidates()`, allowlist/scope generation |
180
180
  | `assistant/src/permissions/shell-identity.ts` | `analyzeShellCommand()`, `deriveShellActionKeys()`, `buildShellCommandCandidates()`, `buildShellAllowlistOptions()` — parser-based shell command identity and action key derivation |
181
181
  | `assistant/src/permissions/trust-store.ts` | Rule persistence, `findHighestPriorityRule()`, execution-target matching, starter bundle |
182
- | `assistant/src/permissions/prompter.ts` | HTTP prompt flow: `confirmation_request` → `confirmation_response` |
182
+ | `assistant/src/permissions/prompter.ts` | HTTP prompt flow: `confirmation_request` → `confirmation_response` |
183
183
  | `assistant/src/permissions/defaults.ts` | Default rule templates (system ask rules for host tools, CU, etc.) |
184
184
  | `assistant/src/skills/version-hash.ts` | `computeSkillVersionHash()` — deterministic SHA-256 of skill source files |
185
185
  | `assistant/src/skills/path-classifier.ts` | `isSkillSourcePath()`, `normalizeFilePath()`, skill root detection |
@@ -233,7 +233,7 @@ sequenceDiagram
233
233
  UI->>HTTP: secret_response {requestId, value, delivery: "store"}
234
234
  HTTP->>Prompter: resolve(value, "store")
235
235
  Prompter->>Vault: {value, delivery: "store"}
236
- Vault->>Keychain: setSecureKey("credential:svc:field", value)
236
+ Vault->>Keychain: setSecureKey("credential/svc/field", value)
237
237
  Vault->>Model: "Credential stored securely" (no value in output)
238
238
  else One-Time Send (if enabled)
239
239
  UI->>HTTP: secret_response {requestId, value, delivery: "transient_send"}
@@ -272,7 +272,7 @@ graph TB
272
272
  TOOL["Tool (e.g. browser_fill_credential)"] --> BROKER["CredentialBroker.use(service, field, tool, domain)"]
273
273
  BROKER --> POLICY{"Check policy:<br/>allowedTools + allowedDomains"}
274
274
  POLICY -->|denied| REJECT["PolicyDenied error"]
275
- POLICY -->|allowed| FETCH["getSecureKey(credential:svc:field)"]
275
+ POLICY -->|allowed| FETCH["getSecureKey(credential/svc/field)"]
276
276
  FETCH --> INJECT["Inject value into tool execution<br/>(never returned to model)"]
277
277
  ```
278
278
 
@@ -290,7 +290,7 @@ The `allowOneTimeSend` config gate (default: `false`) enables a secondary "Send
290
290
 
291
291
  | Component | Location | What it stores |
292
292
  | ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
293
- | Secret values | macOS Keychain (primary) or encrypted file fallback | Encrypted credential values keyed as `credential:{service}:{field}`. Falls back to encrypted file backend on Linux/headless or when Keychain is unavailable. |
293
+ | Secret values | macOS Keychain (primary) or encrypted file fallback | Encrypted credential values keyed as `credential/{service}/{field}`. Falls back to encrypted file backend on Linux/headless or when Keychain is unavailable. |
294
294
  | Credential metadata | `~/.vellum/workspace/data/credentials/metadata.json` | Service, field, label, policy (allowedTools, allowedDomains), timestamps |
295
295
  | Config | `~/.vellum/workspace/config.*` | `secretDetection` settings: enabled, action, entropyThreshold, allowOneTimeSend |
296
296
 
@@ -303,7 +303,7 @@ The `allowOneTimeSend` config gate (default: `false`) enables a secondary "Send
303
303
  | `assistant/src/tools/credentials/metadata-store.ts` | JSON file metadata CRUD for credential records |
304
304
  | `assistant/src/tools/credentials/broker.ts` | Brokered credential access with policy enforcement and transient send |
305
305
  | `assistant/src/tools/credentials/policy-validate.ts` | Policy input validation (allowedTools, allowedDomains) |
306
- | `assistant/src/permissions/secret-prompter.ts` | HTTP secret_request/secret_response flow |
306
+ | `assistant/src/permissions/secret-prompter.ts` | HTTP secret_request/secret_response flow |
307
307
  | `assistant/src/security/secret-scanner.ts` | Regex + entropy-based secret detection |
308
308
  | `assistant/src/security/secret-ingress.ts` | Inbound message secret blocking |
309
309
  | `clients/macos/.../SecretPromptManager.swift` | Floating panel UI for secure credential entry |
@@ -12,7 +12,7 @@ Design doc defining how unknown users gain access to a Vellum assistant via chan
12
12
 
13
13
  ## User Journey
14
14
 
15
- 1. **Unknown user messages the assistant** on Telegram (or SMS, or any channel).
15
+ 1. **Unknown user messages the assistant** on Telegram (or any channel).
16
16
  2. **Assistant rejects the message** via the ingress ACL in `inbound-message-handler.ts`. The user has no matching `contact_channels` record, so the handler replies: _"Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."_ and returns `{ denied: true, reason: 'not_a_member' }`.
17
17
  3. **Notification pipeline alerts the guardian.** The rejection triggers `notifyGuardianOfAccessRequest()` which creates a canonical access request and calls `emitNotificationSignal()` with `sourceEventName: 'ingress.access_request'`. The notification routes through the decision engine to all connected channels (vellum macOS app, Telegram, etc.). The guardian sees who is requesting access, including a request code for approve/reject and an `open invite flow` option to start the Trusted Contacts invite flow.
18
18
 
@@ -32,7 +32,7 @@ Design doc defining how unknown users gain access to a Vellum assistant via chan
32
32
  This ensures unknown inbound access attempts always trigger guardian notification, even when the requester's source channel has no guardian binding.
33
33
 
34
34
  4. **Guardian approves the request.** The guardian responds to the notification (via Telegram inline button, macOS app, or local app). On approval, the assistant creates a verification session via `createOutboundSession()` and generates a 6-digit verification code.
35
- 5. **Guardian receives the verification code.** The assistant delivers the code to the guardian's verified channel (Telegram chat, SMS, etc.).
35
+ 5. **Guardian receives the verification code.** The assistant delivers the code to the guardian's verified channel (Telegram chat, etc.).
36
36
  6. **Guardian gives the code to the requester out-of-band** (in person, text message, phone call, etc.). This out-of-band transfer is the trust anchor: it proves the requester has a real-world relationship with the guardian.
37
37
  7. **Requester enters the code** back to the assistant on the same channel. The inbound message handler intercepts bare 6-digit codes when a pending verification session exists for that channel.
38
38
  8. **Assistant verifies the code and activates the user.** `validateAndConsumeVerification()` hashes the code, matches it against the pending session, verifies identity binding (the code must come from the expected channel identity), consumes the session, and calls `upsertContactChannel()` with `status: 'active'` and `policy: 'allow'`.
@@ -60,8 +60,7 @@ Identity binding ensures the verification code can only be consumed by the inten
60
60
  | Channel | Identity fields | Binding behavior |
61
61
  | -------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
62
  | Telegram | `expectedExternalUserId` = Telegram user ID, `expectedChatId` = Telegram chat ID | Both are set when the guardian provides the requester's Telegram identity (from the original rejected message metadata). The `identityBindingStatus` is `'bound'`. Verification requires `actorExternalUserId` or `actorChatId` to match. |
63
- | SMS | `expectedPhoneE164` = phone number in E.164 format | Set from the requester's phone number. Verification requires `actorExternalUserId` to match the expected phone. |
64
- | Voice | `expectedPhoneE164` = phone number in E.164 format | Same as SMS: phone-based identity binding. |
63
+ | Voice | `expectedPhoneE164` = phone number in E.164 format | Phone-based identity binding. Verification requires `actorExternalUserId` to match the expected phone. |
65
64
  | HTTP API | `expectedExternalUserId` = API caller identity | Bound to whatever external user ID the API client provides. |
66
65
 
67
66
  **Anti-oracle invariant:** When identity verification fails, the error message is identical to the "invalid or expired code" message. This prevents attackers from distinguishing between a wrong code and a wrong identity, which would leak information about which identities have pending sessions.
@@ -151,13 +150,13 @@ sequenceDiagram
151
150
  participant G as Guardian
152
151
  participant N as Notification Pipeline
153
152
 
154
- U->>A: Send message on Telegram/SMS
153
+ U->>A: Send message on Telegram
155
154
  A->>A: findMember() → null
156
155
  A-->>U: "You haven't been approved. Ask the Guardian."
157
156
 
158
157
  A->>N: emitNotificationSignal('ingress.access_request')
159
158
  N->>N: evaluateSignal() → shouldNotify: true
160
- N->>G: Deliver notification (Telegram/vellum/SMS)
159
+ N->>G: Deliver notification (Telegram/vellum)
161
160
 
162
161
  Note over G: Guardian sees access request<br/>with requester identity
163
162
 
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.45",
3
+ "version": "0.4.48",
4
4
  "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
5
8
  "bin": {
6
9
  "assistant": "./src/index.ts"
7
10
  },
@@ -5,9 +5,6 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
7
  let mockGeminiKey: string | undefined = "test-gemini-key";
8
- let mockApiKey: string | undefined = "test-api-key-123";
9
- let mockBaseUrl = "https://platform.test.vellum.ai";
10
- let mockPlatformEnvUrl = "https://env.test.vellum.ai";
11
8
  let mockWorkspaceDir = "/tmp/test-workspace-e2e";
12
9
 
13
10
  const mkdirSyncFn = mock(() => {});
@@ -15,7 +12,6 @@ const writeFileSyncFn = mock(() => {});
15
12
  const renameSyncFn = mock(() => {});
16
13
 
17
14
  let logInfoCalls: Array<[unknown, string]> = [];
18
- let logWarnCalls: Array<[unknown, string]> = [];
19
15
  let logErrorCalls: Array<[unknown, string]> = [];
20
16
 
21
17
  // ---------------------------------------------------------------------------
@@ -32,22 +28,10 @@ const geminiGenerateContentFn = mock(async () => geminiGenerateContentResult);
32
28
  mock.module("../config/loader.js", () => ({
33
29
  getConfig: () => ({
34
30
  apiKeys: { gemini: mockGeminiKey },
35
- platform: { baseUrl: mockBaseUrl },
36
31
  imageGenModel: "gemini-2.5-flash-image",
37
32
  }),
38
33
  }));
39
34
 
40
- mock.module("../config/env.js", () => ({
41
- getPlatformBaseUrl: () => mockPlatformEnvUrl,
42
- }));
43
-
44
- mock.module("../security/secure-keys.js", () => ({
45
- getSecureKey: (account: string) => {
46
- if (account === "credential:vellum:assistant_api_key") return mockApiKey;
47
- return undefined;
48
- },
49
- }));
50
-
51
35
  mock.module("../util/platform.js", () => ({
52
36
  getWorkspaceDir: () => mockWorkspaceDir,
53
37
  }));
@@ -60,9 +44,7 @@ mock.module("pino", () => {
60
44
  info: (...args: unknown[]) => {
61
45
  logInfoCalls.push(args as [unknown, string]);
62
46
  },
63
- warn: (...args: unknown[]) => {
64
- logWarnCalls.push(args as [unknown, string]);
65
- },
47
+ warn: () => {},
66
48
  error: (...args: unknown[]) => {
67
49
  logErrorCalls.push(args as [unknown, string]);
68
50
  },
@@ -109,7 +91,6 @@ mock.module("@google/genai", () => ({
109
91
  }));
110
92
 
111
93
  // Import after all mocks are set up
112
- import { AVATAR_MAX_DECODED_BYTES } from "../media/avatar-types.js";
113
94
  import { setAvatarTool } from "../tools/system/avatar-generator.js";
114
95
 
115
96
  // ---------------------------------------------------------------------------
@@ -123,18 +104,6 @@ function executeAvatar(description: string) {
123
104
  );
124
105
  }
125
106
 
126
- /** Standard successful Vertex predictions response. */
127
- function managedPlatformResponse() {
128
- return {
129
- predictions: [
130
- {
131
- bytesBase64Encoded: "iVBORw0KGgoAAAANSUhEUg==",
132
- mimeType: "image/png",
133
- },
134
- ],
135
- };
136
- }
137
-
138
107
  /** Standard successful Gemini generateContent response. */
139
108
  function geminiContentResponse() {
140
109
  return {
@@ -155,26 +124,6 @@ function geminiContentResponse() {
155
124
  };
156
125
  }
157
126
 
158
- function mockFetchReturning(response: {
159
- ok: boolean;
160
- status: number;
161
- body: unknown;
162
- }) {
163
- globalThis.fetch = mock(() =>
164
- Promise.resolve({
165
- ok: response.ok,
166
- status: response.status,
167
- json: async () => response.body,
168
- text: async () =>
169
- typeof response.body === "string"
170
- ? response.body
171
- : JSON.stringify(response.body),
172
- }),
173
- ) as unknown as typeof globalThis.fetch;
174
- }
175
-
176
- const originalFetch = globalThis.fetch;
177
-
178
127
  const expectedAvatarPath =
179
128
  "/tmp/test-workspace-e2e/data/avatar/custom-avatar.png";
180
129
 
@@ -188,9 +137,6 @@ describe("avatar E2E integration", () => {
188
137
 
189
138
  beforeEach(() => {
190
139
  mockGeminiKey = "test-gemini-key";
191
- mockApiKey = "test-api-key-123";
192
- mockBaseUrl = "https://platform.test.vellum.ai";
193
- mockPlatformEnvUrl = "https://env.test.vellum.ai";
194
140
  mockWorkspaceDir = "/tmp/test-workspace-e2e";
195
141
 
196
142
  mkdirSyncFn.mockClear();
@@ -199,11 +145,9 @@ describe("avatar E2E integration", () => {
199
145
  geminiGenerateContentFn.mockClear();
200
146
 
201
147
  logInfoCalls = [];
202
- logWarnCalls = [];
203
148
  logErrorCalls = [];
204
149
 
205
150
  geminiGenerateContentResult = geminiContentResponse();
206
- globalThis.fetch = originalFetch;
207
151
 
208
152
  // Clear env var so tests control the key entirely via config mock
209
153
  delete process.env.GEMINI_API_KEY;
@@ -219,16 +163,10 @@ describe("avatar E2E integration", () => {
219
163
  });
220
164
 
221
165
  // -----------------------------------------------------------------------
222
- // 1. Managed success E2E
166
+ // 1. Local Gemini success
223
167
  // -----------------------------------------------------------------------
224
168
 
225
- test("managed success — file written, correct content, success message", async () => {
226
- mockFetchReturning({
227
- ok: true,
228
- status: 200,
229
- body: managedPlatformResponse(),
230
- });
231
-
169
+ test("local Gemini success — file written, correct content, success message", async () => {
232
170
  const result = await executeAvatar("a friendly robot");
233
171
 
234
172
  // Verify success message
@@ -252,68 +190,15 @@ describe("avatar E2E integration", () => {
252
190
  const expectedBuffer = Buffer.from("iVBORw0KGgoAAAANSUhEUg==", "base64");
253
191
  expect(writtenBuffer.equals(expectedBuffer)).toBe(true);
254
192
 
255
- // Verify Gemini was never called
256
- expect(geminiGenerateContentFn).not.toHaveBeenCalled();
257
- });
258
-
259
- // -----------------------------------------------------------------------
260
- // 2. Managed failure — falls back to local
261
- // -----------------------------------------------------------------------
262
-
263
- test("managed failure — falls back to local Gemini, file written", async () => {
264
- mockFetchReturning({
265
- ok: false,
266
- status: 502,
267
- body: {
268
- code: "upstream_error",
269
- subcode: "bad_gateway",
270
- detail: "Bad gateway",
271
- retryable: true,
272
- correlation_id: "corr-502",
273
- },
274
- });
275
-
276
- const result = await executeAvatar("a cute cat");
277
-
278
- // Verify success — local path was used
279
- expect(result.isError).toBe(false);
280
- expect(result.content).toContain("Avatar updated");
281
-
282
- // Verify file was written
283
- expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
284
- expect(renameSyncFn).toHaveBeenCalledTimes(1);
285
-
286
- // Verify Gemini was called as fallback
287
- expect(geminiGenerateContentFn).toHaveBeenCalledTimes(1);
288
- });
289
-
290
- // -----------------------------------------------------------------------
291
- // 3. No managed prerequisites — goes straight to local
292
- // -----------------------------------------------------------------------
293
-
294
- test("no managed API key — goes straight to local Gemini", async () => {
295
- mockApiKey = undefined; // No managed API key available
296
-
297
- const result = await executeAvatar("a cute cat");
298
-
299
- // Verify success via local path
300
- expect(result.isError).toBe(false);
301
- expect(result.content).toContain("Avatar updated");
302
-
303
- // Verify Gemini was called directly (no managed attempt)
193
+ // Verify Gemini was called
304
194
  expect(geminiGenerateContentFn).toHaveBeenCalledTimes(1);
305
-
306
- // Verify file was written
307
- expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
308
- expect(renameSyncFn).toHaveBeenCalledTimes(1);
309
195
  });
310
196
 
311
197
  // -----------------------------------------------------------------------
312
- // 4. No managed prerequisites and no local key — error
198
+ // 2. No Gemini key — error
313
199
  // -----------------------------------------------------------------------
314
200
 
315
- test("no managed API key and no Gemini key — error surfaced", async () => {
316
- mockApiKey = undefined;
201
+ test("no Gemini key — error surfaced", async () => {
317
202
  mockGeminiKey = undefined;
318
203
 
319
204
  const result = await executeAvatar("a whimsical owl");
@@ -323,109 +208,23 @@ describe("avatar E2E integration", () => {
323
208
  });
324
209
 
325
210
  // -----------------------------------------------------------------------
326
- // 5. Response validation — bad MIME type
211
+ // 3. Gemini API failure
327
212
  // -----------------------------------------------------------------------
328
213
 
329
- test("managed response with disallowed MIME type falls back to local", async () => {
330
- mockFetchReturning({
331
- ok: true,
332
- status: 200,
333
- body: {
334
- predictions: [
335
- {
336
- bytesBase64Encoded: "iVBORw0KGgoAAAANSUhEUg==",
337
- mimeType: "image/gif",
214
+ test("Gemini API failureerror surfaced", async () => {
215
+ geminiGenerateContentResult = {
216
+ candidates: [
217
+ {
218
+ content: {
219
+ parts: [],
338
220
  },
339
- ],
340
- },
341
- });
342
-
343
- const result = await executeAvatar("a cat");
344
-
345
- // Managed fails validation, falls back to local Gemini
346
- expect(result.isError).toBe(false);
347
- expect(result.content).toContain("Avatar updated");
348
- expect(geminiGenerateContentFn).toHaveBeenCalledTimes(1);
349
- });
350
-
351
- // -----------------------------------------------------------------------
352
- // 6. Response validation — oversized image
353
- // -----------------------------------------------------------------------
354
-
355
- test("managed response with oversized data — falls back to local", async () => {
356
- const oversizedBase64 = "A".repeat(
357
- Math.ceil(((AVATAR_MAX_DECODED_BYTES + 100) * 4) / 3),
358
- );
359
- mockFetchReturning({
360
- ok: true,
361
- status: 200,
362
- body: {
363
- predictions: [
364
- { bytesBase64Encoded: oversizedBase64, mimeType: "image/png" },
365
- ],
366
- },
367
- });
368
-
369
- const result = await executeAvatar("a cat");
370
-
371
- // Managed fails validation, falls back to local Gemini
372
- expect(result.isError).toBe(false);
373
- expect(result.content).toContain("Avatar updated");
374
- expect(geminiGenerateContentFn).toHaveBeenCalledTimes(1);
375
- });
376
-
377
- // -----------------------------------------------------------------------
378
- // 7. Rate limit — falls back to local
379
- // -----------------------------------------------------------------------
380
-
381
- test("managed 429 response — falls back to local Gemini", async () => {
382
- mockFetchReturning({
383
- ok: false,
384
- status: 429,
385
- body: {
386
- code: "avatar_rate_limited",
387
- subcode: "too_many_requests",
388
- detail: "Rate limit exceeded",
389
- retryable: true,
390
- correlation_id: "corr-429",
391
- },
392
- });
221
+ },
222
+ ],
223
+ };
393
224
 
394
225
  const result = await executeAvatar("a cat");
395
226
 
396
- // Falls back to local successfully
397
- expect(result.isError).toBe(false);
398
- expect(result.content).toContain("Avatar updated");
399
- expect(geminiGenerateContentFn).toHaveBeenCalledTimes(1);
400
- });
401
-
402
- // -----------------------------------------------------------------------
403
- // 8. Correlation ID propagation
404
- // -----------------------------------------------------------------------
405
-
406
- test("managed success — correlation ID is propagated through the pipeline", async () => {
407
- mockFetchReturning({
408
- ok: true,
409
- status: 200,
410
- body: managedPlatformResponse(),
411
- });
412
-
413
- const result = await executeAvatar("a robot");
414
-
415
- // The managed path succeeded and wrote the avatar file
416
- expect(result.isError).toBe(false);
417
- expect(writeFileSyncFn).toHaveBeenCalled();
418
-
419
- // Correlation ID is now generated client-side, so just verify one is logged
420
- const correlationLogged = logInfoCalls.some(([meta, message]) => {
421
- if (message !== "Avatar saved successfully") return false;
422
- if (!meta || typeof meta !== "object" || !("correlationId" in meta)) {
423
- return false;
424
- }
425
- const cid = (meta as { correlationId?: unknown }).correlationId;
426
- return typeof cid === "string" && cid.length > 0;
427
- });
428
-
429
- expect(correlationLogged).toBe(true);
227
+ expect(result.isError).toBe(true);
228
+ expect(result.content).toContain("failed");
430
229
  });
431
230
  });
@@ -1,7 +1,5 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
- import { ManagedAvatarError } from "../media/avatar-types.js";
4
-
5
3
  // ---------------------------------------------------------------------------
6
4
  // Mock state
7
5
  // ---------------------------------------------------------------------------
@@ -10,7 +8,7 @@ let mockRouterResult: unknown;
10
8
  let mockRouterError: Error | undefined;
11
9
  let mockWorkspaceDir = "/tmp/test-workspace";
12
10
 
13
- const routedGenerateAvatarFn = mock(async () => {
11
+ const generateAvatarFn = mock(async () => {
14
12
  if (mockRouterError) throw mockRouterError;
15
13
  return mockRouterResult;
16
14
  });
@@ -24,7 +22,7 @@ const renameSyncFn = mock(() => {});
24
22
  // ---------------------------------------------------------------------------
25
23
 
26
24
  mock.module("../media/avatar-router.js", () => ({
27
- routedGenerateAvatar: routedGenerateAvatarFn,
25
+ generateAvatar: generateAvatarFn,
28
26
  }));
29
27
 
30
28
  mock.module("../util/logger.js", () => ({
@@ -57,8 +55,6 @@ function successResult() {
57
55
  return {
58
56
  imageBase64: "iVBORw0KGgoAAAANSUhEUg==",
59
57
  mimeType: "image/png",
60
- pathUsed: "local" as const,
61
- correlationId: "test-corr-id",
62
58
  };
63
59
  }
64
60
 
@@ -78,7 +74,7 @@ describe("setAvatarTool", () => {
78
74
  mockRouterResult = successResult();
79
75
  mockRouterError = undefined;
80
76
  mockWorkspaceDir = "/tmp/test-workspace";
81
- routedGenerateAvatarFn.mockClear();
77
+ generateAvatarFn.mockClear();
82
78
  mkdirSyncFn.mockClear();
83
79
  writeFileSyncFn.mockClear();
84
80
  renameSyncFn.mockClear();
@@ -89,7 +85,7 @@ describe("setAvatarTool", () => {
89
85
 
90
86
  expect(result.isError).toBe(false);
91
87
  expect(result.content).toContain("Avatar updated");
92
- expect(routedGenerateAvatarFn).toHaveBeenCalledTimes(1);
88
+ expect(generateAvatarFn).toHaveBeenCalledTimes(1);
93
89
  });
94
90
 
95
91
  test("empty description returns error", async () => {
@@ -97,7 +93,7 @@ describe("setAvatarTool", () => {
97
93
 
98
94
  expect(result.isError).toBe(true);
99
95
  expect(result.content).toContain("description is required");
100
- expect(routedGenerateAvatarFn).not.toHaveBeenCalled();
96
+ expect(generateAvatarFn).not.toHaveBeenCalled();
101
97
  });
102
98
 
103
99
  test("no image data returned yields error", async () => {
@@ -109,54 +105,6 @@ describe("setAvatarTool", () => {
109
105
  expect(result.content).toContain("No image data returned");
110
106
  });
111
107
 
112
- test("ManagedAvatarError with statusCode 429 returns user-friendly rate limit message", async () => {
113
- mockRouterError = new ManagedAvatarError({
114
- code: "some_error_code",
115
- subcode: "too_many_requests",
116
- detail: "Rate limited",
117
- retryable: true,
118
- correlationId: "corr-rate",
119
- statusCode: 429,
120
- });
121
-
122
- const result = await executeAvatar("a cat");
123
-
124
- expect(result.isError).toBe(true);
125
- expect(result.content).toContain("rate limited");
126
- });
127
-
128
- test("ManagedAvatarError with 503 returns service unavailable message", async () => {
129
- mockRouterError = new ManagedAvatarError({
130
- code: "avatar_service_error",
131
- subcode: "upstream_unavailable",
132
- detail: "Service down",
133
- retryable: true,
134
- correlationId: "corr-503",
135
- statusCode: 503,
136
- });
137
-
138
- const result = await executeAvatar("a cat");
139
-
140
- expect(result.isError).toBe(true);
141
- expect(result.content).toContain("temporarily unavailable");
142
- });
143
-
144
- test("ManagedAvatarError with other code returns detail message", async () => {
145
- mockRouterError = new ManagedAvatarError({
146
- code: "avatar_content_filtered",
147
- subcode: "policy_violation",
148
- detail: "Content was filtered by safety policy",
149
- retryable: false,
150
- correlationId: "corr-filter",
151
- statusCode: 400,
152
- });
153
-
154
- const result = await executeAvatar("a cat");
155
-
156
- expect(result.isError).toBe(true);
157
- expect(result.content).toContain("Content was filtered by safety policy");
158
- });
159
-
160
108
  test("generic error returns mapped message", async () => {
161
109
  mockRouterError = new Error("Network timeout");
162
110