@vellumai/assistant 0.3.27 → 0.4.0
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 +81 -4
- package/Dockerfile +2 -2
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +9 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +21 -19
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +212 -36
- package/src/__tests__/notification-decision-fallback.test.ts +63 -3
- package/src/__tests__/notification-decision-strategy.test.ts +78 -0
- package/src/__tests__/notification-guardian-path.test.ts +15 -15
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +126 -59
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +358 -24
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +22 -16
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +33 -6
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +68 -326
- package/src/daemon/session-runtime-assembly.ts +119 -25
- package/src/daemon/session-tool-setup.ts +3 -2
- package/src/daemon/session.ts +4 -3
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +586 -0
- package/src/memory/channel-guardian-store.ts +2 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +56 -0
- package/src/notifications/copy-composer.ts +31 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +173 -0
- package/src/runtime/actor-trust-resolver.ts +221 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -71
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +717 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +20 -2
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +205 -529
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +53 -10
- package/src/tools/types.ts +13 -2
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
package/ARCHITECTURE.md
CHANGED
|
@@ -18,7 +18,7 @@ This document owns assistant-runtime architecture details. The repo-level archit
|
|
|
18
18
|
- The same resolver is used by:
|
|
19
19
|
- `/channels/inbound` (Telegram/SMS/WhatsApp path) before run orchestration.
|
|
20
20
|
- Inbound Twilio voice setup (`RelayConnection.handleSetup`) to seed call-time actor context.
|
|
21
|
-
- Runtime channel runs pass this as `guardianContext`, and session runtime assembly injects `<
|
|
21
|
+
- Runtime channel runs pass this as `guardianContext`, and session runtime assembly injects `<inbound_actor_context>` (via `inboundActorContextFromGuardian()`) into provider-facing prompts.
|
|
22
22
|
- Voice calls mirror the same prompt contract: `CallController` receives guardian context on setup and refreshes it immediately after successful voice challenge verification, so the first post-verification turn is grounded as `actor_role: guardian`.
|
|
23
23
|
- Voice-specific behavior (DTMF/speech verification flow, relay state machine) remains voice-local; only actor-role resolution is shared.
|
|
24
24
|
|
|
@@ -62,6 +62,41 @@ All guardian approval decisions — regardless of how they arrive — route thro
|
|
|
62
62
|
| `src/daemon/ipc-contract/guardian-actions.ts` | IPC message type definitions for guardian action requests/responses |
|
|
63
63
|
| `src/runtime/channel-approval-types.ts` | Channel-facing approval action types and `toApprovalActionOptions` bridge |
|
|
64
64
|
|
|
65
|
+
### Canonical Guardian Request System
|
|
66
|
+
|
|
67
|
+
The canonical guardian request system provides a channel-agnostic, unified domain for all guardian approval and question flows. It replaces the fragmented per-channel storage with a single source of truth that works identically for voice calls, Telegram/SMS/WhatsApp, and desktop UI.
|
|
68
|
+
|
|
69
|
+
**Architecture layers:**
|
|
70
|
+
|
|
71
|
+
1. **Canonical domain (single source of truth):** All guardian requests — tool approvals, pending questions, access requests — are persisted in the `canonical_guardian_requests` table (`src/memory/canonical-guardian-store.js`). Each request has a unique ID, a short human-readable request code, and a status that follows a CAS (compare-and-swap) lifecycle: `pending` -> `approved` | `denied` | `expired` | `cancelled`. Deliveries (notifications sent to guardians) are tracked in `canonical_guardian_deliveries`.
|
|
72
|
+
|
|
73
|
+
2. **Unified apply primitive (single write path):** `applyCanonicalGuardianDecision()` in `src/approvals/guardian-decision-primitive.ts` is the single write path for all guardian decisions. It enforces identity validation, expiry checks, CAS resolution, `approve_always` downgrade (guardian-on-behalf invariant), kind-specific resolver dispatch via the resolver registry, and scoped grant minting. All callers — HTTP API, IPC handlers, inbound channel router, desktop session — route decisions through this function.
|
|
74
|
+
|
|
75
|
+
3. **Shared reply router (priority-ordered routing):** `routeGuardianReply()` in `src/runtime/guardian-reply-router.ts` provides a single entry point for all inbound guardian reply processing across channels. It routes through a priority-ordered pipeline: (a) deterministic callback parsing (button presses with `apr:<requestId>:<action>`), (b) request code parsing (6-char alphanumeric prefix), (c) NL classification via the conversational approval engine. All decisions flow through `applyCanonicalGuardianDecision`.
|
|
76
|
+
|
|
77
|
+
4. **Deterministic API (prompt listing and decision endpoints):** Desktop clients and API consumers use `GET /v1/guardian-actions/pending` and `POST /v1/guardian-actions/decision` (HTTP) or the equivalent IPC messages. These endpoints surface canonical requests alongside legacy pending interactions and channel approval records, with deduplication to avoid double-rendering.
|
|
78
|
+
|
|
79
|
+
5. **Dual-mode (deterministic + conversational):** Guardians can respond via structured button UIs (deterministic path) or free-text conversation (NL path). Both paths converge on the same canonical primitive. Code-only messages (just a request code without decision text) return clarification instead of auto-approving. Disambiguation with multiple pending requests stays fail-closed — no auto-resolve when the target is ambiguous.
|
|
80
|
+
|
|
81
|
+
**Resolver registry:** Kind-specific resolvers (`src/approvals/guardian-request-resolvers.ts`) handle side effects after CAS resolution. Built-in resolvers: `tool_approval` (channel/desktop approval path) and `pending_question` (voice call question path). New request kinds register resolvers without touching the core primitive.
|
|
82
|
+
|
|
83
|
+
**Expiry sweeps:** Three complementary sweeps run on 60-second intervals to clean up stale requests:
|
|
84
|
+
- `src/calls/guardian-action-sweep.ts` — voice call guardian action requests
|
|
85
|
+
- `src/runtime/routes/guardian-expiry-sweep.ts` — channel guardian approval requests
|
|
86
|
+
- `src/runtime/routes/canonical-guardian-expiry-sweep.ts` — canonical guardian requests (CAS-safe)
|
|
87
|
+
|
|
88
|
+
**Key source files:**
|
|
89
|
+
|
|
90
|
+
| File | Purpose |
|
|
91
|
+
|------|---------|
|
|
92
|
+
| `src/memory/canonical-guardian-store.ts` | Canonical request and delivery persistence (CRUD, CAS resolve, list with filters) |
|
|
93
|
+
| `src/approvals/guardian-decision-primitive.ts` | Unified decision primitive: `applyCanonicalGuardianDecision` (canonical) and `applyGuardianDecision` (legacy) |
|
|
94
|
+
| `src/approvals/guardian-request-resolvers.ts` | Resolver registry: kind-specific side-effect dispatch after CAS resolution |
|
|
95
|
+
| `src/runtime/guardian-reply-router.ts` | Shared inbound router: callback -> code -> NL classification pipeline |
|
|
96
|
+
| `src/runtime/routes/guardian-action-routes.ts` | HTTP endpoints for prompt listing and decision submission |
|
|
97
|
+
| `src/daemon/handlers/guardian-actions.ts` | IPC handlers for desktop socket clients |
|
|
98
|
+
| `src/runtime/routes/canonical-guardian-expiry-sweep.ts` | Canonical request expiry sweep |
|
|
99
|
+
|
|
65
100
|
### Outbound Guardian Verification (HTTP Endpoints)
|
|
66
101
|
|
|
67
102
|
Guardian verification can be initiated through gateway HTTP endpoints (which forward to runtime handlers) as an alternative to the legacy IPC-only flow. This enables chat-first verification where the assistant guides the user through guardian setup via normal conversation.
|
|
@@ -357,26 +392,56 @@ A complementary access-granting flow where the guardian proactively creates a sh
|
|
|
357
392
|
|
|
358
393
|
**Inbound intercept points:** Invite token extraction runs early in the inbound handler, before ACL denial, so valid invites short-circuit the membership check. Two intercept branches handle: (a) non-members — the invite creates their first member record; (b) inactive members (revoked/pending) — the invite reactivates them.
|
|
359
394
|
|
|
360
|
-
**
|
|
395
|
+
**Channel adapter status:**
|
|
361
396
|
|
|
362
397
|
| Channel | Status | Prerequisites |
|
|
363
398
|
|---------|--------|--------------|
|
|
364
399
|
| Telegram | Shipped | Bot username resolved from credential metadata or `TELEGRAM_BOT_USERNAME` env |
|
|
400
|
+
| Voice | Shipped | Identity-bound voice code redemption via DTMF/speech in the relay state machine. Gated behind `feature_flags.voice-invite-redemption.enabled` (default OFF). |
|
|
365
401
|
| SMS | Deferred | Needs a deep-link strategy compatible with SMS (short URL or web redemption page) |
|
|
366
402
|
| Slack | Deferred | Needs DM-safe ingress — Socket Mode handles channel messages but DM-initiated invite flows need routing |
|
|
367
|
-
|
|
403
|
+
|
|
404
|
+
### Voice Invite Flow (invite_redemption_pending)
|
|
405
|
+
|
|
406
|
+
Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL token. The guardian creates an invite bound to the invitee's E.164 phone number; the invitee redeems it by entering the code during an inbound voice call.
|
|
407
|
+
|
|
408
|
+
**Creation flow:**
|
|
409
|
+
1. Guardian creates a voice invite via `POST /v1/ingress/invites` with `sourceChannel: "voice"` and `expectedExternalUserId` (E.164 phone).
|
|
410
|
+
2. `ingress-service.ts` generates a cryptographically random numeric code (`generateVoiceCode`), hashes it with SHA-256 (`hashVoiceCode`), and stores only the hash.
|
|
411
|
+
3. The one-time plaintext `voiceCode` is returned in the creation response. The raw token is NOT returned for voice invites — redemption uses the identity-bound code flow exclusively.
|
|
412
|
+
4. Guardian communicates the code to the invitee out-of-band.
|
|
413
|
+
|
|
414
|
+
**Call-time redemption subflow (`invite_redemption_pending`):**
|
|
415
|
+
1. Unknown caller dials in. `relay-server.ts` resolves trust via `resolveActorTrust`. Caller is `unknown`, no pending guardian challenge.
|
|
416
|
+
2. If `feature_flags.voice-invite-redemption.enabled` is ON, the relay checks `findActiveVoiceInvites` for invites bound to the caller's phone number.
|
|
417
|
+
3. If active, non-expired invites exist, the relay enters the `invite_redemption_pending` state (reuses the `verification_pending` connection state) and prompts the caller to enter their invite code via DTMF or speech.
|
|
418
|
+
4. `redeemVoiceInviteCode` validates: identity match, code hash match, expiry, use count. On success, an active member record is upserted and the call transitions to the normal call flow.
|
|
419
|
+
5. On failure, the caller gets up to 3 attempts. After max attempts, the call is terminated.
|
|
420
|
+
|
|
421
|
+
**Security invariants:**
|
|
422
|
+
- The plaintext voice code is returned exactly once at creation time and never stored.
|
|
423
|
+
- Voice invites are identity-bound: `expectedExternalUserId` must match the caller's E.164 number. An attacker with the code but the wrong phone number cannot redeem.
|
|
424
|
+
- Failure responses are intentionally generic (`invalid_or_expired`) to prevent oracle attacks.
|
|
425
|
+
- Blocked members cannot bypass the guardian's explicit block via invite redemption.
|
|
426
|
+
|
|
427
|
+
**Feature flag:** `feature_flags.voice-invite-redemption.enabled` (default OFF). When disabled, unknown callers with active voice invites are denied normally — the invite check is skipped entirely.
|
|
368
428
|
|
|
369
429
|
**Key source files:**
|
|
370
430
|
|
|
371
431
|
| File | Purpose |
|
|
372
432
|
|------|---------|
|
|
373
|
-
| `src/runtime/invite-redemption-service.ts` | Core redemption engine — token validation, member creation, discriminated-union outcomes |
|
|
433
|
+
| `src/runtime/invite-redemption-service.ts` | Core redemption engine — token validation, voice code redemption, member creation, discriminated-union outcomes |
|
|
374
434
|
| `src/runtime/invite-redemption-templates.ts` | Deterministic reply templates for each redemption outcome |
|
|
375
435
|
| `src/runtime/channel-invite-transport.ts` | Transport adapter registry with `buildShareableInvite` / `extractInboundToken` interface |
|
|
376
436
|
| `src/runtime/channel-invite-transports/telegram.ts` | Telegram adapter — `t.me/<bot>?start=iv_<token>` deep links, `/start iv_<token>` extraction |
|
|
437
|
+
| `src/runtime/channel-invite-transports/voice.ts` | Voice transport adapter — code-based redemption metadata |
|
|
377
438
|
| `src/daemon/guardian-invite-intent.ts` | Intent detection — routes create/list/revoke requests into the trusted-contacts skill |
|
|
378
439
|
| `src/runtime/ingress-service.ts` | Shared business logic for invite/member operations (used by both HTTP routes and IPC) |
|
|
440
|
+
| `src/runtime/routes/ingress-routes.ts` | HTTP API handlers for member/invite management including voice invite creation and redemption |
|
|
379
441
|
| `src/runtime/routes/inbound-message-handler.ts` | Invite token intercept in the inbound flow (non-member and inactive-member branches) |
|
|
442
|
+
| `src/calls/relay-server.ts` | Voice relay state machine — `invite_redemption_pending` subflow, feature flag gate |
|
|
443
|
+
| `src/util/voice-code.ts` | Cryptographic voice code generation and SHA-256 hashing |
|
|
444
|
+
| `src/memory/ingress-invite-store.ts` | Invite persistence including `findActiveVoiceInvites` for identity-bound lookup |
|
|
380
445
|
|
|
381
446
|
### Update Bulletin System
|
|
382
447
|
|
|
@@ -1874,6 +1939,18 @@ Connected channels are resolved at signal emission time: vellum is always includ
|
|
|
1874
1939
|
|
|
1875
1940
|
|
|
1876
1941
|
|
|
1942
|
+
### Sensitive Tool Output Placeholder Substitution
|
|
1943
|
+
|
|
1944
|
+
Some tool outputs contain values that must reach the user's final reply but should never be visible to the LLM (e.g., invite tokens). The system handles this with a three-stage pipeline:
|
|
1945
|
+
|
|
1946
|
+
1. **Directive extraction** (`src/tools/sensitive-output-placeholders.ts`): Tool output may include `<vellum-sensitive-output kind="invite_code" value="<raw>" />` directives. The executor strips directives, replaces raw values with deterministic placeholders (`VELLUM_ASSISTANT_INVITE_CODE_<shortId>`), and attaches `sensitiveBindings` metadata to the tool result.
|
|
1947
|
+
|
|
1948
|
+
2. **Placeholder-only model context**: The agent loop stores placeholder->value bindings in a per-run `substitutionMap`. Tool results sent to the LLM contain only placeholders — the model generates conversational text referencing them without ever seeing the real values.
|
|
1949
|
+
|
|
1950
|
+
3. **Post-generation substitution** (`src/agent/loop.ts`): Before emitting streamed `text_delta` events and before building the final `assistantMessage`, all placeholders are deterministically replaced with their real values. The substitution is chunk-safe for streaming (buffering partial placeholder prefixes across deltas).
|
|
1951
|
+
|
|
1952
|
+
Key files: `src/tools/sensitive-output-placeholders.ts`, `src/tools/executor.ts` (extraction hook), `src/agent/loop.ts` (substitution), `src/config/vellum-skills/trusted-contacts/SKILL.md` (invite flow adoption).
|
|
1953
|
+
|
|
1877
1954
|
### Notifications
|
|
1878
1955
|
|
|
1879
1956
|
For full notification developer guidance and lifecycle details, see [`assistant/src/notifications/README.md`](src/notifications/README.md).
|
package/Dockerfile
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Use debian as base image
|
|
2
|
-
FROM debian:
|
|
2
|
+
FROM debian:trixie@sha256:3615a749858a1cba49b408fb49c37093db813321355a9ab7c1f9f4836341e9db AS builder
|
|
3
3
|
|
|
4
4
|
WORKDIR /app
|
|
5
5
|
|
|
@@ -26,7 +26,7 @@ RUN bun install --frozen-lockfile
|
|
|
26
26
|
COPY . .
|
|
27
27
|
|
|
28
28
|
# Final stage
|
|
29
|
-
FROM debian:
|
|
29
|
+
FROM debian:trixie@sha256:3615a749858a1cba49b408fb49c37093db813321355a9ab7c1f9f4836341e9db AS runner
|
|
30
30
|
|
|
31
31
|
WORKDIR /app
|
|
32
32
|
|
package/bun.lock
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"@anthropic-ai/claude-agent-sdk": "^0.2.42",
|
|
9
9
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
10
10
|
"@google/genai": "^1.40.0",
|
|
11
|
-
"@huggingface/transformers": "^3.8.1",
|
|
12
11
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
13
12
|
"@qdrant/js-client-rest": "^1.16.2",
|
|
14
13
|
"@sentry/node": "^10.38.0",
|
|
@@ -35,6 +34,7 @@
|
|
|
35
34
|
"zod": "^4.3.6",
|
|
36
35
|
},
|
|
37
36
|
"devDependencies": {
|
|
37
|
+
"@huggingface/transformers": "^3.8.1",
|
|
38
38
|
"@pydantic/logfire-node": "^0.13.0",
|
|
39
39
|
"@types/archiver": "^7.0.0",
|
|
40
40
|
"@types/bun": "^1.2.4",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
47
47
|
"fast-check": "^4.5.3",
|
|
48
48
|
"knip": "^5.83.1",
|
|
49
|
+
"prettier": "^3.8.1",
|
|
49
50
|
"quicktype-core": "^23.2.6",
|
|
50
51
|
"typescript": "^5.7.3",
|
|
51
52
|
"typescript-eslint": "^8.54.0",
|
|
@@ -1136,6 +1137,8 @@
|
|
|
1136
1137
|
|
|
1137
1138
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
|
1138
1139
|
|
|
1140
|
+
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
|
1141
|
+
|
|
1139
1142
|
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
|
1140
1143
|
|
|
1141
1144
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
|
@@ -13,8 +13,15 @@ Design doc defining how unknown users gain access to a Vellum assistant via chan
|
|
|
13
13
|
## User Journey
|
|
14
14
|
|
|
15
15
|
1. **Unknown user messages the assistant** on Telegram (or SMS, or any channel).
|
|
16
|
-
2. **Assistant rejects the message** via the ingress ACL in `inbound-message-handler.ts`. The user has no `assistant_ingress_members` record, so the handler replies: *"
|
|
17
|
-
3. **Notification pipeline alerts the guardian.** The rejection triggers `emitNotificationSignal()` with `sourceEventName: 'ingress.access_request'
|
|
16
|
+
2. **Assistant rejects the message** via the ingress ACL in `inbound-message-handler.ts`. The user has no `assistant_ingress_members` record, so the handler replies: *"Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."* and returns `{ denied: true, reason: 'not_a_member' }`.
|
|
17
|
+
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
|
+
|
|
19
|
+
**Guardian binding resolution for access requests** uses a fallback strategy:
|
|
20
|
+
1. Source-channel active binding first (e.g., Telegram binding for a Telegram access request).
|
|
21
|
+
2. Any active binding for the assistant on another channel (deterministic: most recently verified first, then alphabetical by channel).
|
|
22
|
+
3. No guardian identity — the notification pipeline delivers via trusted/vellum channels even when no channel binding exists.
|
|
23
|
+
|
|
24
|
+
This ensures unknown inbound access attempts always trigger guardian notification, even when the requester's source channel has no guardian binding.
|
|
18
25
|
4. **Guardian approves the request.** The guardian responds to the notification (via Telegram inline button, macOS app, or IPC). On approval, the assistant creates a verification session via `createOutboundSession()` and generates a 6-digit verification code.
|
|
19
26
|
5. **Guardian receives the verification code.** The assistant delivers the code to the guardian's verified channel (Telegram chat, SMS, etc.).
|
|
20
27
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vellum": "./src/index.ts"
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"ipc:inventory:update": "bun run scripts/ipc/check-contract-inventory.ts --update",
|
|
15
15
|
"generate:ipc": "bun run scripts/ipc/generate-swift.ts",
|
|
16
16
|
"check:ipc-generated": "bun run scripts/ipc/generate-swift.ts --check",
|
|
17
|
+
"format": "prettier --write .",
|
|
18
|
+
"format:check": "prettier --check .",
|
|
17
19
|
"ipc:check-swift-drift": "bun run scripts/ipc/check-swift-decoder-drift.ts",
|
|
18
20
|
"lint": "eslint",
|
|
19
21
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -28,7 +30,6 @@
|
|
|
28
30
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
29
31
|
"@google/genai": "^1.40.0",
|
|
30
32
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
31
|
-
"@huggingface/transformers": "^3.8.1",
|
|
32
33
|
"@qdrant/js-client-rest": "^1.16.2",
|
|
33
34
|
"@sentry/node": "^10.38.0",
|
|
34
35
|
"agentmail": "^0.1.0",
|
|
@@ -65,9 +66,11 @@
|
|
|
65
66
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
66
67
|
"fast-check": "^4.5.3",
|
|
67
68
|
"knip": "^5.83.1",
|
|
69
|
+
"prettier": "^3.8.1",
|
|
68
70
|
"quicktype-core": "^23.2.6",
|
|
69
71
|
"typescript": "^5.7.3",
|
|
70
72
|
"typescript-eslint": "^8.54.0",
|
|
71
|
-
"typescript-json-schema": "^0.67.1"
|
|
73
|
+
"typescript-json-schema": "^0.67.1",
|
|
74
|
+
"@huggingface/transformers": "^3.8.1"
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -94,9 +94,9 @@ function generateSchemas(): Record<string, SchemaDef> {
|
|
|
94
94
|
|
|
95
95
|
const program = TJS.getProgramFromFiles(contractFiles, {
|
|
96
96
|
strict: true,
|
|
97
|
-
target:
|
|
98
|
-
module:
|
|
99
|
-
moduleResolution:
|
|
97
|
+
target: 'es2022',
|
|
98
|
+
module: 'commonjs',
|
|
99
|
+
moduleResolution: 'node',
|
|
100
100
|
skipLibCheck: true,
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -411,14 +411,18 @@ function emitStruct(s: SwiftStruct): string {
|
|
|
411
411
|
const lines: string[] = [];
|
|
412
412
|
|
|
413
413
|
if (s.doc) {
|
|
414
|
-
|
|
414
|
+
for (const docLine of s.doc.split('\n')) {
|
|
415
|
+
lines.push(`/// ${docLine}`);
|
|
416
|
+
}
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
lines.push(`public struct ${s.name}: Codable, Sendable {`);
|
|
418
420
|
|
|
419
421
|
for (const p of s.properties) {
|
|
420
422
|
if (p.doc) {
|
|
421
|
-
|
|
423
|
+
for (const docLine of p.doc.split('\n')) {
|
|
424
|
+
lines.push(` /// ${docLine}`);
|
|
425
|
+
}
|
|
422
426
|
}
|
|
423
427
|
lines.push(` public let ${p.swiftName}: ${p.swiftType}`);
|
|
424
428
|
}
|
|
@@ -1302,6 +1302,15 @@ exports[`IPC message snapshots ServerMessage types message_complete serializes t
|
|
|
1302
1302
|
}
|
|
1303
1303
|
`;
|
|
1304
1304
|
|
|
1305
|
+
exports[`IPC message snapshots ServerMessage types message_request_complete serializes to expected JSON 1`] = `
|
|
1306
|
+
{
|
|
1307
|
+
"requestId": "req-inline-001",
|
|
1308
|
+
"runStillActive": true,
|
|
1309
|
+
"sessionId": "sess-001",
|
|
1310
|
+
"type": "message_request_complete",
|
|
1311
|
+
}
|
|
1312
|
+
`;
|
|
1313
|
+
|
|
1305
1314
|
exports[`IPC message snapshots ServerMessage types session_info serializes to expected JSON 1`] = `
|
|
1306
1315
|
{
|
|
1307
1316
|
"correlationId": "corr-001",
|
|
@@ -3112,3 +3121,74 @@ exports[`IPC message snapshots ServerMessage types generate_avatar_response seri
|
|
|
3112
3121
|
"type": "generate_avatar_response",
|
|
3113
3122
|
}
|
|
3114
3123
|
`;
|
|
3124
|
+
|
|
3125
|
+
exports[`IPC message snapshots ClientMessage types guardian_actions_pending_request serializes to expected JSON 1`] = `
|
|
3126
|
+
{
|
|
3127
|
+
"conversationId": "conv-guardian-001",
|
|
3128
|
+
"type": "guardian_actions_pending_request",
|
|
3129
|
+
}
|
|
3130
|
+
`;
|
|
3131
|
+
|
|
3132
|
+
exports[`IPC message snapshots ClientMessage types guardian_action_decision serializes to expected JSON 1`] = `
|
|
3133
|
+
{
|
|
3134
|
+
"action": "approve_once",
|
|
3135
|
+
"conversationId": "conv-guardian-001",
|
|
3136
|
+
"requestId": "req-guardian-001",
|
|
3137
|
+
"type": "guardian_action_decision",
|
|
3138
|
+
}
|
|
3139
|
+
`;
|
|
3140
|
+
|
|
3141
|
+
exports[`IPC message snapshots ClientMessage types reorder_threads serializes to expected JSON 1`] = `
|
|
3142
|
+
{
|
|
3143
|
+
"type": "reorder_threads",
|
|
3144
|
+
"updates": [
|
|
3145
|
+
{
|
|
3146
|
+
"displayOrder": 0,
|
|
3147
|
+
"isPinned": false,
|
|
3148
|
+
"sessionId": "sess-001",
|
|
3149
|
+
},
|
|
3150
|
+
{
|
|
3151
|
+
"displayOrder": 1,
|
|
3152
|
+
"isPinned": true,
|
|
3153
|
+
"sessionId": "sess-002",
|
|
3154
|
+
},
|
|
3155
|
+
],
|
|
3156
|
+
}
|
|
3157
|
+
`;
|
|
3158
|
+
|
|
3159
|
+
exports[`IPC message snapshots ServerMessage types guardian_actions_pending_response serializes to expected JSON 1`] = `
|
|
3160
|
+
{
|
|
3161
|
+
"conversationId": "conv-guardian-001",
|
|
3162
|
+
"prompts": [
|
|
3163
|
+
{
|
|
3164
|
+
"actions": [
|
|
3165
|
+
{
|
|
3166
|
+
"action": "approve_once",
|
|
3167
|
+
"label": "Approve once",
|
|
3168
|
+
},
|
|
3169
|
+
{
|
|
3170
|
+
"action": "reject",
|
|
3171
|
+
"label": "Reject",
|
|
3172
|
+
},
|
|
3173
|
+
],
|
|
3174
|
+
"callSessionId": null,
|
|
3175
|
+
"conversationId": "conv-guardian-001",
|
|
3176
|
+
"expiresAt": 1700100000000,
|
|
3177
|
+
"questionText": "Approve tool: bash",
|
|
3178
|
+
"requestCode": "REQ-GU",
|
|
3179
|
+
"requestId": "req-guardian-001",
|
|
3180
|
+
"state": "pending",
|
|
3181
|
+
"toolName": "bash",
|
|
3182
|
+
},
|
|
3183
|
+
],
|
|
3184
|
+
"type": "guardian_actions_pending_response",
|
|
3185
|
+
}
|
|
3186
|
+
`;
|
|
3187
|
+
|
|
3188
|
+
exports[`IPC message snapshots ServerMessage types guardian_action_decision_response serializes to expected JSON 1`] = `
|
|
3189
|
+
{
|
|
3190
|
+
"applied": true,
|
|
3191
|
+
"requestId": "req-guardian-001",
|
|
3192
|
+
"type": "guardian_action_decision_response",
|
|
3193
|
+
}
|
|
3194
|
+
`;
|
|
@@ -44,7 +44,7 @@ describe('AgentLoop thinking budget clamping', () => {
|
|
|
44
44
|
const config = lastConfig()!;
|
|
45
45
|
const thinking = config.thinking as { type: string; budget_tokens: number };
|
|
46
46
|
expect(thinking.type).toBe('enabled');
|
|
47
|
-
expect(thinking.budget_tokens).toBe(
|
|
47
|
+
expect(thinking.budget_tokens).toBe(3072); // clamped to floor(maxTokens * 0.75)
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test('does not clamp when budget_tokens is within max_tokens', async () => {
|
|
@@ -1167,4 +1167,123 @@ describe('AgentLoop', () => {
|
|
|
1167
1167
|
expect(sentBlock).toBeDefined();
|
|
1168
1168
|
expect(sentBlock!.content).toBe(smallContent);
|
|
1169
1169
|
});
|
|
1170
|
+
|
|
1171
|
+
// ---------------------------------------------------------------------------
|
|
1172
|
+
// Sensitive output placeholder substitution tests
|
|
1173
|
+
// ---------------------------------------------------------------------------
|
|
1174
|
+
|
|
1175
|
+
// 32. Tool results with sensitiveBindings populate substitution map and
|
|
1176
|
+
// final assistant message text is resolved with real values.
|
|
1177
|
+
test('resolves sensitive output placeholders in final assistant message', async () => {
|
|
1178
|
+
const placeholder = 'VELLUM_ASSISTANT_INVITE_CODE_TEST1234';
|
|
1179
|
+
const realToken = 'realInviteToken999';
|
|
1180
|
+
|
|
1181
|
+
const { provider, calls } = createMockProvider([
|
|
1182
|
+
toolUseResponse('t1', 'bash', { command: 'create invite' }),
|
|
1183
|
+
// The LLM responds using the placeholder (it never saw the real token)
|
|
1184
|
+
textResponse(`Here is your invite link: https://t.me/bot?start=iv_${placeholder}`),
|
|
1185
|
+
]);
|
|
1186
|
+
|
|
1187
|
+
const toolExecutor = async () => ({
|
|
1188
|
+
content: `https://t.me/bot?start=iv_${placeholder}`,
|
|
1189
|
+
isError: false,
|
|
1190
|
+
sensitiveBindings: [{ kind: 'invite_code' as const, placeholder, value: realToken }],
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
1194
|
+
const events: AgentEvent[] = [];
|
|
1195
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
1196
|
+
|
|
1197
|
+
// The final assistant message in HISTORY should retain placeholders
|
|
1198
|
+
// (so the model never sees real values on subsequent turns)
|
|
1199
|
+
const lastAssistant = history[history.length - 1];
|
|
1200
|
+
expect(lastAssistant.role).toBe('assistant');
|
|
1201
|
+
const historyTextBlock = lastAssistant.content.find(
|
|
1202
|
+
(b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text',
|
|
1203
|
+
);
|
|
1204
|
+
expect(historyTextBlock).toBeDefined();
|
|
1205
|
+
expect(historyTextBlock!.text).toContain(placeholder);
|
|
1206
|
+
expect(historyTextBlock!.text).not.toContain(realToken);
|
|
1207
|
+
|
|
1208
|
+
// The message_complete EVENT should also retain placeholders (persisted
|
|
1209
|
+
// to conversation store; real values leak on session reload otherwise)
|
|
1210
|
+
const completeEvents = events.filter(
|
|
1211
|
+
(e): e is Extract<AgentEvent, { type: 'message_complete' }> => e.type === 'message_complete',
|
|
1212
|
+
);
|
|
1213
|
+
const lastComplete = completeEvents[completeEvents.length - 1];
|
|
1214
|
+
const completeText = lastComplete.message.content.find(
|
|
1215
|
+
(b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text',
|
|
1216
|
+
);
|
|
1217
|
+
expect(completeText!.text).toContain(placeholder);
|
|
1218
|
+
expect(completeText!.text).not.toContain(realToken);
|
|
1219
|
+
|
|
1220
|
+
// The tool result content in provider history should contain the PLACEHOLDER,
|
|
1221
|
+
// NOT the raw token (model never sees the real value)
|
|
1222
|
+
const secondCallMessages = calls[1].messages;
|
|
1223
|
+
const toolResultMsg = secondCallMessages.find(
|
|
1224
|
+
(m) => m.role === 'user' && m.content.some((b) => b.type === 'tool_result'),
|
|
1225
|
+
);
|
|
1226
|
+
expect(toolResultMsg).toBeDefined();
|
|
1227
|
+
const toolResultBlock = toolResultMsg!.content.find(
|
|
1228
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
1229
|
+
);
|
|
1230
|
+
expect(toolResultBlock!.content).toContain(placeholder);
|
|
1231
|
+
expect(toolResultBlock!.content).not.toContain(realToken);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// 33. Streamed text_delta events have placeholders resolved to real values
|
|
1235
|
+
test('resolves sensitive output placeholders in streamed text_delta events', async () => {
|
|
1236
|
+
const placeholder = 'VELLUM_ASSISTANT_INVITE_CODE_STRM5678';
|
|
1237
|
+
const realToken = 'streamedRealToken';
|
|
1238
|
+
|
|
1239
|
+
const { provider } = createMockProvider([
|
|
1240
|
+
toolUseResponse('t1', 'bash', { command: 'invite' }),
|
|
1241
|
+
// Response text includes the placeholder
|
|
1242
|
+
textResponse(`Link: https://t.me/bot?start=iv_${placeholder}`),
|
|
1243
|
+
]);
|
|
1244
|
+
|
|
1245
|
+
const toolExecutor = async () => ({
|
|
1246
|
+
content: `https://t.me/bot?start=iv_${placeholder}`,
|
|
1247
|
+
isError: false,
|
|
1248
|
+
sensitiveBindings: [{ kind: 'invite_code' as const, placeholder, value: realToken }],
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
1252
|
+
const events: AgentEvent[] = [];
|
|
1253
|
+
await loop.run([userMessage], collectEvents(events));
|
|
1254
|
+
|
|
1255
|
+
// Collect all text_delta events from the final turn (after tool result)
|
|
1256
|
+
const textDeltas = events.filter(
|
|
1257
|
+
(e): e is Extract<AgentEvent, { type: 'text_delta' }> => e.type === 'text_delta',
|
|
1258
|
+
);
|
|
1259
|
+
const allStreamedText = textDeltas.map((e) => e.text).join('');
|
|
1260
|
+
|
|
1261
|
+
// Streamed text should contain the real token, not the placeholder
|
|
1262
|
+
expect(allStreamedText).toContain(realToken);
|
|
1263
|
+
expect(allStreamedText).not.toContain(placeholder);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// 34. Without sensitive bindings, text passes through unchanged
|
|
1267
|
+
test('text passes through unchanged when no sensitive bindings exist', async () => {
|
|
1268
|
+
const { provider } = createMockProvider([
|
|
1269
|
+
toolUseResponse('t1', 'read_file', { path: '/test.txt' }),
|
|
1270
|
+
textResponse('Normal response with no placeholders.'),
|
|
1271
|
+
]);
|
|
1272
|
+
|
|
1273
|
+
const toolExecutor = async () => ({
|
|
1274
|
+
content: 'file contents',
|
|
1275
|
+
isError: false,
|
|
1276
|
+
// No sensitiveBindings
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
1280
|
+
const events: AgentEvent[] = [];
|
|
1281
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
1282
|
+
|
|
1283
|
+
const lastAssistant = history[history.length - 1];
|
|
1284
|
+
const textBlock = lastAssistant.content.find(
|
|
1285
|
+
(b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text',
|
|
1286
|
+
);
|
|
1287
|
+
expect(textBlock!.text).toBe('Normal response with no placeholders.');
|
|
1288
|
+
});
|
|
1170
1289
|
});
|
|
@@ -36,6 +36,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
36
36
|
|
|
37
37
|
mock.module('../config/loader.js', () => ({
|
|
38
38
|
getConfig: () => ({
|
|
39
|
+
ui: {},
|
|
40
|
+
|
|
39
41
|
model: 'test',
|
|
40
42
|
provider: 'test',
|
|
41
43
|
apiKeys: {},
|
|
@@ -79,8 +81,11 @@ function makeIdleSession(opts?: {
|
|
|
79
81
|
setAssistantId: () => {},
|
|
80
82
|
setGuardianContext: () => {},
|
|
81
83
|
setCommandIntent: () => {},
|
|
84
|
+
setTurnChannelContext: () => {},
|
|
85
|
+
setTurnInterfaceContext: () => {},
|
|
82
86
|
updateClient: () => {},
|
|
83
87
|
enqueueMessage: () => ({ queued: false, requestId: 'noop' }),
|
|
88
|
+
hasAnyPendingConfirmation: () => false,
|
|
84
89
|
runAgentLoop: async (_content: string, _messageId: string, onEvent: (msg: ServerMessage) => void) => {
|
|
85
90
|
onEvent({ type: 'assistant_text_delta', text: 'Hello!' });
|
|
86
91
|
onEvent({ type: 'message_complete', sessionId: 'test-session' });
|
|
@@ -118,8 +123,11 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
118
123
|
setAssistantId: () => {},
|
|
119
124
|
setGuardianContext: () => {},
|
|
120
125
|
setCommandIntent: () => {},
|
|
126
|
+
setTurnChannelContext: () => {},
|
|
127
|
+
setTurnInterfaceContext: () => {},
|
|
121
128
|
updateClient: () => {},
|
|
122
129
|
enqueueMessage: () => ({ queued: false, requestId: 'noop' }),
|
|
130
|
+
hasAnyPendingConfirmation: () => false,
|
|
123
131
|
runAgentLoop: async (_content: string, _messageId: string, onEvent: (msg: ServerMessage) => void) => {
|
|
124
132
|
// Emit confirmation_request — this triggers the hub publisher to register
|
|
125
133
|
// the pending interaction
|
|
@@ -549,8 +557,8 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
549
557
|
});
|
|
550
558
|
|
|
551
559
|
expect(res.status).toBe(403);
|
|
552
|
-
const body = await res.json() as { error: string };
|
|
553
|
-
expect(body.error).toContain('pattern');
|
|
560
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
561
|
+
expect(body.error.message).toContain('pattern');
|
|
554
562
|
|
|
555
563
|
await stopServer();
|
|
556
564
|
});
|
|
@@ -579,8 +587,8 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
579
587
|
});
|
|
580
588
|
|
|
581
589
|
expect(res.status).toBe(403);
|
|
582
|
-
const body = await res.json() as { error: string };
|
|
583
|
-
expect(body.error).toContain('scope');
|
|
590
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
591
|
+
expect(body.error.message).toContain('scope');
|
|
584
592
|
|
|
585
593
|
await stopServer();
|
|
586
594
|
});
|
|
@@ -637,7 +645,7 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
637
645
|
const res = await fetch(url('messages'), {
|
|
638
646
|
method: 'POST',
|
|
639
647
|
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
|
|
640
|
-
body: JSON.stringify({ conversationKey: 'conv-auto', content: 'Run ls', sourceChannel: 'vellum' }),
|
|
648
|
+
body: JSON.stringify({ conversationKey: 'conv-auto', content: 'Run ls', sourceChannel: 'vellum', interface: 'macos' }),
|
|
641
649
|
});
|
|
642
650
|
expect(res.status).toBe(202);
|
|
643
651
|
|
|
@@ -37,6 +37,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
37
37
|
|
|
38
38
|
mock.module('../config/loader.js', () => ({
|
|
39
39
|
getConfig: () => ({
|
|
40
|
+
ui: {},
|
|
41
|
+
|
|
40
42
|
model: 'test',
|
|
41
43
|
provider: 'test',
|
|
42
44
|
apiKeys: {},
|
|
@@ -181,8 +183,8 @@ describe('SSE route — capacity limit', () => {
|
|
|
181
183
|
|
|
182
184
|
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub });
|
|
183
185
|
expect(response.status).toBe(503);
|
|
184
|
-
const body = await response.json() as { error: string };
|
|
185
|
-
expect(body.error).toMatch(/Too many concurrent connections/);
|
|
186
|
+
const body = await response.json() as { error: { message: string; code?: string } };
|
|
187
|
+
expect(body.error.message).toMatch(/Too many concurrent connections/);
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
test('returns 200 when hub has remaining capacity', () => {
|
|
@@ -64,10 +64,10 @@ describe('browser skill migration end-state', () => {
|
|
|
64
64
|
|
|
65
65
|
test('startup tool definition count is reduced (no browser tools)', () => {
|
|
66
66
|
const definitions = getAllToolDefinitions();
|
|
67
|
-
// Startup has ~
|
|
67
|
+
// Startup has ~15 eager + ~11 explicit definitions (no browser tools).
|
|
68
68
|
// Allow wider drift for unrelated tool additions while still failing if
|
|
69
|
-
// browser tools are reintroduced at startup (+
|
|
70
|
-
expect(definitions.length).toBeGreaterThanOrEqual(
|
|
69
|
+
// browser tools are reintroduced at startup (+14 definitions).
|
|
70
|
+
expect(definitions.length).toBeGreaterThanOrEqual(10);
|
|
71
71
|
expect(definitions.length).toBeLessThanOrEqual(50);
|
|
72
72
|
|
|
73
73
|
const defNames = definitions.map((d) => d.name);
|