@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.
- package/ARCHITECTURE.md +6 -6
- package/docs/architecture/memory.md +1 -1
- package/docs/architecture/scheduling.md +2 -3
- package/docs/architecture/security.md +5 -5
- package/docs/trusted-contact-access.md +5 -6
- package/package.json +4 -1
- package/src/__tests__/avatar-e2e.test.ts +18 -219
- package/src/__tests__/avatar-generator.test.ts +5 -57
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-readiness-routes.test.ts +20 -19
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +240 -18
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +104 -7
- package/src/__tests__/credential-vault-unit.test.ts +22 -20
- package/src/__tests__/credential-vault.test.ts +284 -12
- package/src/__tests__/credentials-cli.test.ts +11 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/integration-status.test.ts +53 -21
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/schedule-store.test.ts +1 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +0 -9
- package/src/__tests__/slack-channel-config.test.ts +9 -8
- package/src/__tests__/slack-share-routes.test.ts +11 -6
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
- package/src/approvals/AGENTS.md +1 -1
- package/src/calls/call-domain.ts +7 -4
- package/src/calls/twilio-config.ts +2 -1
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -2
- package/src/cli/commands/browser-relay.ts +40 -15
- package/src/cli/commands/credentials.ts +9 -8
- package/src/cli/commands/oauth.ts +1 -1
- package/src/cli.ts +3 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/loader.ts +6 -0
- package/src/daemon/computer-use-session.ts +7 -1
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-slack-channel.ts +37 -20
- package/src/daemon/handlers/config-telegram.ts +33 -20
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/ride-shotgun-handler.ts +3 -1
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-tool-setup.ts +18 -2
- package/src/daemon/session.ts +1 -1
- package/src/email/providers/index.ts +2 -1
- package/src/instrument.ts +15 -1
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +28 -62
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/schema/guardian.ts +1 -1
- package/src/messaging/provider.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +203 -122
- package/src/messaging/providers/gmail/people-client.ts +26 -18
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
- package/src/messaging/providers/whatsapp/adapter.ts +6 -3
- package/src/messaging/registry.ts +2 -1
- package/src/oauth/byo-connection.test.ts +436 -0
- package/src/oauth/byo-connection.ts +112 -0
- package/src/oauth/connect-orchestrator.ts +27 -0
- package/src/oauth/connection-resolver.ts +34 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/platform-connection.test.ts +163 -0
- package/src/oauth/platform-connection.ts +110 -0
- package/src/oauth/provider-base-urls.ts +21 -0
- package/src/oauth/provider-profiles.ts +1 -1
- package/src/oauth/token-persistence.ts +20 -20
- package/src/permissions/checker.ts +6 -1
- package/src/prompts/system-prompt.ts +52 -15
- package/src/prompts/templates/BOOTSTRAP.md +1 -1
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/runtime/AGENTS.md +18 -1
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -2
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +9 -4
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -3
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
- package/src/runtime/routes/integrations/slack/share.ts +3 -2
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +75 -17
- package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
- package/src/runtime/telegram-streaming-delivery.ts +11 -1
- package/src/schedule/integration-status.ts +5 -4
- package/src/security/credential-key.ts +170 -0
- package/src/security/token-manager.ts +36 -7
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/request-computer-control.ts +0 -5
- package/src/tools/credentials/broker.ts +6 -4
- package/src/tools/credentials/metadata-store.ts +72 -20
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +77 -16
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +0 -5
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +3 -44
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +93 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/avatar-router.test.ts +0 -149
- package/src/__tests__/managed-avatar-client.test.ts +0 -337
- package/src/config/bundled-skills/doordash/SKILL.md +0 -170
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
- package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
- package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
- package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
- package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
- package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
- package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
- package/src/media/avatar-types.ts +0 -53
- 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 /
|
|
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
|
|
351
|
-
| `credential
|
|
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** | `
|
|
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; `
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
@@ -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: (
|
|
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.
|
|
166
|
+
// 1. Local Gemini success
|
|
223
167
|
// -----------------------------------------------------------------------
|
|
224
168
|
|
|
225
|
-
test("
|
|
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
|
|
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
|
-
//
|
|
198
|
+
// 2. No Gemini key — error
|
|
313
199
|
// -----------------------------------------------------------------------
|
|
314
200
|
|
|
315
|
-
test("no
|
|
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
|
-
//
|
|
211
|
+
// 3. Gemini API failure
|
|
327
212
|
// -----------------------------------------------------------------------
|
|
328
213
|
|
|
329
|
-
test("
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
{
|
|
336
|
-
bytesBase64Encoded: "iVBORw0KGgoAAAANSUhEUg==",
|
|
337
|
-
mimeType: "image/gif",
|
|
214
|
+
test("Gemini API failure — error 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
|
-
|
|
397
|
-
expect(result.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|