@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d
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/docs/workspace-tools.md +42 -33
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
- package/openapi.yaml +74 -1
- package/package.json +1 -1
- package/scripts/test.sh +36 -15
- package/src/__tests__/actor-token-service.test.ts +36 -14
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/approval-cascade.test.ts +2 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
- package/src/__tests__/btw-routes.test.ts +2 -0
- package/src/__tests__/build-persisted-content.test.ts +2 -0
- package/src/__tests__/call-controller.test.ts +19 -0
- package/src/__tests__/channel-guardian.test.ts +94 -58
- package/src/__tests__/channel-reply-delivery.test.ts +2 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
- package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
- package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
- package/src/__tests__/computer-use-tools.test.ts +13 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
- package/src/__tests__/contacts-relay-reads.test.ts +13 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
- package/src/__tests__/conversation-history-web-search.test.ts +2 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-process-callsite.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +91 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/db-test-helpers.ts +5 -4
- package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
- package/src/__tests__/disk-pressure-guard.test.ts +41 -0
- package/src/__tests__/dm-persistence.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
- package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
- package/src/__tests__/filing-service.test.ts +2 -0
- package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
- package/src/__tests__/guardian-dispatch.test.ts +95 -1
- package/src/__tests__/guardian-outbound-http.test.ts +13 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +2 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
- package/src/__tests__/host-app-control-routes.test.ts +24 -30
- package/src/__tests__/host-bash-routes.test.ts +31 -41
- package/src/__tests__/host-browser-routes.test.ts +26 -32
- package/src/__tests__/host-cu-proxy.test.ts +299 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
- package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
- package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
- package/src/__tests__/http-user-message-parity.test.ts +167 -8
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/invite-redemption-service.test.ts +43 -0
- package/src/__tests__/llm-context-normalization.test.ts +105 -0
- package/src/__tests__/llm-usage-store.test.ts +25 -0
- package/src/__tests__/media-stream-server-integration.test.ts +127 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
- package/src/__tests__/messaging-send-tool.test.ts +2 -0
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/native-web-search.test.ts +2 -0
- package/src/__tests__/non-member-access-request.test.ts +189 -17
- package/src/__tests__/notification-broadcaster.test.ts +4 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
- package/src/__tests__/notification-deep-link.test.ts +6 -0
- package/src/__tests__/notification-guardian-path.test.ts +19 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/plugin-bootstrap.test.ts +3 -73
- package/src/__tests__/plugin-route-contribution.test.ts +4 -17
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
- package/src/__tests__/plugin-types.test.ts +0 -2
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/process-message-display-content.test.ts +2 -0
- package/src/__tests__/provider-usage-tracking.test.ts +39 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
- package/src/__tests__/registry.test.ts +3 -0
- package/src/__tests__/relay-server.test.ts +694 -25
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/secret-ingress-http.test.ts +14 -0
- package/src/__tests__/send-endpoint-busy.test.ts +30 -8
- package/src/__tests__/skills.test.ts +44 -0
- package/src/__tests__/slack-inbound-verification.test.ts +47 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
- package/src/__tests__/stt-hints.test.ts +44 -13
- package/src/__tests__/subagent-detail.test.ts +27 -0
- package/src/__tests__/subagent-disposal.test.ts +65 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
- package/src/__tests__/subagent-tools.test.ts +2 -0
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor.test.ts +16 -11
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +96 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
- package/src/__tests__/web-search-backend-failure.test.ts +2 -0
- package/src/__tests__/workspace-tool-loader.test.ts +195 -2
- package/src/agent/loop-exclusive-tool.test.ts +150 -0
- package/src/agent/loop.ts +56 -0
- package/src/api/constants/sse-replay.ts +41 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/llm-request-log-entry.ts +25 -0
- package/src/api/responses/subagent-detail.ts +17 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +10 -8
- package/src/calls/inbound-trust-reader.ts +17 -1
- package/src/calls/media-stream-server.ts +21 -0
- package/src/calls/relay-server.ts +167 -50
- package/src/calls/relay-setup-router.ts +37 -7
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +9 -12
- package/src/calls/twilio-routes.ts +14 -4
- package/src/cli/commands/__tests__/cache.test.ts +8 -1
- package/src/cli/commands/cache.ts +194 -181
- package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
- package/src/cli/commands/db/status.ts +37 -1
- package/src/cli/commands/mcp.ts +252 -218
- package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/worker.ts +175 -0
- package/src/cli/commands/plugins.ts +75 -3
- package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
- package/src/cli/lib/list-installed-plugins.ts +179 -1
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/loader.ts +36 -5
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/memory-v3.ts +7 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/timeouts.ts +8 -0
- package/src/config/seed-inference-profiles.ts +14 -5
- package/src/config/skills.ts +27 -5
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
- package/src/contacts/contacts-write.ts +3 -0
- package/src/contacts/guardian-delivery-reader.ts +223 -0
- package/src/daemon/conversation-agent-loop.ts +9 -0
- package/src/daemon/conversation-process.ts +39 -17
- package/src/daemon/conversation-surfaces.ts +8 -0
- package/src/daemon/conversation-tool-setup.ts +49 -16
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/disk-pressure-guard.ts +12 -2
- package/src/daemon/event-loop-watchdog.ts +28 -1
- package/src/daemon/external-plugins-bootstrap.ts +4 -34
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
- package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
- package/src/daemon/handlers/config-a2a.ts +6 -14
- package/src/daemon/handlers/config-channels.ts +78 -22
- package/src/daemon/handlers/conversations.ts +77 -0
- package/src/daemon/host-cu-proxy.ts +102 -11
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/memory-v2-startup.test.ts +72 -0
- package/src/daemon/memory-v2-startup.ts +87 -19
- package/src/daemon/server.ts +0 -4
- package/src/daemon/shutdown-handlers.ts +20 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
- package/src/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/memory/conversation-crud.ts +38 -0
- package/src/memory/db-connection.ts +22 -3
- package/src/memory/db-init.ts +36 -502
- package/src/memory/db-singleton.ts +6 -4
- package/src/memory/jobs-worker.ts +58 -0
- package/src/memory/llm-usage-store.ts +48 -20
- package/src/memory/memory-retrospective-job.ts +9 -8
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
- package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
- package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
- package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
- package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +90 -6
- package/src/memory/migrations/schema-introspection.ts +14 -0
- package/src/memory/migrations/validate-migration-state.ts +101 -66
- package/src/memory/prompt-override.ts +129 -0
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/infrastructure.ts +20 -0
- package/src/memory/steps.ts +573 -0
- package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
- package/src/memory/v2/cli-command-store.ts +75 -38
- package/src/memory/v2/prompts/consolidation.ts +13 -82
- package/src/memory/v2/prompts/router.ts +21 -93
- package/src/memory/v2/skill-store.ts +68 -31
- package/src/memory/watchdog-events-store.ts +87 -0
- package/src/memory/worker-control.ts +118 -0
- package/src/memory/worker-process.ts +72 -0
- package/src/notifications/__tests__/broadcaster.test.ts +16 -8
- package/src/notifications/__tests__/connected-channels.test.ts +114 -0
- package/src/notifications/__tests__/decision-engine.test.ts +78 -9
- package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
- package/src/notifications/broadcaster.ts +8 -1
- package/src/notifications/decision-engine.ts +15 -7
- package/src/notifications/destination-resolver.ts +68 -24
- package/src/notifications/emit-signal.ts +39 -14
- package/src/onboarding/checkin-event.test.ts +220 -0
- package/src/onboarding/checkin-event.ts +321 -0
- package/src/onboarding/schedule-checkin.ts +190 -0
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +7 -4
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +3 -5
- package/src/plugin-api/vision-support.test.ts +28 -4
- package/src/plugin-api/vision-support.ts +66 -31
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
- package/src/plugins/defaults/advisor/consult.ts +110 -6
- package/src/plugins/defaults/advisor/context-pack.ts +288 -0
- package/src/plugins/defaults/advisor/steering.ts +14 -2
- package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
- package/src/plugins/mtime-cache.ts +7 -2
- package/src/plugins/types.ts +0 -2
- package/src/providers/anthropic/client.ts +5 -0
- package/src/providers/call-site-routing.ts +4 -0
- package/src/providers/model-catalog.ts +16 -0
- package/src/providers/openai/responses-provider.ts +5 -0
- package/src/providers/openrouter/client.ts +5 -0
- package/src/providers/provider-send-message.ts +4 -0
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/retry.ts +4 -0
- package/src/providers/types.ts +9 -0
- package/src/providers/usage-tracking.ts +4 -0
- package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
- package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
- package/src/runtime/access-request-helper.ts +19 -39
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/anchored-guardian.test.ts +156 -0
- package/src/runtime/anchored-guardian.ts +135 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +9 -2
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
- package/src/runtime/auth/require-bound-guardian.ts +21 -11
- package/src/runtime/channel-verification-service.ts +56 -31
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +66 -7
- package/src/runtime/invite-redemption-service.ts +50 -18
- package/src/runtime/local-actor-identity.ts +76 -11
- package/src/runtime/local-principal-trust.ts +52 -0
- package/src/runtime/pending-interactions.ts +11 -1
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +8 -32
- package/src/runtime/routes/conversation-cli-routes.ts +4 -5
- package/src/runtime/routes/conversation-list-routes.ts +4 -7
- package/src/runtime/routes/conversation-routes.ts +74 -81
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +3 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -5
- package/src/runtime/routes/host-app-control-routes.ts +5 -4
- package/src/runtime/routes/host-bash-routes.ts +5 -4
- package/src/runtime/routes/host-browser-routes.ts +9 -11
- package/src/runtime/routes/host-cu-routes.ts +5 -4
- package/src/runtime/routes/host-file-routes.ts +5 -4
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +71 -0
- package/src/runtime/routes/mcp-auth-routes.ts +38 -15
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
- package/src/runtime/routes/subagents-routes.ts +5 -0
- package/src/runtime/routes/surface-action-routes.ts +51 -55
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +7 -9
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +85 -9
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +16 -0
- package/src/subagent/manager.ts +9 -0
- package/src/telemetry/types.ts +34 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +87 -3
- package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
- package/src/tools/ask-question/ask-question-tool.ts +13 -0
- package/src/tools/computer-use/definitions.ts +8 -2
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +18 -0
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +9 -2
- package/src/tools/types.ts +17 -2
- package/src/tools/workspace-tools/loader.ts +348 -244
- package/src/util/platform.ts +5 -0
- package/src/util/telemetry-db-path.ts +24 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
- package/src/daemon/workspace-tools-watcher.ts +0 -328
- package/src/memory/migrations/registry.ts +0 -573
|
@@ -87,6 +87,19 @@ mock.module("../prompts/user-reference.js", () => ({
|
|
|
87
87
|
},
|
|
88
88
|
}));
|
|
89
89
|
|
|
90
|
+
// ── Guardian delivery reader mock ───────────────────────────────────
|
|
91
|
+
//
|
|
92
|
+
// resolveGuardianLabel primes its displayName from the gateway binding via
|
|
93
|
+
// getGuardianDelivery at setup. Tests drive the binding through this list.
|
|
94
|
+
|
|
95
|
+
// Tests set this to drive the guardian binding directly. When null (the
|
|
96
|
+
// default), the guardian-delivery-reader mock below derives the binding from
|
|
97
|
+
// the DB-seeded createGuardianBinding setup. Single mock registration lives
|
|
98
|
+
// below since `mock.module` is process-global and last-write-wins in Bun.
|
|
99
|
+
let mockGuardianDeliveryList:
|
|
100
|
+
| Array<{ channelType: string; status: string; displayName: string | null }>
|
|
101
|
+
| null = null;
|
|
102
|
+
|
|
90
103
|
// ── Config mock ─────────────────────────────────────────────────────
|
|
91
104
|
|
|
92
105
|
const mockConfig = {
|
|
@@ -163,6 +176,103 @@ mock.module("../calls/channel-admission-reader.js", () => ({
|
|
|
163
176
|
},
|
|
164
177
|
}));
|
|
165
178
|
|
|
179
|
+
// ── Inbound trust verdict reader mock ───────────────────────────────
|
|
180
|
+
//
|
|
181
|
+
// Mid-call re-resolution (post verification/activation) prefers the gateway
|
|
182
|
+
// verdict via getInboundTrustVerdict, falling back to local resolution on a
|
|
183
|
+
// missing/failed/unusable verdict. Tests drive the verdict through
|
|
184
|
+
// `mockMidCallVerdict`; null (the default) exercises the local fallback. As
|
|
185
|
+
// with the admission reader, delegate to the real module for sibling files
|
|
186
|
+
// that load later in the same worker.
|
|
187
|
+
let mockMidCallVerdict:
|
|
188
|
+
| import("@vellumai/gateway-client").TrustVerdict
|
|
189
|
+
| null = null;
|
|
190
|
+
// When set, the mid-call re-resolution verdict reader blocks on this gate
|
|
191
|
+
// before returning, simulating a slow gateway round-trip so a test can drive a
|
|
192
|
+
// prompt into the re-resolution await window. The gate targets the mid-call
|
|
193
|
+
// re-resolution read (getInboundTrustVerdict) only — the per-caller redemption
|
|
194
|
+
// gate read (getPhoneCallerVerdict) resolves immediately so invite redemption
|
|
195
|
+
// reaches activation before the gated re-resolution runs.
|
|
196
|
+
let mockMidCallVerdictGate: Promise<void> | null = null;
|
|
197
|
+
const realInboundTrustReaderModule = {
|
|
198
|
+
...(await import("../calls/inbound-trust-reader.js")),
|
|
199
|
+
};
|
|
200
|
+
let inboundTrustMockActive = false;
|
|
201
|
+
mock.module("../calls/inbound-trust-reader.js", () => ({
|
|
202
|
+
...realInboundTrustReaderModule,
|
|
203
|
+
getInboundTrustVerdict: async (input: {
|
|
204
|
+
channelType: import("../channels/types.js").ChannelId;
|
|
205
|
+
actorExternalId?: string;
|
|
206
|
+
}) => {
|
|
207
|
+
if (!inboundTrustMockActive) {
|
|
208
|
+
return realInboundTrustReaderModule.getInboundTrustVerdict(input);
|
|
209
|
+
}
|
|
210
|
+
if (mockMidCallVerdictGate) await mockMidCallVerdictGate;
|
|
211
|
+
return mockMidCallVerdict;
|
|
212
|
+
},
|
|
213
|
+
getPhoneCallerVerdict: async (otherPartyNumber: string | undefined) => {
|
|
214
|
+
if (!inboundTrustMockActive) {
|
|
215
|
+
return realInboundTrustReaderModule.getPhoneCallerVerdict(
|
|
216
|
+
otherPartyNumber,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return mockMidCallVerdict;
|
|
220
|
+
},
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
// ── Guardian delivery reader ────────────────────────────────────────
|
|
224
|
+
//
|
|
225
|
+
// Guardian identity now resolves via the gateway delivery reader. Derive the
|
|
226
|
+
// list from the DB-seeded guardian bindings so the existing createGuardianBinding
|
|
227
|
+
// setup keeps driving guardian resolution without per-test changes.
|
|
228
|
+
const realContactStoreModule = {
|
|
229
|
+
...(await import("../contacts/contact-store.js")),
|
|
230
|
+
};
|
|
231
|
+
mock.module("../contacts/guardian-delivery-reader.js", () => ({
|
|
232
|
+
getGuardianDelivery: async () => {
|
|
233
|
+
// Tests that set mockGuardianDeliveryList drive the binding directly;
|
|
234
|
+
// otherwise derive from the DB-seeded createGuardianBinding bindings.
|
|
235
|
+
if (mockGuardianDeliveryList) return mockGuardianDeliveryList;
|
|
236
|
+
const guardians = realContactStoreModule.listGuardianChannels();
|
|
237
|
+
if (!guardians) return [];
|
|
238
|
+
return guardians.channels.map((ch) => ({
|
|
239
|
+
channelType: ch.type,
|
|
240
|
+
contactId: guardians.contact.id,
|
|
241
|
+
principalId: guardians.contact.principalId ?? null,
|
|
242
|
+
displayName: guardians.contact.displayName ?? null,
|
|
243
|
+
address: ch.address,
|
|
244
|
+
externalChatId: ch.externalChatId ?? null,
|
|
245
|
+
status: ch.status,
|
|
246
|
+
verifiedAt: ch.verifiedAt ?? null,
|
|
247
|
+
}));
|
|
248
|
+
},
|
|
249
|
+
guardianForChannel: (
|
|
250
|
+
list: Array<{ channelType: string; status: string }>,
|
|
251
|
+
channelType: string,
|
|
252
|
+
) => list.find((g) => g.channelType === channelType && g.status === "active"),
|
|
253
|
+
anyGuardian: (list: unknown[]) => list[0],
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
// ── Trust verdict consumer spy ──────────────────────────────────────
|
|
257
|
+
//
|
|
258
|
+
// Tracks whether the verdict mapper produced the final mid-call context, so a
|
|
259
|
+
// test can assert the local resolver was used instead (verdict not consumed).
|
|
260
|
+
let trustVerdictMapperUsed = false;
|
|
261
|
+
const realTrustVerdictConsumerModule = {
|
|
262
|
+
...(await import("../runtime/trust-verdict-consumer.js")),
|
|
263
|
+
};
|
|
264
|
+
mock.module("../runtime/trust-verdict-consumer.js", () => ({
|
|
265
|
+
...realTrustVerdictConsumerModule,
|
|
266
|
+
trustContextFromVerdict: (
|
|
267
|
+
...args: Parameters<
|
|
268
|
+
typeof realTrustVerdictConsumerModule.trustContextFromVerdict
|
|
269
|
+
>
|
|
270
|
+
) => {
|
|
271
|
+
trustVerdictMapperUsed = true;
|
|
272
|
+
return realTrustVerdictConsumerModule.trustContextFromVerdict(...args);
|
|
273
|
+
},
|
|
274
|
+
}));
|
|
275
|
+
|
|
166
276
|
// ── TTS provider mocks (for call-speech-output) ─────────────────────
|
|
167
277
|
|
|
168
278
|
let mockTtsProviderId: string = "elevenlabs";
|
|
@@ -308,10 +418,12 @@ await initializeDb();
|
|
|
308
418
|
// sibling files that load later in the same worker.
|
|
309
419
|
beforeAll(() => {
|
|
310
420
|
admissionMockActive = true;
|
|
421
|
+
inboundTrustMockActive = true;
|
|
311
422
|
});
|
|
312
423
|
|
|
313
424
|
afterAll(() => {
|
|
314
425
|
admissionMockActive = false;
|
|
426
|
+
inboundTrustMockActive = false;
|
|
315
427
|
resetDbForTesting();
|
|
316
428
|
});
|
|
317
429
|
|
|
@@ -477,8 +589,12 @@ describe("relay-server", () => {
|
|
|
477
589
|
inviteClaimGate = null;
|
|
478
590
|
mockUserReference = "my human";
|
|
479
591
|
mockAssistantName = "Vellum";
|
|
592
|
+
mockGuardianDeliveryList = null;
|
|
480
593
|
mockAdmissionPolicy = null;
|
|
481
594
|
mockAdmissionGate = null;
|
|
595
|
+
mockMidCallVerdict = null;
|
|
596
|
+
mockMidCallVerdictGate = null;
|
|
597
|
+
trustVerdictMapperUsed = false;
|
|
482
598
|
mockSendMessage.mockImplementation(createMockProviderResponse(["Hello"]));
|
|
483
599
|
mockConfig.calls.verification.enabled = false;
|
|
484
600
|
mockConfig.calls.verification.maxAttempts = 3;
|
|
@@ -1630,7 +1746,7 @@ describe("relay-server", () => {
|
|
|
1630
1746
|
// Guardian binding is NOT created by the assistant — the gateway owns
|
|
1631
1747
|
// binding creation for inbound voice verification. The assistant only
|
|
1632
1748
|
// transitions to connected state and starts the normal call flow.
|
|
1633
|
-
const binding = getGuardianBinding("self", "phone");
|
|
1749
|
+
const binding = await getGuardianBinding("self", "phone");
|
|
1634
1750
|
expect(binding).toBeNull();
|
|
1635
1751
|
|
|
1636
1752
|
// Orchestrator greeting should have fired
|
|
@@ -1701,7 +1817,7 @@ describe("relay-server", () => {
|
|
|
1701
1817
|
expect(relay.getConnectionState()).toBe("connected");
|
|
1702
1818
|
|
|
1703
1819
|
// Binding is NOT created by the assistant — gateway owns this.
|
|
1704
|
-
const binding = getGuardianBinding("self", "phone");
|
|
1820
|
+
const binding = await getGuardianBinding("self", "phone");
|
|
1705
1821
|
expect(binding).toBeNull();
|
|
1706
1822
|
|
|
1707
1823
|
// Greeting should have started
|
|
@@ -2223,6 +2339,9 @@ describe("relay-server", () => {
|
|
|
2223
2339
|
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
2224
2340
|
}
|
|
2225
2341
|
|
|
2342
|
+
// Let the fire-and-forget verification result handler flush
|
|
2343
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2344
|
+
|
|
2226
2345
|
// Verification should have succeeded
|
|
2227
2346
|
expect(relay.isVerificationSessionActive()).toBe(false);
|
|
2228
2347
|
|
|
@@ -4672,7 +4791,7 @@ describe("relay-server", () => {
|
|
|
4672
4791
|
expect(relay.getConnectionState()).toBe("connected");
|
|
4673
4792
|
|
|
4674
4793
|
// Guardian binding is NOT created by the assistant — gateway owns this.
|
|
4675
|
-
const binding = getGuardianBinding("self", "phone");
|
|
4794
|
+
const binding = await getGuardianBinding("self", "phone");
|
|
4676
4795
|
expect(binding).toBeNull();
|
|
4677
4796
|
|
|
4678
4797
|
// Normal greeting should fire (from mockSendMessage), not the handoff copy
|
|
@@ -4950,15 +5069,10 @@ describe("relay-server", () => {
|
|
|
4950
5069
|
test("guardian label: guardian persona name takes precedence over Contact.displayName", async () => {
|
|
4951
5070
|
mockUserReference = "Alice";
|
|
4952
5071
|
|
|
4953
|
-
//
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
guardianDeliveryChatId: "+15559990001",
|
|
4958
|
-
guardianPrincipalId: "+15559990001",
|
|
4959
|
-
verifiedVia: "test",
|
|
4960
|
-
metadataJson: JSON.stringify({ displayName: "Bob" }),
|
|
4961
|
-
});
|
|
5072
|
+
// Gateway binding carries a different displayName
|
|
5073
|
+
mockGuardianDeliveryList = [
|
|
5074
|
+
{ channelType: "phone", status: "active", displayName: "Bob" },
|
|
5075
|
+
];
|
|
4962
5076
|
|
|
4963
5077
|
ensureConversation("conv-label-user-md");
|
|
4964
5078
|
const session = createCallSession({
|
|
@@ -4995,15 +5109,10 @@ describe("relay-server", () => {
|
|
|
4995
5109
|
test("guardian label: Contact.displayName used when guardian persona name is empty", async () => {
|
|
4996
5110
|
mockUserReference = "my human";
|
|
4997
5111
|
|
|
4998
|
-
//
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
guardianDeliveryChatId: "+15559990002",
|
|
5003
|
-
guardianPrincipalId: "+15559990002",
|
|
5004
|
-
verifiedVia: "test",
|
|
5005
|
-
metadataJson: JSON.stringify({ displayName: "Charlie" }),
|
|
5006
|
-
});
|
|
5112
|
+
// Gateway binding carries the guardian displayName
|
|
5113
|
+
mockGuardianDeliveryList = [
|
|
5114
|
+
{ channelType: "phone", status: "active", displayName: "Charlie" },
|
|
5115
|
+
];
|
|
5007
5116
|
|
|
5008
5117
|
ensureConversation("conv-label-contact");
|
|
5009
5118
|
const session = createCallSession({
|
|
@@ -5039,10 +5148,8 @@ describe("relay-server", () => {
|
|
|
5039
5148
|
test("guardian label: DEFAULT_USER_REFERENCE used when both guardian persona name and Contact.displayName are empty", async () => {
|
|
5040
5149
|
mockUserReference = "my human";
|
|
5041
5150
|
|
|
5042
|
-
//
|
|
5043
|
-
|
|
5044
|
-
db.run("DELETE FROM contact_channels");
|
|
5045
|
-
db.run("DELETE FROM contacts");
|
|
5151
|
+
// Empty binding list so resolveGuardianLabel falls back to DEFAULT_USER_REFERENCE
|
|
5152
|
+
mockGuardianDeliveryList = [];
|
|
5046
5153
|
|
|
5047
5154
|
ensureConversation("conv-label-default");
|
|
5048
5155
|
const session = createCallSession({
|
|
@@ -5440,4 +5547,566 @@ describe("relay-server", () => {
|
|
|
5440
5547
|
relay.destroy();
|
|
5441
5548
|
});
|
|
5442
5549
|
});
|
|
5550
|
+
|
|
5551
|
+
// ── Mid-call trust re-resolution from the gateway verdict ───────────
|
|
5552
|
+
//
|
|
5553
|
+
// After a verification/activation success the relay re-resolves caller trust.
|
|
5554
|
+
// It prefers the gateway verdict (authoritative right after the gateway
|
|
5555
|
+
// updated the binding) and falls back to local resolution on a missing/
|
|
5556
|
+
// failed/unusable verdict so a blip never drops the call.
|
|
5557
|
+
|
|
5558
|
+
function readControllerTrustClass(relay: RelayConnection): string | undefined {
|
|
5559
|
+
return (
|
|
5560
|
+
relay.getController() as unknown as {
|
|
5561
|
+
trustContext?: { trustClass?: string };
|
|
5562
|
+
}
|
|
5563
|
+
)?.trustContext?.trustClass;
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
test("inbound guardian verification: re-resolves trust from the gateway verdict", async () => {
|
|
5567
|
+
ensureConversation("conv-midcall-verdict-guardian");
|
|
5568
|
+
const session = createCallSession({
|
|
5569
|
+
conversationId: "conv-midcall-verdict-guardian",
|
|
5570
|
+
provider: "twilio",
|
|
5571
|
+
fromNumber: "+15559999999",
|
|
5572
|
+
toNumber: "+15551111111",
|
|
5573
|
+
});
|
|
5574
|
+
|
|
5575
|
+
const secret = createPendingVoiceGuardianChallenge();
|
|
5576
|
+
|
|
5577
|
+
// The gateway verdict upgrades the caller to guardian post-verification.
|
|
5578
|
+
mockMidCallVerdict = {
|
|
5579
|
+
trustClass: "guardian",
|
|
5580
|
+
canonicalSenderId: "+15559999999",
|
|
5581
|
+
guardianExternalUserId: "+15559999999",
|
|
5582
|
+
guardianPrincipalId: "+15559999999",
|
|
5583
|
+
};
|
|
5584
|
+
|
|
5585
|
+
mockSendMessage.mockImplementation(
|
|
5586
|
+
createMockProviderResponse(["Hello, verified guardian!"]),
|
|
5587
|
+
);
|
|
5588
|
+
|
|
5589
|
+
const { relay } = createMockWs(session.id);
|
|
5590
|
+
|
|
5591
|
+
await relay.handleMessage(
|
|
5592
|
+
JSON.stringify({
|
|
5593
|
+
type: "setup",
|
|
5594
|
+
callSid: "CA_midcall_verdict_guardian",
|
|
5595
|
+
from: "+15559999999",
|
|
5596
|
+
to: "+15551111111",
|
|
5597
|
+
}),
|
|
5598
|
+
);
|
|
5599
|
+
|
|
5600
|
+
for (const digit of secret) {
|
|
5601
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
5602
|
+
}
|
|
5603
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5604
|
+
|
|
5605
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
5606
|
+
// Controller trust reflects the gateway verdict's upgraded class.
|
|
5607
|
+
expect(readControllerTrustClass(relay)).toBe("guardian");
|
|
5608
|
+
|
|
5609
|
+
relay.destroy();
|
|
5610
|
+
});
|
|
5611
|
+
|
|
5612
|
+
test("inbound guardian verification: resolutionFailed verdict falls back to local resolution without dropping the call", async () => {
|
|
5613
|
+
ensureConversation("conv-midcall-verdict-failed");
|
|
5614
|
+
const session = createCallSession({
|
|
5615
|
+
conversationId: "conv-midcall-verdict-failed",
|
|
5616
|
+
provider: "twilio",
|
|
5617
|
+
fromNumber: "+15559999999",
|
|
5618
|
+
toNumber: "+15551111111",
|
|
5619
|
+
});
|
|
5620
|
+
|
|
5621
|
+
const secret = createPendingVoiceGuardianChallenge();
|
|
5622
|
+
|
|
5623
|
+
// A failed verdict must fall back to local resolution — the call stays up.
|
|
5624
|
+
mockMidCallVerdict = {
|
|
5625
|
+
trustClass: "unknown",
|
|
5626
|
+
canonicalSenderId: null,
|
|
5627
|
+
resolutionFailed: true,
|
|
5628
|
+
};
|
|
5629
|
+
|
|
5630
|
+
mockSendMessage.mockImplementation(
|
|
5631
|
+
createMockProviderResponse(["Hello, how can I help you?"]),
|
|
5632
|
+
);
|
|
5633
|
+
|
|
5634
|
+
const { relay } = createMockWs(session.id);
|
|
5635
|
+
|
|
5636
|
+
await relay.handleMessage(
|
|
5637
|
+
JSON.stringify({
|
|
5638
|
+
type: "setup",
|
|
5639
|
+
callSid: "CA_midcall_verdict_failed",
|
|
5640
|
+
from: "+15559999999",
|
|
5641
|
+
to: "+15551111111",
|
|
5642
|
+
}),
|
|
5643
|
+
);
|
|
5644
|
+
|
|
5645
|
+
for (const digit of secret) {
|
|
5646
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
5647
|
+
}
|
|
5648
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5649
|
+
|
|
5650
|
+
// Fail-soft: verification still completes and the call connects.
|
|
5651
|
+
expect(relay.isVerificationSessionActive()).toBe(false);
|
|
5652
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
5653
|
+
expect(readControllerTrustClass(relay)).toBeDefined();
|
|
5654
|
+
|
|
5655
|
+
relay.destroy();
|
|
5656
|
+
});
|
|
5657
|
+
|
|
5658
|
+
test("inbound guardian verification: member-claiming but unusable verdict falls back to local resolution", async () => {
|
|
5659
|
+
ensureConversation("conv-midcall-verdict-unusable");
|
|
5660
|
+
const session = createCallSession({
|
|
5661
|
+
conversationId: "conv-midcall-verdict-unusable",
|
|
5662
|
+
provider: "twilio",
|
|
5663
|
+
fromNumber: "+15559999999",
|
|
5664
|
+
toNumber: "+15551111111",
|
|
5665
|
+
});
|
|
5666
|
+
|
|
5667
|
+
const secret = createPendingVoiceGuardianChallenge();
|
|
5668
|
+
|
|
5669
|
+
// Claims a member (contactId/channelId) but the ACL can't be reassembled
|
|
5670
|
+
// (missing status/policy) — mirrors the setup path's unusable condition.
|
|
5671
|
+
mockMidCallVerdict = {
|
|
5672
|
+
trustClass: "trusted_contact",
|
|
5673
|
+
canonicalSenderId: "+15559999999",
|
|
5674
|
+
contactId: "ct_unusable",
|
|
5675
|
+
channelId: "ch_unusable",
|
|
5676
|
+
};
|
|
5677
|
+
|
|
5678
|
+
mockSendMessage.mockImplementation(
|
|
5679
|
+
createMockProviderResponse(["Hello there."]),
|
|
5680
|
+
);
|
|
5681
|
+
|
|
5682
|
+
const { relay } = createMockWs(session.id);
|
|
5683
|
+
|
|
5684
|
+
await relay.handleMessage(
|
|
5685
|
+
JSON.stringify({
|
|
5686
|
+
type: "setup",
|
|
5687
|
+
callSid: "CA_midcall_verdict_unusable",
|
|
5688
|
+
from: "+15559999999",
|
|
5689
|
+
to: "+15551111111",
|
|
5690
|
+
}),
|
|
5691
|
+
);
|
|
5692
|
+
|
|
5693
|
+
for (const digit of secret) {
|
|
5694
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
5695
|
+
}
|
|
5696
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5697
|
+
|
|
5698
|
+
// Fail-soft: unusable verdict does not drop the call; local fallback fires.
|
|
5699
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
5700
|
+
expect(readControllerTrustClass(relay)).toBeDefined();
|
|
5701
|
+
|
|
5702
|
+
relay.destroy();
|
|
5703
|
+
});
|
|
5704
|
+
|
|
5705
|
+
test("inbound guardian verification: memberless unknown verdict falls back to local resolution (just-activated invitee not downgraded)", async () => {
|
|
5706
|
+
ensureConversation("conv-midcall-verdict-unknown");
|
|
5707
|
+
const session = createCallSession({
|
|
5708
|
+
conversationId: "conv-midcall-verdict-unknown",
|
|
5709
|
+
provider: "twilio",
|
|
5710
|
+
fromNumber: "+15559999999",
|
|
5711
|
+
toNumber: "+15551111111",
|
|
5712
|
+
});
|
|
5713
|
+
|
|
5714
|
+
const secret = createPendingVoiceGuardianChallenge();
|
|
5715
|
+
|
|
5716
|
+
// Invite redemption writes the channel assistant-side, so right after
|
|
5717
|
+
// activation the gateway has no member and returns a memberless unknown
|
|
5718
|
+
// verdict. Mid-call re-resolution must treat it as a stale gateway view
|
|
5719
|
+
// and fall back to local resolution (which has the fresh channel) rather
|
|
5720
|
+
// than downgrade the just-activated invitee to unknown.
|
|
5721
|
+
mockMidCallVerdict = {
|
|
5722
|
+
trustClass: "unknown",
|
|
5723
|
+
canonicalSenderId: "+15559999999",
|
|
5724
|
+
};
|
|
5725
|
+
|
|
5726
|
+
mockSendMessage.mockImplementation(
|
|
5727
|
+
createMockProviderResponse(["Hello there."]),
|
|
5728
|
+
);
|
|
5729
|
+
|
|
5730
|
+
const { relay } = createMockWs(session.id);
|
|
5731
|
+
|
|
5732
|
+
await relay.handleMessage(
|
|
5733
|
+
JSON.stringify({
|
|
5734
|
+
type: "setup",
|
|
5735
|
+
callSid: "CA_midcall_verdict_unknown",
|
|
5736
|
+
from: "+15559999999",
|
|
5737
|
+
to: "+15551111111",
|
|
5738
|
+
}),
|
|
5739
|
+
);
|
|
5740
|
+
|
|
5741
|
+
for (const digit of secret) {
|
|
5742
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
5743
|
+
}
|
|
5744
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5745
|
+
|
|
5746
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
5747
|
+
// Local resolver produced the final context; the unknown verdict was not
|
|
5748
|
+
// consumed as authoritative.
|
|
5749
|
+
expect(trustVerdictMapperUsed).toBe(false);
|
|
5750
|
+
expect(readControllerTrustClass(relay)).toBeDefined();
|
|
5751
|
+
|
|
5752
|
+
relay.destroy();
|
|
5753
|
+
});
|
|
5754
|
+
|
|
5755
|
+
function readControllerMemberStatus(
|
|
5756
|
+
relay: RelayConnection,
|
|
5757
|
+
): string | undefined {
|
|
5758
|
+
return (
|
|
5759
|
+
relay.getController() as unknown as {
|
|
5760
|
+
trustContext?: { memberStatus?: string };
|
|
5761
|
+
}
|
|
5762
|
+
)?.trustContext?.memberStatus;
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
test("inbound guardian verification: memberful blocked unknown verdict is honored (verdict path enforces blocked status)", async () => {
|
|
5766
|
+
ensureConversation("conv-midcall-verdict-blocked");
|
|
5767
|
+
const session = createCallSession({
|
|
5768
|
+
conversationId: "conv-midcall-verdict-blocked",
|
|
5769
|
+
provider: "twilio",
|
|
5770
|
+
fromNumber: "+15559999999",
|
|
5771
|
+
toNumber: "+15551111111",
|
|
5772
|
+
});
|
|
5773
|
+
|
|
5774
|
+
const secret = createPendingVoiceGuardianChallenge();
|
|
5775
|
+
|
|
5776
|
+
mockSendMessage.mockImplementation(
|
|
5777
|
+
createMockProviderResponse(["Hello there."]),
|
|
5778
|
+
);
|
|
5779
|
+
|
|
5780
|
+
const { relay } = createMockWs(session.id);
|
|
5781
|
+
|
|
5782
|
+
// Setup resolves locally (no verdict) so the pending guardian challenge
|
|
5783
|
+
// drives verification rather than denying at the door.
|
|
5784
|
+
await relay.handleMessage(
|
|
5785
|
+
JSON.stringify({
|
|
5786
|
+
type: "setup",
|
|
5787
|
+
callSid: "CA_midcall_verdict_blocked",
|
|
5788
|
+
from: "+15559999999",
|
|
5789
|
+
to: "+15551111111",
|
|
5790
|
+
}),
|
|
5791
|
+
);
|
|
5792
|
+
|
|
5793
|
+
// The gateway classifies a blocked member as trustClass "unknown" but still
|
|
5794
|
+
// carries contactId/channelId and the deny ACL. This memberful unknown must
|
|
5795
|
+
// take the verdict path on mid-call re-resolution so its blocked status is
|
|
5796
|
+
// enforced — not fall back to local, which could miss a stale block.
|
|
5797
|
+
mockMidCallVerdict = {
|
|
5798
|
+
trustClass: "unknown",
|
|
5799
|
+
canonicalSenderId: "+15559999999",
|
|
5800
|
+
contactId: "ct_blocked",
|
|
5801
|
+
channelId: "ch_blocked",
|
|
5802
|
+
status: "blocked",
|
|
5803
|
+
policy: "deny",
|
|
5804
|
+
};
|
|
5805
|
+
|
|
5806
|
+
for (const digit of secret) {
|
|
5807
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
5808
|
+
}
|
|
5809
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5810
|
+
|
|
5811
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
5812
|
+
// Verdict path consumed the memberful unknown verdict; blocked status lands.
|
|
5813
|
+
expect(trustVerdictMapperUsed).toBe(true);
|
|
5814
|
+
expect(readControllerMemberStatus(relay)).toBe("blocked");
|
|
5815
|
+
|
|
5816
|
+
relay.destroy();
|
|
5817
|
+
});
|
|
5818
|
+
|
|
5819
|
+
test("inbound guardian verification: memberful revoked unknown verdict is honored (verdict path enforces revoked status)", async () => {
|
|
5820
|
+
ensureConversation("conv-midcall-verdict-revoked");
|
|
5821
|
+
const session = createCallSession({
|
|
5822
|
+
conversationId: "conv-midcall-verdict-revoked",
|
|
5823
|
+
provider: "twilio",
|
|
5824
|
+
fromNumber: "+15559999999",
|
|
5825
|
+
toNumber: "+15551111111",
|
|
5826
|
+
});
|
|
5827
|
+
|
|
5828
|
+
const secret = createPendingVoiceGuardianChallenge();
|
|
5829
|
+
|
|
5830
|
+
mockSendMessage.mockImplementation(
|
|
5831
|
+
createMockProviderResponse(["Hello there."]),
|
|
5832
|
+
);
|
|
5833
|
+
|
|
5834
|
+
const { relay } = createMockWs(session.id);
|
|
5835
|
+
|
|
5836
|
+
await relay.handleMessage(
|
|
5837
|
+
JSON.stringify({
|
|
5838
|
+
type: "setup",
|
|
5839
|
+
callSid: "CA_midcall_verdict_revoked",
|
|
5840
|
+
from: "+15559999999",
|
|
5841
|
+
to: "+15551111111",
|
|
5842
|
+
}),
|
|
5843
|
+
);
|
|
5844
|
+
|
|
5845
|
+
mockMidCallVerdict = {
|
|
5846
|
+
trustClass: "unknown",
|
|
5847
|
+
canonicalSenderId: "+15559999999",
|
|
5848
|
+
contactId: "ct_revoked",
|
|
5849
|
+
channelId: "ch_revoked",
|
|
5850
|
+
status: "revoked",
|
|
5851
|
+
policy: "deny",
|
|
5852
|
+
};
|
|
5853
|
+
|
|
5854
|
+
for (const digit of secret) {
|
|
5855
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
5856
|
+
}
|
|
5857
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5858
|
+
|
|
5859
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
5860
|
+
expect(trustVerdictMapperUsed).toBe(true);
|
|
5861
|
+
expect(readControllerMemberStatus(relay)).toBe("revoked");
|
|
5862
|
+
|
|
5863
|
+
relay.destroy();
|
|
5864
|
+
});
|
|
5865
|
+
|
|
5866
|
+
test("a prompt arriving during the mid-call re-resolution await is deferred and runs under the upgraded trust", async () => {
|
|
5867
|
+
ensureConversation("conv-midcall-race");
|
|
5868
|
+
const session = createCallSession({
|
|
5869
|
+
conversationId: "conv-midcall-race",
|
|
5870
|
+
provider: "twilio",
|
|
5871
|
+
fromNumber: "+15559999999",
|
|
5872
|
+
toNumber: "+15551111111",
|
|
5873
|
+
});
|
|
5874
|
+
|
|
5875
|
+
const secret = createPendingVoiceGuardianChallenge();
|
|
5876
|
+
|
|
5877
|
+
// Capture the controller's trust class at the moment a turn actually fires,
|
|
5878
|
+
// so we can prove the deferred prompt did not run under the stale context.
|
|
5879
|
+
const trustClassAtTurn: Array<string | undefined> = [];
|
|
5880
|
+
mockSendMessage.mockImplementation((...args: unknown[]) => {
|
|
5881
|
+
trustClassAtTurn.push(readControllerTrustClass(relay));
|
|
5882
|
+
return createMockProviderResponse(["Hello, verified guardian!"])(
|
|
5883
|
+
...(args as Parameters<ReturnType<typeof createMockProviderResponse>>),
|
|
5884
|
+
);
|
|
5885
|
+
});
|
|
5886
|
+
|
|
5887
|
+
const { relay } = createMockWs(session.id);
|
|
5888
|
+
|
|
5889
|
+
// Setup resolves locally (no verdict) so the pending challenge drives
|
|
5890
|
+
// verification; the gated guardian verdict is armed only for the mid-call
|
|
5891
|
+
// re-resolution below.
|
|
5892
|
+
await relay.handleMessage(
|
|
5893
|
+
JSON.stringify({
|
|
5894
|
+
type: "setup",
|
|
5895
|
+
callSid: "CA_midcall_race",
|
|
5896
|
+
from: "+15559999999",
|
|
5897
|
+
to: "+15551111111",
|
|
5898
|
+
}),
|
|
5899
|
+
);
|
|
5900
|
+
|
|
5901
|
+
// The gateway verdict upgrades the caller to guardian, but the round-trip is
|
|
5902
|
+
// slow — gate it so a prompt can land in the re-resolution await window.
|
|
5903
|
+
mockMidCallVerdict = {
|
|
5904
|
+
trustClass: "guardian",
|
|
5905
|
+
canonicalSenderId: "+15559999999",
|
|
5906
|
+
guardianExternalUserId: "+15559999999",
|
|
5907
|
+
guardianPrincipalId: "+15559999999",
|
|
5908
|
+
};
|
|
5909
|
+
let releaseVerdict!: () => void;
|
|
5910
|
+
mockMidCallVerdictGate = new Promise<void>((resolve) => {
|
|
5911
|
+
releaseVerdict = resolve;
|
|
5912
|
+
});
|
|
5913
|
+
|
|
5914
|
+
// Enter the gated re-resolution: the final DTMF digit triggers
|
|
5915
|
+
// handleVerificationCodeResult, which awaits the slow verdict.
|
|
5916
|
+
const digits = secret.split("");
|
|
5917
|
+
for (const digit of digits.slice(0, -1)) {
|
|
5918
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
5919
|
+
}
|
|
5920
|
+
const verificationDone = relay.handleMessage(
|
|
5921
|
+
JSON.stringify({ type: "dtmf", digit: digits[digits.length - 1] }),
|
|
5922
|
+
);
|
|
5923
|
+
// Let the verdict await begin (connectionState is now "connected", guard set).
|
|
5924
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
5925
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
5926
|
+
// Trust is still stale (verdict gated) — caller is not yet guardian.
|
|
5927
|
+
expect(readControllerTrustClass(relay)).not.toBe("guardian");
|
|
5928
|
+
|
|
5929
|
+
// Prompt arrives mid-await: it must be deferred, not processed under stale trust.
|
|
5930
|
+
await relay.handleMessage(
|
|
5931
|
+
JSON.stringify({
|
|
5932
|
+
type: "prompt",
|
|
5933
|
+
voicePrompt: "Are my appointments confirmed?",
|
|
5934
|
+
lang: "en-US",
|
|
5935
|
+
last: true,
|
|
5936
|
+
}),
|
|
5937
|
+
);
|
|
5938
|
+
// No turn yet — the prompt was buffered, not run under the stale context.
|
|
5939
|
+
expect(trustClassAtTurn).toHaveLength(0);
|
|
5940
|
+
|
|
5941
|
+
// Release the verdict; re-resolution installs the upgraded context, then the
|
|
5942
|
+
// deferred prompt is flushed and its turn runs under guardian trust.
|
|
5943
|
+
releaseVerdict();
|
|
5944
|
+
await verificationDone;
|
|
5945
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
5946
|
+
|
|
5947
|
+
expect(readControllerTrustClass(relay)).toBe("guardian");
|
|
5948
|
+
expect(trustClassAtTurn.length).toBeGreaterThan(0);
|
|
5949
|
+
expect(trustClassAtTurn.every((c) => c === "guardian")).toBe(true);
|
|
5950
|
+
|
|
5951
|
+
relay.destroy();
|
|
5952
|
+
});
|
|
5953
|
+
|
|
5954
|
+
test("invite redemption: a prompt buffered during re-resolution runs as a real turn after activation (not dropped)", async () => {
|
|
5955
|
+
ensureConversation("conv-midcall-invite-flush");
|
|
5956
|
+
const session = createCallSession({
|
|
5957
|
+
conversationId: "conv-midcall-invite-flush",
|
|
5958
|
+
provider: "twilio",
|
|
5959
|
+
fromNumber: "+15558887777",
|
|
5960
|
+
toNumber: "+15551111111",
|
|
5961
|
+
});
|
|
5962
|
+
|
|
5963
|
+
const code = generateVoiceCode(6);
|
|
5964
|
+
createInvite({
|
|
5965
|
+
sourceChannel: "phone",
|
|
5966
|
+
contactId: createTargetContact("Alice"),
|
|
5967
|
+
maxUses: 1,
|
|
5968
|
+
expectedExternalUserId: "+15558887777",
|
|
5969
|
+
voiceCodeHash: hashVoiceCode(code),
|
|
5970
|
+
voiceCodeDigits: 6,
|
|
5971
|
+
});
|
|
5972
|
+
|
|
5973
|
+
const turnCountBefore = mockSendMessage.mock.calls.length;
|
|
5974
|
+
mockSendMessage.mockImplementation(
|
|
5975
|
+
createMockProviderResponse(["Sure, here you go."]),
|
|
5976
|
+
);
|
|
5977
|
+
|
|
5978
|
+
const { relay } = createMockWs(session.id);
|
|
5979
|
+
|
|
5980
|
+
await relay.handleMessage(
|
|
5981
|
+
JSON.stringify({
|
|
5982
|
+
type: "setup",
|
|
5983
|
+
callSid: "CA_midcall_invite_flush",
|
|
5984
|
+
from: "+15558887777",
|
|
5985
|
+
to: "+15551111111",
|
|
5986
|
+
}),
|
|
5987
|
+
);
|
|
5988
|
+
expect(relay.getConnectionState()).toBe("verification_pending");
|
|
5989
|
+
|
|
5990
|
+
// Gate the mid-call verdict so the prompt lands inside the re-resolution
|
|
5991
|
+
// await, after activation flips connectionState to "connected".
|
|
5992
|
+
let releaseVerdict!: () => void;
|
|
5993
|
+
mockMidCallVerdictGate = new Promise<void>((resolve) => {
|
|
5994
|
+
releaseVerdict = resolve;
|
|
5995
|
+
});
|
|
5996
|
+
|
|
5997
|
+
const digits = code.split("");
|
|
5998
|
+
for (const digit of digits.slice(0, -1)) {
|
|
5999
|
+
await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
|
|
6000
|
+
}
|
|
6001
|
+
const redemptionDone = relay.handleMessage(
|
|
6002
|
+
JSON.stringify({ type: "dtmf", digit: digits[digits.length - 1] }),
|
|
6003
|
+
);
|
|
6004
|
+
// Let the redemption chain (gateway claim, caller-verdict read, DB write)
|
|
6005
|
+
// drain up to activation, which flips the state to "connected" before
|
|
6006
|
+
// entering the gated re-resolution.
|
|
6007
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6008
|
+
// Activation already reached the terminal state before re-resolution.
|
|
6009
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
6010
|
+
|
|
6011
|
+
await relay.handleMessage(
|
|
6012
|
+
JSON.stringify({
|
|
6013
|
+
type: "prompt",
|
|
6014
|
+
voicePrompt: "What's on my calendar today?",
|
|
6015
|
+
lang: "en-US",
|
|
6016
|
+
last: true,
|
|
6017
|
+
}),
|
|
6018
|
+
);
|
|
6019
|
+
// Buffered, not yet run (verdict gated).
|
|
6020
|
+
expect(mockSendMessage.mock.calls.length).toBe(turnCountBefore);
|
|
6021
|
+
|
|
6022
|
+
releaseVerdict();
|
|
6023
|
+
await redemptionDone;
|
|
6024
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
6025
|
+
|
|
6026
|
+
// Flushed onto the real-turn path: the prompt produced an LLM turn rather
|
|
6027
|
+
// than being dropped by the verification-pending branch.
|
|
6028
|
+
expect(mockSendMessage.mock.calls.length).toBeGreaterThan(turnCountBefore);
|
|
6029
|
+
|
|
6030
|
+
relay.destroy();
|
|
6031
|
+
});
|
|
6032
|
+
|
|
6033
|
+
test("access-request approval: a prompt buffered during re-resolution runs as a real turn (not misrouted to wait-state)", async () => {
|
|
6034
|
+
ensureConversation("conv-midcall-access-flush");
|
|
6035
|
+
const session = createCallSession({
|
|
6036
|
+
conversationId: "conv-midcall-access-flush",
|
|
6037
|
+
provider: "twilio",
|
|
6038
|
+
fromNumber: "+15557770003",
|
|
6039
|
+
toNumber: "+15551111111",
|
|
6040
|
+
});
|
|
6041
|
+
|
|
6042
|
+
const turnCountBefore = mockSendMessage.mock.calls.length;
|
|
6043
|
+
mockSendMessage.mockImplementation(
|
|
6044
|
+
createMockProviderResponse(["Sure, here you go."]),
|
|
6045
|
+
);
|
|
6046
|
+
|
|
6047
|
+
const { relay } = createMockWs(session.id);
|
|
6048
|
+
|
|
6049
|
+
await relay.handleMessage(
|
|
6050
|
+
JSON.stringify({
|
|
6051
|
+
type: "setup",
|
|
6052
|
+
callSid: "CA_midcall_access_flush",
|
|
6053
|
+
from: "+15557770003",
|
|
6054
|
+
to: "+15551111111",
|
|
6055
|
+
}),
|
|
6056
|
+
);
|
|
6057
|
+
|
|
6058
|
+
await relay.handleMessage(
|
|
6059
|
+
JSON.stringify({
|
|
6060
|
+
type: "prompt",
|
|
6061
|
+
voicePrompt: "Bob Smith",
|
|
6062
|
+
lang: "en-US",
|
|
6063
|
+
last: true,
|
|
6064
|
+
}),
|
|
6065
|
+
);
|
|
6066
|
+
expect(relay.getConnectionState()).toBe("awaiting_guardian_decision");
|
|
6067
|
+
|
|
6068
|
+
// Gate the mid-call verdict so the prompt lands inside the re-resolution
|
|
6069
|
+
// await triggered by the approval poll.
|
|
6070
|
+
let releaseVerdict!: () => void;
|
|
6071
|
+
mockMidCallVerdictGate = new Promise<void>((resolve) => {
|
|
6072
|
+
releaseVerdict = resolve;
|
|
6073
|
+
});
|
|
6074
|
+
|
|
6075
|
+
const pending = listCanonicalGuardianRequests({
|
|
6076
|
+
status: "pending",
|
|
6077
|
+
requesterExternalUserId: "+15557770003",
|
|
6078
|
+
sourceChannel: "phone",
|
|
6079
|
+
kind: "access_request",
|
|
6080
|
+
});
|
|
6081
|
+
expect(pending.length).toBe(1);
|
|
6082
|
+
resolveCanonicalGuardianRequest(pending[0].id, "pending", {
|
|
6083
|
+
status: "approved",
|
|
6084
|
+
answerText: undefined,
|
|
6085
|
+
decidedByExternalUserId: undefined,
|
|
6086
|
+
});
|
|
6087
|
+
|
|
6088
|
+
// Let the poll detect approval and enter the gated re-resolution.
|
|
6089
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
6090
|
+
expect(relay.getConnectionState()).toBe("connected");
|
|
6091
|
+
|
|
6092
|
+
await relay.handleMessage(
|
|
6093
|
+
JSON.stringify({
|
|
6094
|
+
type: "prompt",
|
|
6095
|
+
voicePrompt: "What's on my calendar today?",
|
|
6096
|
+
lang: "en-US",
|
|
6097
|
+
last: true,
|
|
6098
|
+
}),
|
|
6099
|
+
);
|
|
6100
|
+
// Buffered, not yet run (verdict gated).
|
|
6101
|
+
expect(mockSendMessage.mock.calls.length).toBe(turnCountBefore);
|
|
6102
|
+
|
|
6103
|
+
releaseVerdict();
|
|
6104
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
6105
|
+
|
|
6106
|
+
// Flushed onto the real-turn path rather than the awaiting-guardian-decision
|
|
6107
|
+
// wait-state classifier: the prompt produced an LLM turn.
|
|
6108
|
+
expect(mockSendMessage.mock.calls.length).toBeGreaterThan(turnCountBefore);
|
|
6109
|
+
|
|
6110
|
+
relay.destroy();
|
|
6111
|
+
});
|
|
5443
6112
|
});
|